From 078490cdd54aefb0b9454bfbea4e0f95c0e2a320 Mon Sep 17 00:00:00 2001 From: jake <77554505+brxken128@users.noreply.github.com> Date: Wed, 11 Oct 2023 01:58:59 +0100 Subject: [PATCH] [ENG-1143] Media View sort by date taken (#1390) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * clippy -_- * migrations * alter the migration so it just renames the `dimensions` field (no db re-creation required) * remove confusing semver addition for `libheif-sys` * remove warning on the migration as it's just a rename * add sort opts for resolution and date image was truly taken * major serde ckeanup & add epoch_time and pixel_count * rename, cleanup et optimise * clippy * ignore this mess * bindings * add explanation to schema * comment out dt test * better comment and WIP time * cleanup rust * failed timezone attempt * remove image resolution as a sort by option * update schema (and rename the dimensions table instead of dropping it) * just show raw date * add comments and update bindings * fix migration hopefully * fix broken migration --------- Co-authored-by: Jamie Pine <32987599+jamiepine@users.noreply.github.com> Co-authored-by: Jamie Pine Co-authored-by: Vítor Vasconcellos --- .../migration.sql | 28 +++++ core/prisma/schema.prisma | 18 ++- core/src/api/search.rs | 23 ++++ core/src/object/media/mod.rs | 30 ++--- crates/media-metadata/src/image/datetime.rs | 109 ++++++++++++++++++ crates/media-metadata/src/image/mod.rs | 20 ++-- .../image/{dimensions.rs => resolution.rs} | 16 +-- crates/media-metadata/src/image/time.rs | 101 ---------------- .../Explorer/Inspector/MediaData.tsx | 53 +++++++-- interface/app/$libraryId/Explorer/store.ts | 10 +- packages/client/src/core.ts | 27 +++-- 11 files changed, 261 insertions(+), 174 deletions(-) create mode 100644 core/prisma/migrations/20231005202254_sort_by_image_taken/migration.sql create mode 100644 crates/media-metadata/src/image/datetime.rs rename crates/media-metadata/src/image/{dimensions.rs => resolution.rs} (69%) delete mode 100644 crates/media-metadata/src/image/time.rs diff --git a/core/prisma/migrations/20231005202254_sort_by_image_taken/migration.sql b/core/prisma/migrations/20231005202254_sort_by_image_taken/migration.sql new file mode 100644 index 000000000..ab0ead21b --- /dev/null +++ b/core/prisma/migrations/20231005202254_sort_by_image_taken/migration.sql @@ -0,0 +1,28 @@ +/* + Warnings: + + - You are about to drop the column `dimensions` on the `media_data` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_media_data" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "resolution" BLOB, + "media_date" BLOB, + "media_location" BLOB, + "camera_data" BLOB, + "artist" TEXT, + "description" TEXT, + "copyright" TEXT, + "exif_version" TEXT, + "epoch_time" BIGINT, + "object_id" INTEGER NOT NULL, + CONSTRAINT "media_data_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); +INSERT INTO "new_media_data" ("artist", "camera_data", "copyright", "description", "exif_version", "id", "media_date", "media_location", "object_id") SELECT "artist", "camera_data", "copyright", "description", "exif_version", "id", "media_date", "media_location", "object_id" FROM "media_data"; +DROP TABLE "media_data"; +ALTER TABLE "new_media_data" RENAME TO "media_data"; +CREATE UNIQUE INDEX "media_data_object_id_key" ON "media_data"("object_id"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index c9cf6807d..184ed33a9 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -173,12 +173,12 @@ model FilePath { // the name and extension, MUST have 'COLLATE NOCASE' in migration name String? extension String? - hidden Boolean? + hidden Boolean? size_in_bytes String? // deprecated size_in_bytes_bytes Bytes? - inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite + inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite // the unique Object for this file path object_id Int? @@ -242,7 +242,9 @@ model Object { @@map("object") } -// if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as the field is unique, however this record is kept to tell the indexer (upon discovering this CAS) that there is alternate versions of the file and to check by a full integrity hash to define for which to associate with. +// if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as +//the field is unique, however this record is kept to tell the indexer (upon discovering this CAS) that +//there is alternate versions of the file and to check by a full integrity hash to define for which to associate with. // @brendan: nah this probably won't fly // model FileConflict { // original_object_id Int @unique @@ -296,7 +298,7 @@ model Object { model MediaData { id Int @id @default(autoincrement()) - dimensions Bytes? + resolution Bytes? media_date Bytes? media_location Bytes? camera_data Bytes? @@ -305,11 +307,17 @@ model MediaData { copyright String? exif_version String? + // purely for sorting/ordering, never sent to the frontend as they'd be useless + // these are also usually one-way, and not reversible + // (e.g. we can't get `MediaDate::Utc(2023-09-26T22:04:37+01:00)` from `1695758677` as we don't store the TZ) + epoch_time BigInt? // time since unix epoch + // video-specific // duration Int? // fps Int? // streams Int? - // codecs String? // eg: "h264,acc" + // video_codec String? // eg: "h264, h265, av1" + // audio_codec String? // eg: "opus" object_id Int @unique object Object @relation(fields: [object_id], references: [id], onDelete: Cascade) diff --git a/core/src/api/search.rs b/core/src/api/search.rs index a87767cc1..4391a227e 100644 --- a/core/src/api/search.rs +++ b/core/src/api/search.rs @@ -17,6 +17,7 @@ use std::{collections::BTreeSet, path::PathBuf}; use chrono::{DateTime, FixedOffset, Utc}; use prisma_client_rust::{operator, or, WhereQuery}; use rspc::{alpha::AlphaRouter, ErrorCode}; +use sd_prisma::prisma::media_data; use serde::{Deserialize, Serialize}; use specta::Type; @@ -62,6 +63,7 @@ pub enum FilePathOrder { DateModified(SortOrder), DateIndexed(SortOrder), Object(Box), + DateImageTaken(Box), } impl FilePathOrder { @@ -73,6 +75,7 @@ impl FilePathOrder { Self::DateModified(v) => v, Self::DateIndexed(v) => v, Self::Object(v) => return v.get_sort_order(), + Self::DateImageTaken(v) => return v.get_sort_order(), }) .into() } @@ -87,6 +90,7 @@ impl FilePathOrder { Self::DateModified(_) => date_modified::order(dir), Self::DateIndexed(_) => date_indexed::order(dir), Self::Object(v) => object::order(vec![v.into_param()]), + Self::DateImageTaken(v) => object::order(vec![v.into_param()]), } } } @@ -245,6 +249,11 @@ pub enum ObjectCursor { pub enum ObjectOrder { DateAccessed(SortOrder), Kind(SortOrder), + DateImageTaken(SortOrder), +} + +enum MediaDataSortParameter { + DateImageTaken, } impl ObjectOrder { @@ -252,10 +261,23 @@ impl ObjectOrder { (*match self { Self::DateAccessed(v) => v, Self::Kind(v) => v, + Self::DateImageTaken(v) => v, }) .into() } + fn media_data( + &self, + param: MediaDataSortParameter, + dir: prisma::SortOrder, + ) -> object::OrderByWithRelationParam { + let order = match param { + MediaDataSortParameter::DateImageTaken => media_data::epoch_time::order(dir), + }; + + object::media_data::order(vec![order]) + } + fn into_param(self) -> object::OrderByWithRelationParam { let dir = self.get_sort_order(); use object::*; @@ -263,6 +285,7 @@ impl ObjectOrder { match self { Self::DateAccessed(_) => date_accessed::order(dir), Self::Kind(_) => kind::order(dir), + Self::DateImageTaken(_) => self.media_data(MediaDataSortParameter::DateImageTaken, dir), } } } diff --git a/core/src/object/media/mod.rs b/core/src/object/media/mod.rs index a76e771e3..a959cea23 100644 --- a/core/src/object/media/mod.rs +++ b/core/src/object/media/mod.rs @@ -17,12 +17,13 @@ pub fn media_data_image_to_query( _params: vec![ camera_data::set(serde_json::to_vec(&mdi.camera_data).ok()), media_date::set(serde_json::to_vec(&mdi.date_taken).ok()), - dimensions::set(serde_json::to_vec(&mdi.dimensions).ok()), + resolution::set(serde_json::to_vec(&mdi.resolution).ok()), media_location::set(serde_json::to_vec(&mdi.location).ok()), - artist::set(serde_json::to_string(&mdi.artist).ok()), - description::set(serde_json::to_string(&mdi.description).ok()), - copyright::set(serde_json::to_string(&mdi.copyright).ok()), - exif_version::set(serde_json::to_string(&mdi.exif_version).ok()), + artist::set(mdi.artist), + description::set(mdi.description), + copyright::set(mdi.copyright), + exif_version::set(mdi.exif_version), + epoch_time::set(mdi.date_taken.map(|x| x.unix_timestamp())), ], }) } @@ -31,14 +32,14 @@ pub fn media_data_image_from_prisma_data( data: sd_prisma::prisma::media_data::Data, ) -> Result { Ok(ImageMetadata { - dimensions: from_slice_option_to_option(data.dimensions).unwrap_or_default(), camera_data: from_slice_option_to_option(data.camera_data).unwrap_or_default(), date_taken: from_slice_option_to_option(data.media_date).unwrap_or_default(), - description: from_string_option_to_option(data.description), - copyright: from_string_option_to_option(data.copyright), - artist: from_string_option_to_option(data.artist), + resolution: from_slice_option_to_option(data.resolution).unwrap_or_default(), location: from_slice_option_to_option(data.media_location), - exif_version: from_string_option_to_option(data.exif_version), + artist: data.artist, + description: data.description, + copyright: data.copyright, + exif_version: data.exif_version, }) } @@ -50,12 +51,3 @@ fn from_slice_option_to_option( - value: Option, -) -> Option { - value - .map(|x| serde_json::from_str(&x).ok()) - .unwrap_or_default() -} diff --git a/crates/media-metadata/src/image/datetime.rs b/crates/media-metadata/src/image/datetime.rs new file mode 100644 index 000000000..72fd31c00 --- /dev/null +++ b/crates/media-metadata/src/image/datetime.rs @@ -0,0 +1,109 @@ +use super::{ + consts::{OFFSET_TAGS, TIME_TAGS}, + ExifReader, +}; +use chrono::{DateTime, FixedOffset, NaiveDateTime}; +use serde::{ + de::{self, Visitor}, + Deserialize, Deserializer, +}; + +pub const UTC_FORMAT_STR: &str = "%F %T %z"; +pub const NAIVE_FORMAT_STR: &str = "%F %T"; + +/// This can be either naive with no TZ (`YYYY-MM-DD HH-MM-SS`) or UTC (`YYYY-MM-DD HH-MM-SS ±HHMM`), +/// where `±HHMM` is the timezone data. It may be negative if West of the Prime Meridian, or positive if East. +#[derive(Clone, Debug, PartialEq, Eq, specta::Type)] +#[serde(untagged)] +pub enum MediaDate { + Naive(NaiveDateTime), + Utc(DateTime), +} + +impl MediaDate { + /// This iterates over all 3 pairs of time/offset tags in an attempt to create a UTC time. + /// + /// If the above fails, we fall back to Naive time - if that's not present this is `Undefined`. + pub fn from_reader(reader: &ExifReader) -> Option { + let z = TIME_TAGS + .into_iter() + .zip(OFFSET_TAGS) + .filter_map(|(time_tag, offset_tag)| { + let time = reader.get_tag::(time_tag); + let offset = reader.get_tag::(offset_tag); + + if let (Some(t), Some(o)) = (time.clone(), offset) { + DateTime::parse_from_str(&(format!("{t} {o}")), UTC_FORMAT_STR) + .ok() + .map(Self::Utc) + } else if let Some(t) = time { + NaiveDateTime::parse_from_str(&t, NAIVE_FORMAT_STR) + .map_or(None, |x| Some(Self::Naive(x))) + } else { + None + } + }) + .collect::>(); + + z.iter() + .find(|x| matches!(x, Self::Utc(_) | Self::Naive(_))) + .map(Clone::clone) + } + + /// Returns the amount of non-leap secods since the Unix Epoch (1970-01-01T00:00:00+00:00) + /// + /// This is for search ordering/sorting + #[must_use] + pub fn unix_timestamp(&self) -> i64 { + match self { + Self::Utc(t) => t.timestamp(), + Self::Naive(t) => t.timestamp(), + } + } +} + +impl serde::Serialize for MediaDate { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self { + Self::Utc(t) => serializer.serialize_str(&t.format(UTC_FORMAT_STR).to_string()), + Self::Naive(t) => serializer.serialize_str(&t.format(NAIVE_FORMAT_STR).to_string()), + } + } +} + +struct MediaDateVisitor; + +impl<'de> Visitor<'de> for MediaDateVisitor { + type Value = MediaDate; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("either `UTC_FORMAT_STR` or `NAIVE_FORMAT_STR`") + } + + fn visit_str(self, v: &str) -> Result + where + E: de::Error, + { + DateTime::parse_from_str(v, UTC_FORMAT_STR).map_or_else( + |_| { + NaiveDateTime::parse_from_str(v, NAIVE_FORMAT_STR).map_or_else( + |_| Err(E::custom("unable to parse utc or naive from str")), + |time| Ok(Self::Value::Naive(time)), + ) + }, + |time| Ok(Self::Value::Utc(time)), + ) + } +} + +impl<'de> Deserialize<'de> for MediaDate { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_str(MediaDateVisitor) + } +} diff --git a/crates/media-metadata/src/image/mod.rs b/crates/media-metadata/src/image/mod.rs index 7ea58f2ea..35c4f1bd0 100644 --- a/crates/media-metadata/src/image/mod.rs +++ b/crates/media-metadata/src/image/mod.rs @@ -3,32 +3,32 @@ use std::path::Path; mod composite; mod consts; -mod dimensions; +mod datetime; mod flash; mod geographic; mod orientation; mod profile; mod reader; -mod time; +mod resolution; pub use composite::Composite; pub use consts::DMS_DIVISION; -pub use dimensions::Dimensions; +pub use datetime::MediaDate; pub use flash::{Flash, FlashMode, FlashValue}; pub use geographic::{MediaLocation, PlusCode}; pub use orientation::Orientation; pub use profile::ColorProfile; pub use reader::ExifReader; -pub use time::MediaTime; +pub use resolution::Resolution; use crate::Result; #[derive(Default, Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize, specta::Type)] pub struct ImageMetadata { - pub dimensions: Dimensions, - pub date_taken: MediaTime, + pub resolution: Resolution, + pub date_taken: Option, pub location: Option, - pub camera_data: ImageData, + pub camera_data: CameraData, pub artist: Option, pub description: Option, pub copyright: Option, @@ -36,7 +36,7 @@ pub struct ImageMetadata { } #[derive(Default, Clone, PartialEq, Debug, serde::Serialize, serde::Deserialize, specta::Type)] -pub struct ImageData { +pub struct CameraData { pub device_make: Option, pub device_model: Option, pub color_space: Option, @@ -74,8 +74,8 @@ impl ImageMetadata { let mut data = Self::default(); let camera_data = &mut data.camera_data; - data.date_taken = MediaTime::from_reader(reader); - data.dimensions = Dimensions::from_reader(reader); + data.date_taken = MediaDate::from_reader(reader); + data.resolution = Resolution::from_reader(reader); data.artist = reader.get_tag(Tag::Artist); data.description = reader.get_tag(Tag::ImageDescription); data.copyright = reader.get_tag(Tag::Copyright); diff --git a/crates/media-metadata/src/image/dimensions.rs b/crates/media-metadata/src/image/resolution.rs similarity index 69% rename from crates/media-metadata/src/image/dimensions.rs rename to crates/media-metadata/src/image/resolution.rs index 46f011ad5..f1da3b224 100644 --- a/crates/media-metadata/src/image/dimensions.rs +++ b/crates/media-metadata/src/image/resolution.rs @@ -1,5 +1,3 @@ -use std::fmt::Display; - use exif::Tag; use super::ExifReader; @@ -7,21 +5,21 @@ use super::ExifReader; #[derive( Default, Clone, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize, specta::Type, )] -pub struct Dimensions { +pub struct Resolution { pub width: i32, pub height: i32, } -impl Dimensions { +impl Resolution { #[must_use] /// Creates a new width and height container /// /// # Examples /// /// ``` - /// use sd_media_metadata::image::Dimensions; + /// use sd_media_metadata::image::Resolution; /// - /// Dimensions::new(1920, 1080); + /// Resolution::new(1920, 1080); /// ``` pub const fn new(width: i32, height: i32) -> Self { Self { width, height } @@ -39,9 +37,3 @@ impl Dimensions { } } } - -impl Display for Dimensions { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.write_fmt(format_args!("{}x{}", self.width, self.height)) - } -} diff --git a/crates/media-metadata/src/image/time.rs b/crates/media-metadata/src/image/time.rs deleted file mode 100644 index f33a2646a..000000000 --- a/crates/media-metadata/src/image/time.rs +++ /dev/null @@ -1,101 +0,0 @@ -use super::{ - consts::{OFFSET_TAGS, TIME_TAGS}, - ExifReader, -}; -use crate::Error; -use chrono::{DateTime, FixedOffset, NaiveDateTime}; -use std::fmt::Display; - -pub const NAIVE_FORMAT_STR: &str = "%Y-%m-%d %H:%M:%S"; - -#[derive(Default, Clone, Debug, PartialEq, Eq, serde::Deserialize, specta::Type)] -/// This can be either naive with no TZ (`YYYY-MM-DD HH-MM-SS`) or UTC with a fixed offset (`rfc3339`). -/// -/// This may also be `undefined`. -pub enum MediaTime { - Naive(NaiveDateTime), - Utc(DateTime), - #[default] - Undefined, -} - -impl MediaTime { - /// This iterates over all 3 pairs of time/offset tags in an attempt to create a UTC time. - /// - /// If the above fails, we fall back to Naive time - if that's not present this is `Undefined`. - pub fn from_reader(reader: &ExifReader) -> Self { - let z = TIME_TAGS - .into_iter() - .zip(OFFSET_TAGS) - .filter_map(|(time_tag, offset_tag)| { - let time = reader.get_tag::(time_tag); - let offset = reader.get_tag::(offset_tag); - - if let (Some(t), Some(o)) = (time.clone(), offset) { - DateTime::parse_and_remainder(&format!("{t} {o}"), "%F %X %#z") - .ok() - .map(|x| Self::Utc(x.0)) - } else if let Some(t) = time { - Some( - NaiveDateTime::parse_from_str(&t, NAIVE_FORMAT_STR) - .map_or(Self::Undefined, Self::Naive), - ) - } else { - Some(Self::Undefined) - } - }) - .collect::>(); - - z.iter() - .find(|x| match x { - Self::Utc(_) | Self::Naive(_) => true, - Self::Undefined => false, - }) - .map_or(Self::Undefined, Clone::clone) - } -} - -impl TryFrom for MediaTime { - type Error = Error; - - fn try_from(value: String) -> Result { - if &value == "Undefined" { - return Ok(Self::Undefined); - } - - if let Ok(time) = DateTime::parse_from_rfc3339(&value) { - return Ok(Self::Utc(time)); - } - - Ok(NaiveDateTime::parse_from_str(&value, NAIVE_FORMAT_STR) - .map_or(Self::Undefined, Self::Naive)) - } -} - -impl Display for MediaTime { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Undefined => f.write_str("Undefined"), - Self::Naive(l) => f.write_str(&l.to_string()), - Self::Utc(u) => f.write_str(&u.to_rfc3339()), - } - } -} - -impl serde::Serialize for MediaTime { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - match self { - Self::Naive(t) => serializer.collect_str(&t.to_string()), - Self::Utc(t) => { - let local = NaiveDateTime::from_timestamp_millis(t.timestamp_millis()).ok_or_else( - || serde::ser::Error::custom("Error converting UTC to Naive time"), - )?; - serializer.collect_str(&local.format("%Y-%m-%d %H:%M:%S").to_string()) - } - Self::Undefined => serializer.collect_str("Undefined"), - } - } -} diff --git a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx index cdff91774..dfa53a359 100644 --- a/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx +++ b/interface/app/$libraryId/Explorer/Inspector/MediaData.tsx @@ -1,8 +1,13 @@ +import dayjs from 'dayjs'; +import customParseFormat from 'dayjs/plugin/customParseFormat'; // import plugin + +import utc from 'dayjs/plugin/utc'; // import plugin + import { CoordinatesFormat, + MediaDate, MediaLocation, MediaMetadata, - MediaTime, useUnitFormatStore } from '@sd/client'; import Accordion from '~/components/Accordion'; @@ -15,12 +20,33 @@ interface Props { data: MediaMetadata; } -const formatMediaTime = (time: MediaTime): string | null => { - if (time === 'Undefined') return null; - if ('Utc' in time) return time.Utc; - if ('Naive' in time) return time.Naive; - return null; -}; +// const DateFormatWithTz = 'YYYY-MM-DD HH:mm:ss ZZ'; +// const DateFormatWithoutTz = 'YYYY-MM-DD HH:mm:ss'; + +// const formatMediaDate = (datetime: MediaDate): { formatted: string; raw: string } | undefined => { +// dayjs.extend(customParseFormat); +// dayjs.extend(utc); + +// // dayjs.tz.setDefault(dayjs.tz.guess()); + +// const getTzData = (dt: string): [string, number] => { +// if (dt.includes('+')) +// return [DateFormatWithTz, Number.parseInt(dt.substring(dt.indexOf('+'), 3))]; +// return [DateFormatWithoutTz, 0]; +// }; + +// const [tzFormat, tzOffset] = getTzData(datetime); + +// console.log({ +// formatted: dayjs(datetime, tzFormat).utcOffset(tzOffset).format('HH:mm:ss, MMM Do YYYY'), +// raw: datetime +// }); + +// return { +// formatted: dayjs(datetime, tzFormat).utcOffset(tzOffset).format('HH:mm:ss, MMM Do YYYY'), +// raw: datetime +// }; +// }; const formatLocationDD = (loc: MediaLocation, dp?: number): string => { // the lack of a + here will mean that coordinates may have padding at the end @@ -85,6 +111,7 @@ const orientations = { const MediaData = ({ data }: Props) => { const platform = usePlatform(); const coordinatesFormat = useUnitFormatStore().coordinatesFormat; + const explorerStore = useExplorerStore(); return data.type === 'Image' ? ( @@ -95,7 +122,12 @@ const MediaData = ({ data }: Props) => { variant="apple" title="More info" > - + { } /> - diff --git a/interface/app/$libraryId/Explorer/store.ts b/interface/app/$libraryId/Explorer/store.ts index 5699f505e..053a29af5 100644 --- a/interface/app/$libraryId/Explorer/store.ts +++ b/interface/app/$libraryId/Explorer/store.ts @@ -84,8 +84,10 @@ export const createDefaultExplorerSettings = (args?: { sizeInBytes: true, dateCreated: true, dateModified: true, + dateImageTaken: true, dateAccessed: false, dateIndexed: false, + imageResolution: true, contentId: false, objectId: false }, @@ -95,8 +97,10 @@ export const createDefaultExplorerSettings = (args?: { sizeInBytes: 100, dateCreated: 150, dateModified: 150, + dateImageTaken: 150, dateAccessed: 150, dateIndexed: 150, + imageResolution: 180, contentId: 180, objectId: 180 } @@ -162,12 +166,14 @@ export const filePathOrderingKeysSchema = z.union([ z.literal('dateModified').describe('Date Modified'), z.literal('dateIndexed').describe('Date Indexed'), z.literal('dateCreated').describe('Date Created'), - z.literal('object.dateAccessed').describe('Date Accessed') + z.literal('object.dateAccessed').describe('Date Accessed'), + z.literal('object.dateImageTaken').describe('Date Taken') ]); export const objectOrderingKeysSchema = z.union([ z.literal('dateAccessed').describe('Date Accessed'), - z.literal('kind').describe('Kind') + z.literal('kind').describe('Kind'), + z.literal('dateImageTaken').describe('Date Taken') ]); export const nonIndexedPathOrderingSchema = z.union([ diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index b4fd5475f..724e65d1b 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -123,6 +123,8 @@ export type CRDTOperation = { instance: string; timestamp: number; id: string; t export type CRDTOperationType = SharedOperation | RelationOperation +export type CameraData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null } + /** * Meow */ @@ -146,8 +148,6 @@ export type CreateLibraryArgs = { name: LibraryName } export type CursorOrderItem = { order: SortOrder; data: T } -export type Dimensions = { width: number; height: number } - export type DiskType = "SSD" | "HDD" | "Removable" export type DoubleClickAction = "openFile" | "quickPreview" @@ -189,7 +189,7 @@ export type FilePathFilterArgs = { locationId?: number | null; search?: string | export type FilePathObjectCursor = { dateAccessed: CursorOrderItem } | { kind: CursorOrderItem } -export type FilePathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } | { field: "dateIndexed"; value: SortOrder } | { field: "object"; value: ObjectOrder } +export type FilePathOrder = { field: "name"; value: SortOrder } | { field: "sizeInBytes"; value: SortOrder } | { field: "dateCreated"; value: SortOrder } | { field: "dateModified"; value: SortOrder } | { field: "dateIndexed"; value: SortOrder } | { field: "object"; value: ObjectOrder } | { field: "dateImageTaken"; value: ObjectOrder } export type FilePathSearchArgs = { take?: number | null; orderAndPagination?: OrderAndPagination | null; filter?: FilePathFilterArgs; groupDirectories?: boolean } @@ -213,9 +213,7 @@ export type Header = { id: string; timestamp: string; library_id: string; librar export type IdentifyUniqueFilesArgs = { id: number; path: string } -export type ImageData = { device_make: string | null; device_model: string | null; color_space: string | null; color_profile: ColorProfile | null; focal_length: number | null; shutter_speed: number | null; flash: Flash | null; orientation: Orientation; lens_make: string | null; lens_model: string | null; bit_depth: number | null; red_eye: boolean | null; zoom: number | null; iso: number | null; software: string | null; serial_number: string | null; lens_serial_number: string | null; contrast: number | null; saturation: number | null; sharpness: number | null; composite: Composite | null } - -export type ImageMetadata = { dimensions: Dimensions; date_taken: MediaTime; location: MediaLocation | null; camera_data: ImageData; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } +export type ImageMetadata = { resolution: Resolution; date_taken: MediaDate | null; location: MediaLocation | null; camera_data: CameraData; artist: string | null; description: string | null; copyright: string | null; exif_version: string | null } export type IndexerRule = { id: number; pub_id: number[]; name: string | null; default: boolean | null; rules_per_kind: number[] | null; date_created: string | null; date_modified: string | null } @@ -290,17 +288,16 @@ export type MaybeNot = T | { not: T } export type MaybeUndefined = null | null | T +/** + * This can be either naive with no TZ (`YYYY-MM-DD HH-MM-SS`) or UTC (`YYYY-MM-DD HH-MM-SS ±HHMM`), + * where `±HHMM` is the timezone data. It may be negative if West of the Prime Meridian, or positive if East. + */ +export type MediaDate = string | string + export type MediaLocation = { latitude: number; longitude: number; pluscode: PlusCode; altitude: number | null; direction: number | null } export type MediaMetadata = ({ type: "Image" } & ImageMetadata) | ({ type: "Video" } & VideoMetadata) | ({ type: "Audio" } & AudioMetadata) -/** - * This can be either naive with no TZ (`YYYY-MM-DD HH-MM-SS`) or UTC with a fixed offset (`rfc3339`). - * - * This may also be `undefined`. - */ -export type MediaTime = { Naive: string } | { Utc: string } | "Undefined" - export type NodeState = ({ id: string; name: string; p2p_port: number | null; features: BackendFeature[]; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string } export type NonIndexedFileSystemEntries = { entries: ExplorerItem[]; errors: Error[] } @@ -328,7 +325,7 @@ export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: ObjectHidde export type ObjectHiddenFilter = "exclude" | "include" -export type ObjectOrder = { field: "dateAccessed"; value: SortOrder } | { field: "kind"; value: SortOrder } +export type ObjectOrder = { field: "dateAccessed"; value: SortOrder } | { field: "kind"; value: SortOrder } | { field: "dateImageTaken"; value: SortOrder } export type ObjectSearchArgs = { take: number; orderAndPagination?: OrderAndPagination | null; filter?: ObjectFilterArgs } @@ -377,6 +374,8 @@ export type RenameOne = { from_file_path_id: number; to: string } export type RescanArgs = { location_id: number; sub_path: string } +export type Resolution = { width: number; height: number } + export type Response = { Start: { user_code: string; verification_url: string; verification_url_complete: string } } | "Complete" | "Error" export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"