[ENG-1143] Media View sort by date taken (#1390)

* 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 <ijamespine@me.com>
Co-authored-by: Vítor Vasconcellos <vasconcellos.dev@gmail.com>
This commit is contained in:
jake
2023-10-11 01:58:59 +01:00
committed by GitHub
parent 9ca22733ad
commit 078490cdd5
11 changed files with 261 additions and 174 deletions

View File

@@ -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;

View File

@@ -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)

View File

@@ -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<ObjectOrder>),
DateImageTaken(Box<ObjectOrder>),
}
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),
}
}
}

View File

@@ -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<ImageMetadata, MediaDataError> {
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<T: serde::Serialize + serde::de::DeserializeOwned
.map(|x| serde_json::from_slice(&x).ok())
.unwrap_or_default()
}
#[must_use]
fn from_string_option_to_option<T: serde::Serialize + serde::de::DeserializeOwned>(
value: Option<String>,
) -> Option<T> {
value
.map(|x| serde_json::from_str(&x).ok())
.unwrap_or_default()
}

View File

@@ -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<FixedOffset>),
}
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<Self> {
let z = TIME_TAGS
.into_iter()
.zip(OFFSET_TAGS)
.filter_map(|(time_tag, offset_tag)| {
let time = reader.get_tag::<String>(time_tag);
let offset = reader.get_tag::<String>(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::<Vec<_>>();
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
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<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
deserializer.deserialize_str(MediaDateVisitor)
}
}

View File

@@ -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<MediaDate>,
pub location: Option<MediaLocation>,
pub camera_data: ImageData,
pub camera_data: CameraData,
pub artist: Option<String>,
pub description: Option<String>,
pub copyright: Option<String>,
@@ -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<String>,
pub device_model: Option<String>,
pub color_space: Option<String>,
@@ -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);

View File

@@ -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))
}
}

View File

@@ -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<FixedOffset>),
#[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::<String>(time_tag);
let offset = reader.get_tag::<String>(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::<Vec<_>>();
z.iter()
.find(|x| match x {
Self::Utc(_) | Self::Naive(_) => true,
Self::Undefined => false,
})
.map_or(Self::Undefined, Clone::clone)
}
}
impl TryFrom<String> for MediaTime {
type Error = Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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"),
}
}
}

View File

@@ -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"
>
<MetaData label="Date" value={formatMediaTime(data.date_taken)} />
<MetaData
label="Date"
tooltipValue={data.date_taken ?? null} // should show full raw value
// should show localised, utc-offset value or plain value with tooltip mentioning that we don't have the timezone metadata
value={data.date_taken ?? null}
/>
<MetaData label="Type" value={data.type} />
<MetaData
label="Location"
@@ -131,12 +163,11 @@ const MediaData = ({ data }: Props) => {
}
/>
<MetaData
label="Dimensions"
value={`${data.dimensions.width} x ${data.dimensions.height}`}
label="Resolution"
value={`${data.resolution.width} x ${data.resolution.height}`}
/>
<MetaData label="Device" value={data.camera_data.device_make} />
<MetaData label="Model" value={data.camera_data.device_model} />
<MetaData label="Orientation" value={orientations[data.camera_data.orientation]} />
<MetaData label="Color profile" value={data.camera_data.color_profile} />
<MetaData label="Color space" value={data.camera_data.color_space} />
<MetaData label="Flash" value={data.camera_data.flash?.mode} />

View File

@@ -84,8 +84,10 @@ export const createDefaultExplorerSettings = <TOrder extends Ordering>(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 = <TOrder extends Ordering>(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([

View File

@@ -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<T> = { 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<string> } | { kind: CursorOrderItem<number> }
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<number, FilePathOrder, FilePathCursor> | 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> = T | { not: T }
export type MaybeUndefined<T> = 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<number, ObjectOrder, ObjectCursor> | 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"