From 996df041a4e285ef2fbb052e79eac2d718c837ed Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Fri, 26 Dec 2025 11:20:01 -0800 Subject: [PATCH] Enhance file querying and alternate instances handling - Updated `FileByIdQuery` and `FileByPathQuery` to populate alternate paths for files with the same content ID, improving data retrieval for file instances. - Introduced `get_alternate_paths` method in both queries to fetch alternate file paths from the database. - Modified the `InstancesTab` component to utilize a new query for alternate instances, enhancing the user interface with detailed instance information. - Updated TypeScript types to support the new alternate instances query structure, ensuring type safety across the application. - Adjusted various components to improve the display of alternate file instances, including device and path information. --- .../ops/files/query/alternate_instances.rs | 360 ++++++++++++++++++ core/src/ops/files/query/file_by_id.rs | 35 +- core/src/ops/files/query/file_by_path.rs | 94 ++++- core/src/ops/files/query/mod.rs | 2 + .../src/inspectors/FileInspector.tsx | 126 +++++- packages/ts-client/src/generated/types.ts | 318 +++++++++------- 6 files changed, 767 insertions(+), 168 deletions(-) create mode 100644 core/src/ops/files/query/alternate_instances.rs diff --git a/core/src/ops/files/query/alternate_instances.rs b/core/src/ops/files/query/alternate_instances.rs new file mode 100644 index 000000000..31a6fbbc8 --- /dev/null +++ b/core/src/ops/files/query/alternate_instances.rs @@ -0,0 +1,360 @@ +//! Query to get all alternate instances of a file by entry ID +//! +//! This query finds all other entries that share the same content_id and returns +//! them as complete File objects with all related data (tags, sidecars, media data). + +use crate::infra::query::{QueryError, QueryResult}; +use crate::{ + context::CoreContext, + domain::{addressing::SdPath, content_identity::ContentIdentity, file::File}, + infra::db::entities::{ + content_identity, device, directory_paths, entry, location, sidecar, tag, user_metadata, + user_metadata_tag, video_media_data, + }, + infra::query::LibraryQuery, +}; +use sea_orm::{ColumnTrait, ConnectionTrait, DatabaseConnection, EntityTrait, QueryFilter}; +use serde::{Deserialize, Serialize}; +use specta::Type; +use std::{collections::HashMap, path::PathBuf, sync::Arc}; +use uuid::Uuid; + +/// Input for alternate instances query +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct AlternateInstancesInput { + /// The entry UUID to find alternates for + pub entry_uuid: Uuid, +} + +/// Output containing alternate instances +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct AlternateInstancesOutput { + /// All instances of this file (including the original) + pub instances: Vec, + /// Total number of instances found + pub total_count: u32, +} + +/// Query to get alternate instances of a file +#[derive(Debug, Clone, Serialize, Deserialize, Type)] +pub struct AlternateInstancesQuery { + pub input: AlternateInstancesInput, +} + +impl AlternateInstancesQuery { + pub fn new(entry_uuid: Uuid) -> Self { + Self { + input: AlternateInstancesInput { entry_uuid }, + } + } +} + +impl LibraryQuery for AlternateInstancesQuery { + type Input = AlternateInstancesInput; + type Output = AlternateInstancesOutput; + + fn from_input(input: Self::Input) -> QueryResult { + Ok(Self { input }) + } + + async fn execute( + self, + context: Arc, + session: crate::infra::api::SessionContext, + ) -> QueryResult { + let library_id = session + .current_library_id + .ok_or_else(|| QueryError::Internal("No library in session".to_string()))?; + + let library = context + .libraries() + .await + .get_library(library_id) + .await + .ok_or_else(|| QueryError::Internal("Library not found".to_string()))?; + + let db = library.db(); + + // Find the original entry + let original_entry = entry::Entity::find() + .filter(entry::Column::Uuid.eq(self.input.entry_uuid)) + .one(db.conn()) + .await? + .ok_or_else(|| QueryError::Internal("Entry not found".to_string()))?; + + // Get the content_id + let content_id = original_entry.content_id.ok_or_else(|| { + QueryError::Internal( + "Entry has no content identity, cannot find alternates".to_string(), + ) + })?; + + // Find all entries with the same content_id + let alternate_entries = entry::Entity::find() + .filter(entry::Column::ContentId.eq(content_id)) + .all(db.conn()) + .await?; + + if alternate_entries.is_empty() { + return Ok(AlternateInstancesOutput { + instances: Vec::new(), + total_count: 0, + }); + } + + // Batch load content identity + let content_identity_model = content_identity::Entity::find_by_id(content_id) + .one(db.conn()) + .await? + .ok_or_else(|| QueryError::Internal("Content identity not found".to_string()))?; + + let content_uuid = content_identity_model.uuid; + + // Batch load sidecars + let sidecars = if let Some(ci_uuid) = content_uuid { + sidecar::Entity::find() + .filter(sidecar::Column::ContentUuid.eq(ci_uuid)) + .all(db.conn()) + .await? + .into_iter() + .map(|s| crate::domain::file::Sidecar { + id: s.id, + content_uuid: s.content_uuid, + kind: s.kind, + variant: s.variant, + format: s.format, + status: s.status, + size: s.size, + created_at: s.created_at, + updated_at: s.updated_at, + }) + .collect() + } else { + Vec::new() + }; + + // Batch load tags for all entries + let entry_uuids: Vec = alternate_entries.iter().filter_map(|e| e.uuid).collect(); + + let mut tags_by_entry: HashMap> = HashMap::new(); + + if !entry_uuids.is_empty() || content_uuid.is_some() { + // Load user_metadata for entries and content + let mut filter = user_metadata::Column::EntryUuid.is_in(entry_uuids.clone()); + if let Some(ci_uuid) = content_uuid { + filter = filter.or(user_metadata::Column::ContentIdentityUuid.eq(ci_uuid)); + } + + let metadata_records = user_metadata::Entity::find() + .filter(filter) + .all(db.conn()) + .await?; + + if !metadata_records.is_empty() { + let metadata_ids: Vec = metadata_records.iter().map(|m| m.id).collect(); + + // Load user_metadata_tag records + let metadata_tags = user_metadata_tag::Entity::find() + .filter(user_metadata_tag::Column::UserMetadataId.is_in(metadata_ids)) + .all(db.conn()) + .await?; + + if !metadata_tags.is_empty() { + let tag_ids: Vec = metadata_tags.iter().map(|mt| mt.tag_id).collect(); + + // Load tag entities + let tag_models = tag::Entity::find() + .filter(tag::Column::Id.is_in(tag_ids)) + .all(db.conn()) + .await?; + + // Build tag_id -> Tag mapping + let tag_map: HashMap = tag_models + .into_iter() + .filter_map(|t| { + let db_id = t.id; + crate::ops::tags::manager::model_to_domain(t) + .ok() + .map(|tag| (db_id, tag)) + }) + .collect(); + + // Build metadata_id -> Vec mapping + let mut tags_by_metadata: HashMap> = + HashMap::new(); + for mt in metadata_tags { + if let Some(tag) = tag_map.get(&mt.tag_id) { + tags_by_metadata + .entry(mt.user_metadata_id) + .or_insert_with(Vec::new) + .push(tag.clone()); + } + } + + // Map tags to entries (prioritize entry-scoped, fall back to content-scoped) + for metadata in &metadata_records { + if let Some(tags) = tags_by_metadata.get(&metadata.id) { + // Entry-scoped metadata (higher priority) + if let Some(entry_uuid) = metadata.entry_uuid { + tags_by_entry.insert(entry_uuid, tags.clone()); + } + // Content-scoped metadata (applies to all entries with this content) + else if let Some(_content_uuid) = metadata.content_identity_uuid { + // Apply to all entries + for entry_uuid in &entry_uuids { + tags_by_entry + .entry(*entry_uuid) + .or_insert_with(|| tags.clone()); + } + } + } + } + } + } + } + + // Build content identity domain object + let content_identity_domain = ContentIdentity { + uuid: content_uuid.unwrap_or_else(Uuid::new_v4), + kind: crate::domain::ContentKind::from_id(content_identity_model.kind_id), + content_hash: content_identity_model.content_hash, + integrity_hash: content_identity_model.integrity_hash, + mime_type_id: content_identity_model.mime_type_id, + text_content: content_identity_model.text_content, + total_size: content_identity_model.total_size, + entry_count: content_identity_model.entry_count, + first_seen_at: content_identity_model.first_seen_at, + last_verified_at: content_identity_model.last_verified_at, + }; + + // Load media data if available + let video_media_data = if let Some(video_id) = content_identity_model.video_media_data_id { + video_media_data::Entity::find_by_id(video_id) + .one(db.conn()) + .await? + .map(Into::into) + } else { + None + }; + + // Convert each entry to a complete File object + let mut instances = Vec::new(); + for entry_model in alternate_entries { + // Resolve full path for this entry + let sd_path = match self.resolve_entry_path(&entry_model, db.conn()).await { + Ok(path) => path, + Err(e) => { + tracing::warn!("Failed to resolve path for entry {}: {}", entry_model.id, e); + continue; + } + }; + + // Create File from entry model + let mut file = File::from_entity_model(entry_model.clone(), sd_path); + + // Add content identity, sidecars, and media data + file.content_identity = Some(content_identity_domain.clone()); + file.sidecars = sidecars.clone(); + file.video_media_data = video_media_data.clone(); + file.content_kind = content_identity_domain.kind; + file.duration_seconds = video_media_data.as_ref().and_then(|v| v.duration_seconds); + + // Add tags for this specific entry + if let Some(entry_uuid) = entry_model.uuid { + if let Some(tags) = tags_by_entry.get(&entry_uuid) { + file.tags = tags.clone(); + } + } + + instances.push(file); + } + + let total_count = instances.len() as u32; + + Ok(AlternateInstancesOutput { + instances, + total_count, + }) + } +} + +impl AlternateInstancesQuery { + /// Resolve the full absolute SdPath for an entry + async fn resolve_entry_path( + &self, + entry: &entry::Model, + db: &DatabaseConnection, + ) -> QueryResult { + // Walk up the entry hierarchy to build the full path + let mut path_components = Vec::new(); + + // Add the file name with extension + let file_name = if let Some(ext) = &entry.extension { + format!("{}.{}", entry.name, ext) + } else { + entry.name.clone() + }; + path_components.push(file_name); + + // Walk up parent chain + let mut current_parent_id = entry.parent_id; + let mut location_entry_id = None; + + while let Some(parent_id) = current_parent_id { + let parent = entry::Entity::find_by_id(parent_id) + .one(db) + .await? + .ok_or_else(|| QueryError::Internal("Parent entry not found".to_string()))?; + + // Check if this is the location root (no parent) + if parent.parent_id.is_none() { + location_entry_id = Some(parent.id); + break; + } + + // Add parent directory name to path + path_components.push(parent.name.clone()); + current_parent_id = parent.parent_id; + } + + // Reverse to get correct order (root -> file) + path_components.reverse(); + + // Get location info + let location_entry_id = location_entry_id + .ok_or_else(|| QueryError::Internal("Could not find location root".to_string()))?; + + let location_model = location::Entity::find() + .filter(location::Column::EntryId.eq(location_entry_id)) + .one(db) + .await? + .ok_or_else(|| QueryError::Internal("Location not found for entry".to_string()))?; + + // Get device slug + let device_model = device::Entity::find_by_id(location_model.device_id) + .one(db) + .await? + .ok_or_else(|| QueryError::Internal("Device not found".to_string()))?; + + // Get location root absolute path + let location_root_path = directory_paths::Entity::find() + .filter(directory_paths::Column::EntryId.eq(location_entry_id)) + .one(db) + .await? + .ok_or_else(|| QueryError::Internal("Location root path not found".to_string()))?; + + // Build absolute path: location_root + relative components + let mut absolute_path = PathBuf::from(&location_root_path.path); + for component in path_components { + absolute_path.push(component); + } + + Ok(SdPath::Physical { + device_slug: device_model.slug, + path: absolute_path.into(), + }) + } +} + +// Register the query +crate::register_library_query!(AlternateInstancesQuery, "files.alternate_instances"); diff --git a/core/src/ops/files/query/file_by_id.rs b/core/src/ops/files/query/file_by_id.rs index 8dad6129b..4ddf90766 100644 --- a/core/src/ops/files/query/file_by_id.rs +++ b/core/src/ops/files/query/file_by_id.rs @@ -167,7 +167,7 @@ impl LibraryQuery for FileByIdQuery { }; // Convert to File using from_entity_model - let mut file = File::from_entity_model(entry_model.clone(), sd_path); + let mut file = File::from_entity_model(entry_model.clone(), sd_path.clone()); file.sidecars = sidecars; file.content_identity = content_identity_domain; file.image_media_data = image_media; @@ -178,6 +178,13 @@ impl LibraryQuery for FileByIdQuery { file.content_kind = ci.kind; } + // Populate alternate paths (other instances of same content) + if let Some(content_id) = entry_model.content_id { + file.alternate_paths = self + .get_alternate_paths(content_id, entry_model.id, db.conn()) + .await?; + } + // Load tags for this entry if let Some(entry_uuid) = entry_model.uuid { use std::collections::HashMap; @@ -255,6 +262,32 @@ impl LibraryQuery for FileByIdQuery { } impl FileByIdQuery { + /// Get alternate paths for all other entries with the same content_id + async fn get_alternate_paths( + &self, + content_id: i32, + current_entry_id: i32, + db: &DatabaseConnection, + ) -> QueryResult> { + // Find all entries with the same content_id (excluding current entry) + let alternate_entries = entry::Entity::find() + .filter(entry::Column::ContentId.eq(content_id)) + .filter(entry::Column::Id.ne(current_entry_id)) + .all(db) + .await?; + + let mut alternate_paths = Vec::new(); + + // Resolve path for each alternate entry + for alt_entry in alternate_entries { + if let Ok(alt_path) = self.resolve_file_path(&alt_entry, db).await { + alternate_paths.push(alt_path); + } + } + + Ok(alternate_paths) + } + /// Resolve the full absolute SdPath for a file entry async fn resolve_file_path( &self, diff --git a/core/src/ops/files/query/file_by_path.rs b/core/src/ops/files/query/file_by_path.rs index 6fb217d49..865ce5762 100644 --- a/core/src/ops/files/query/file_by_path.rs +++ b/core/src/ops/files/query/file_by_path.rs @@ -177,7 +177,7 @@ impl LibraryQuery for FileByPathQuery { }; // Convert to File using from_entity_model - let mut file = File::from_entity_model(entry_model, sd_path); + let mut file = File::from_entity_model(entry_model.clone(), sd_path); file.sidecars = sidecars; file.content_identity = content_identity_domain; file.image_media_data = image_media; @@ -188,6 +188,13 @@ impl LibraryQuery for FileByPathQuery { file.content_kind = ci.kind; } + // Populate alternate paths (other instances of same content) + if let Some(content_id) = entry_model.content_id { + file.alternate_paths = self + .get_alternate_paths(content_id, entry_model.id, db.conn()) + .await?; + } + return Ok(Some(file)); } @@ -213,6 +220,91 @@ impl LibraryQuery for FileByPathQuery { } impl FileByPathQuery { + /// Get alternate paths for all other entries with the same content_id + async fn get_alternate_paths( + &self, + content_id: i32, + current_entry_id: i32, + db: &DatabaseConnection, + ) -> QueryResult> { + use crate::infra::db::entities::{device, directory_paths, location}; + + // Find all entries with the same content_id (excluding current entry) + let alternate_entries = entry::Entity::find() + .filter(entry::Column::ContentId.eq(content_id)) + .filter(entry::Column::Id.ne(current_entry_id)) + .all(db) + .await?; + + let mut alternate_paths = Vec::new(); + + // Resolve path for each alternate entry + for alt_entry in alternate_entries { + // Build the full path for this entry + let mut path_components = Vec::new(); + + // Add the file name with extension + let file_name = if let Some(ext) = &alt_entry.extension { + format!("{}.{}", alt_entry.name, ext) + } else { + alt_entry.name.clone() + }; + path_components.push(file_name); + + // Walk up parent chain + let mut current_parent_id = alt_entry.parent_id; + let mut location_entry_id = None; + + while let Some(parent_id) = current_parent_id { + if let Some(parent) = entry::Entity::find_by_id(parent_id).one(db).await? { + if parent.parent_id.is_none() { + location_entry_id = Some(parent.id); + break; + } + path_components.push(parent.name.clone()); + current_parent_id = parent.parent_id; + } else { + break; + } + } + + if let Some(location_entry_id) = location_entry_id { + path_components.reverse(); + + // Get location and device info + if let Some(location_model) = location::Entity::find() + .filter(location::Column::EntryId.eq(location_entry_id)) + .one(db) + .await? + { + if let Some(device_model) = device::Entity::find_by_id(location_model.device_id) + .one(db) + .await? + { + if let Some(location_root_path) = directory_paths::Entity::find() + .filter(directory_paths::Column::EntryId.eq(location_entry_id)) + .one(db) + .await? + { + // Build absolute path + let mut absolute_path = PathBuf::from(&location_root_path.path); + for component in path_components { + absolute_path.push(component); + } + + alternate_paths.push(SdPath::Physical { + device_slug: device_model.slug, + path: absolute_path.into(), + }); + } + } + } + } + } + + Ok(alternate_paths) + } + /// Find entry by SdPath async fn find_entry_by_sd_path( &self, diff --git a/core/src/ops/files/query/mod.rs b/core/src/ops/files/query/mod.rs index fd4d074a8..e98fb550d 100644 --- a/core/src/ops/files/query/mod.rs +++ b/core/src/ops/files/query/mod.rs @@ -1,5 +1,6 @@ //! File query operations +pub mod alternate_instances; pub mod content_kind_stats; pub mod directory_listing; pub mod file_by_id; @@ -7,6 +8,7 @@ pub mod file_by_path; pub mod media_listing; pub mod unique_to_location; +pub use alternate_instances::*; pub use content_kind_stats::*; pub use directory_listing::*; pub use file_by_id::*; diff --git a/packages/interface/src/inspectors/FileInspector.tsx b/packages/interface/src/inspectors/FileInspector.tsx index b7d15d675..247c82417 100644 --- a/packages/interface/src/inspectors/FileInspector.tsx +++ b/packages/interface/src/inspectors/FileInspector.tsx @@ -948,8 +948,17 @@ function SidecarItem({ } function InstancesTab({ file }: { file: File }) { - const alternatePaths = file.alternate_paths || []; - const allPaths = [file.sd_path, ...alternatePaths]; + // Query for alternate instances with full File data + const instancesQuery = useNormalizedQuery< + { entry_uuid: string }, + { instances: File[]; total_count: number } + >({ + wireMethod: "query:files.alternate_instances", + input: { entry_uuid: file?.id || "" }, + enabled: !!file?.id && !!file?.content_identity, + }); + + const instances = instancesQuery.data?.instances || []; const getPathDisplay = (sdPath: typeof file.sd_path) => { if ("Physical" in sdPath) { @@ -961,39 +970,118 @@ function InstancesTab({ file }: { file: File }) { } }; + const getDeviceDisplay = (sdPath: typeof file.sd_path) => { + if ("Physical" in sdPath) { + return sdPath.Physical.device_slug || "Local Device"; + } else if ("Cloud" in sdPath) { + return "Cloud Storage"; + } else { + return "Content Addressed"; + } + }; + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString("en-US", { + month: "short", + day: "numeric", + year: "numeric", + }); + }; + + if (instancesQuery.isLoading) { + return ( +
+ Loading instances... +
+ ); + } + + if (!file.content_identity) { + return ( +
+

+ This file has not been content-hashed yet. Instances will + appear after indexing completes. +

+
+ ); + } + return (

All copies of this file across your devices and locations

- {allPaths.length === 1 ? ( + {instances.length === 0 || instances.length === 1 ? (
No alternate instances found
) : ( -
- {allPaths.map((sdPath, i) => ( +
+ {instances.map((instance, i) => (
-
- - - -
-
- {getPathDisplay(sdPath)} +
+ {/* Thumbnail */} +
+ +
+ + {/* Info */} +
+
+
+
+ {instance.name} + {instance.extension && + `.${instance.extension}`} +
+
+ {formatBytes(instance.size)} +
+
+
-
- {"Physical" in sdPath && "Local Device"} - {"Cloud" in sdPath && "Cloud Storage"} - {"Content" in sdPath && - "Content Addressed"} + +
+ + + {getDeviceDisplay( + instance.sd_path, + )} + +
+ +
+ {getPathDisplay(instance.sd_path)} +
+ +
+ Modified{" "} + {formatDate(instance.modified_at)}
-
))} diff --git a/packages/ts-client/src/generated/types.ts b/packages/ts-client/src/generated/types.ts index 3f0a88169..25e5828bc 100644 --- a/packages/ts-client/src/generated/types.ts +++ b/packages/ts-client/src/generated/types.ts @@ -22,6 +22,28 @@ export type AddItemInput = { space_id: string; group_id: string | null; item_typ export type AddItemOutput = { item: SpaceItem }; +/** + * Input for alternate instances query + */ +export type AlternateInstancesInput = { +/** + * The entry UUID to find alternates for + */ +entry_uuid: string }; + +/** + * Output containing alternate instances + */ +export type AlternateInstancesOutput = { +/** + * All instances of this file (including the original) + */ +instances: File[]; +/** + * Total number of instances found + */ +total_count: number }; + /** * Represents an APFS container (physical storage with multiple volumes) */ @@ -4178,108 +4200,109 @@ success: boolean }; // ===== API Type Unions ===== export type CoreAction = - { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } - | { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput } - | { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput } + { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput } + | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } | { type: 'libraries.open'; input: LibraryOpenInput; output: LibraryOpenOutput } | { type: 'network.pair.join'; input: PairJoinInput; output: PairJoinOutput } + | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } + | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } + | { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput } + | { type: 'network.pair.cancel'; input: PairCancelInput; output: PairCancelOutput } | { type: 'network.stop'; input: NetworkStopInput; output: NetworkStopOutput } | { type: 'libraries.delete'; input: LibraryDeleteInput; output: LibraryDeleteOutput } | { type: 'network.device.revoke'; input: DeviceRevokeInput; output: DeviceRevokeOutput } - | { type: 'network.spacedrop.send'; input: SpacedropSendInput; output: SpacedropSendOutput } - | { type: 'libraries.create'; input: LibraryCreateInput; output: LibraryCreateOutput } - | { type: 'core.ephemeral_reset'; input: EphemeralCacheResetInput; output: EphemeralCacheResetOutput } | { type: 'network.start'; input: NetworkStartInput; output: NetworkStartOutput } + | { type: 'models.whisper.delete'; input: DeleteWhisperModelInput; output: DeleteWhisperModelOutput } + | { type: 'models.whisper.download'; input: DownloadWhisperModelInput; output: DownloadWhisperModelOutput } | { type: 'network.sync_setup'; input: LibrarySyncSetupInput; output: LibrarySyncSetupOutput } - | { type: 'network.pair.generate'; input: PairGenerateInput; output: PairGenerateOutput } - | { type: 'core.reset'; input: ResetDataInput; output: ResetDataOutput } ; export type LibraryAction = - { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput } - | { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput } - | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } - | { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput } - | { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput } - | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } - | { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } - | { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput } - | { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } - | { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput } - | { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput } - | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } - | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } - | { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput } - | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } - | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } - | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } - | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } - | { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput } - | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } - | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } - | { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput } - | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } - | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } + { type: 'tags.apply'; input: ApplyTagsInput; output: ApplyTagsOutput } + | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } | { type: 'files.rename'; input: FileRenameInput; output: JobReceipt } - | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } - | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } - | { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput } - | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } - | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'files.createFolder'; input: CreateFolderInput; output: CreateFolderOutput } + | { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput } + | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } + | { type: 'spaces.add_item'; input: AddItemInput; output: AddItemOutput } + | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } + | { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput } | { type: 'indexing.start'; input: IndexInput; output: JobReceipt } | { type: 'volumes.untrack'; input: VolumeUntrackInput; output: VolumeUntrackOutput } - | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } - | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } - | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } - | { type: 'volumes.track'; input: VolumeTrackInput; output: VolumeTrackOutput } - | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } - | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } - | { type: 'media.speech.transcribe'; input: TranscribeAudioInput; output: TranscribeAudioOutput } - | { type: 'locations.export'; input: LocationExportInput; output: LocationExportOutput } - | { type: 'spaces.create'; input: SpaceCreateInput; output: SpaceCreateOutput } - | { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput } - | { type: 'jobs.pause'; input: JobPauseInput; output: JobPauseOutput } + | { type: 'media.ocr.extract'; input: ExtractTextInput; output: ExtractTextOutput } + | { type: 'jobs.cancel'; input: JobCancelInput; output: JobCancelOutput } | { type: 'spaces.reorder_items'; input: ReorderItemsInput; output: ReorderOutput } | { type: 'spaces.reorder_groups'; input: ReorderGroupsInput; output: ReorderOutput } + | { type: 'media.proxy.generate'; input: GenerateProxyInput; output: GenerateProxyOutput } + | { type: 'volumes.remove_cloud'; input: VolumeRemoveCloudInput; output: VolumeRemoveCloudOutput } + | { type: 'spaces.delete_group'; input: DeleteGroupInput; output: DeleteGroupOutput } + | { type: 'libraries.export'; input: LibraryExportInput; output: LibraryExportOutput } + | { type: 'libraries.rename'; input: LibraryRenameInput; output: LibraryRenameOutput } + | { type: 'spaces.update_group'; input: UpdateGroupInput; output: UpdateGroupOutput } + | { type: 'tags.create'; input: CreateTagInput; output: CreateTagOutput } + | { type: 'spaces.add_group'; input: AddGroupInput; output: AddGroupOutput } + | { type: 'locations.add'; input: LocationAddInput; output: LocationAddOutput } + | { type: 'volumes.speed_test'; input: VolumeSpeedTestInput; output: VolumeSpeedTestOutput } + | { type: 'spaces.delete'; input: SpaceDeleteInput; output: SpaceDeleteOutput } + | { type: 'indexing.verify'; input: IndexVerifyInput; output: IndexVerifyOutput } + | { type: 'volumes.index'; input: IndexVolumeInput; output: IndexVolumeOutput } + | { type: 'media.splat.generate'; input: GenerateSplatInput; output: GenerateSplatOutput } + | { type: 'jobs.resume'; input: JobResumeInput; output: JobResumeOutput } + | { type: 'locations.remove'; input: LocationRemoveInput; output: LocationRemoveOutput } + | { type: 'spaces.update'; input: SpaceUpdateInput; output: SpaceUpdateOutput } + | { type: 'locations.triggerJob'; input: LocationTriggerJobInput; output: LocationTriggerJobOutput } + | { type: 'files.delete'; input: FileDeleteInput; output: JobReceipt } + | { type: 'files.copy'; input: FileCopyInput; output: JobReceipt } + | { type: 'media.thumbnail.regenerate'; input: RegenerateThumbnailInput; output: RegenerateThumbnailOutput } + | { type: 'media.thumbnail'; input: ThumbnailInput; output: JobReceipt } + | { type: 'volumes.refresh'; input: VolumeRefreshInput; output: VolumeRefreshOutput } + | { type: 'locations.update'; input: LocationUpdateInput; output: LocationUpdateOutput } + | { type: 'locations.enable_indexing'; input: EnableIndexingInput; output: EnableIndexingOutput } + | { type: 'locations.rescan'; input: LocationRescanInput; output: LocationRescanOutput } + | { type: 'locations.import'; input: LocationImportInput; output: LocationImportOutput } + | { type: 'volumes.add_cloud'; input: VolumeAddCloudInput; output: VolumeAddCloudOutput } + | { type: 'media.thumbstrip.generate'; input: GenerateThumbstripInput; output: GenerateThumbstripOutput } + | { type: 'spaces.delete_item'; input: DeleteItemInput; output: DeleteItemOutput } ; export type CoreQuery = - { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } - | { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus } - | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } - | { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput } - | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } - | { type: 'core.status'; input: Empty; output: CoreStatus } - | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } - | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } - | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } - | { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput } + { type: 'jobs.remote.all_devices'; input: RemoteJobsAllDevicesInput; output: RemoteJobsAllDevicesOutput } | { type: 'jobs.remote.for_device'; input: RemoteJobsForDeviceInput; output: RemoteJobsForDeviceOutput } + | { type: 'network.devices.list'; input: ListPairedDevicesInput; output: ListPairedDevicesOutput } + | { type: 'network.status'; input: NetworkStatusQueryInput; output: NetworkStatus } + | { type: 'libraries.list'; input: ListLibrariesInput; output: [LibraryInfo] } + | { type: 'models.whisper.list'; input: ListWhisperModelsInput; output: ListWhisperModelsOutput } + | { type: 'network.sync_setup.discover'; input: DiscoverRemoteLibrariesInput; output: DiscoverRemoteLibrariesOutput } + | { type: 'core.events.list'; input: ListEventsInput; output: ListEventsOutput } + | { type: 'core.status'; input: Empty; output: CoreStatus } + | { type: 'core.ephemeral_status'; input: EphemeralCacheStatusInput; output: EphemeralCacheStatus } + | { type: 'network.pair.status'; input: PairStatusQueryInput; output: PairStatusOutput } ; export type LibraryQuery = - { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } - | { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput } - | { type: 'files.by_path'; input: FileByPathQuery; output: File } - | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } - | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } - | { type: 'test.ping'; input: PingInput; output: PingOutput } - | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } - | { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput } - | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + { type: 'sync.eventLog'; input: GetSyncEventLogInput; output: GetSyncEventLogOutput } | { type: 'jobs.list'; input: JobListInput; output: JobListOutput } - | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } - | { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] } - | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } - | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout } | { type: 'jobs.active'; input: ActiveJobsInput; output: ActiveJobsOutput } - | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } - | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library } - | { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput } - | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'tags.search'; input: SearchTagsInput; output: SearchTagsOutput } + | { type: 'test.ping'; input: PingInput; output: PingOutput } + | { type: 'sync.activity'; input: GetSyncActivityInput; output: GetSyncActivityOutput } + | { type: 'devices.list'; input: ListLibraryDevicesInput; output: [Device] } | { type: 'files.by_id'; input: FileByIdQuery; output: File } + | { type: 'locations.validate_path'; input: ValidateLocationPathInput; output: ValidateLocationPathOutput } + | { type: 'spaces.get_layout'; input: SpaceLayoutQueryInput; output: SpaceLayout } + | { type: 'search.files'; input: FileSearchInput; output: FileSearchOutput } + | { type: 'files.media_listing'; input: MediaListingInput; output: MediaListingOutput } + | { type: 'sync.metrics'; input: GetSyncMetricsInput; output: GetSyncMetricsOutput } + | { type: 'files.content_kind_stats'; input: ContentKindStatsInput; output: ContentKindStatsOutput } + | { type: 'locations.suggested'; input: SuggestedLocationsQueryInput; output: SuggestedLocationsOutput } + | { type: 'jobs.info'; input: JobInfoQueryInput; output: JobInfoOutput } + | { type: 'files.by_path'; input: FileByPathQuery; output: File } | { type: 'files.unique_to_location'; input: UniqueToLocationInput; output: UniqueToLocationOutput } + | { type: 'files.directory_listing'; input: DirectoryListingInput; output: DirectoryListingOutput } + | { type: 'volumes.list'; input: VolumeListQueryInput; output: VolumeListOutput } | { type: 'spaces.list'; input: SpacesListQueryInput; output: SpacesListOutput } + | { type: 'libraries.info'; input: LibraryInfoQueryInput; output: Library } + | { type: 'files.alternate_instances'; input: AlternateInstancesInput; output: AlternateInstancesOutput } | { type: 'spaces.get'; input: SpaceGetQueryInput; output: SpaceGetOutput } | { type: 'locations.list'; input: LocationsListQueryInput; output: LocationsListOutput } ; @@ -4288,108 +4311,109 @@ export type LibraryQuery = export const WIRE_METHODS = { coreActions: { - 'network.pair.cancel': 'action:network.pair.cancel.input', - 'models.whisper.delete': 'action:models.whisper.delete.input', - 'models.whisper.download': 'action:models.whisper.download.input', + 'core.ephemeral_reset': 'action:core.ephemeral_reset.input', + 'network.spacedrop.send': 'action:network.spacedrop.send.input', 'libraries.open': 'action:libraries.open.input', 'network.pair.join': 'action:network.pair.join.input', + 'libraries.create': 'action:libraries.create.input', + 'network.pair.generate': 'action:network.pair.generate.input', + 'core.reset': 'action:core.reset.input', + 'network.pair.cancel': 'action:network.pair.cancel.input', 'network.stop': 'action:network.stop.input', 'libraries.delete': 'action:libraries.delete.input', 'network.device.revoke': 'action:network.device.revoke.input', - 'network.spacedrop.send': 'action:network.spacedrop.send.input', - 'libraries.create': 'action:libraries.create.input', - 'core.ephemeral_reset': 'action:core.ephemeral_reset.input', 'network.start': 'action:network.start.input', + 'models.whisper.delete': 'action:models.whisper.delete.input', + 'models.whisper.download': 'action:models.whisper.download.input', 'network.sync_setup': 'action:network.sync_setup.input', - 'network.pair.generate': 'action:network.pair.generate.input', - 'core.reset': 'action:core.reset.input', }, libraryActions: { - 'locations.triggerJob': 'action:locations.triggerJob.input', - 'locations.enable_indexing': 'action:locations.enable_indexing.input', - 'spaces.delete_group': 'action:spaces.delete_group.input', - 'media.splat.generate': 'action:media.splat.generate.input', - 'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input', - 'media.thumbnail': 'action:media.thumbnail.input', - 'files.copy': 'action:files.copy.input', - 'volumes.index': 'action:volumes.index.input', 'tags.apply': 'action:tags.apply.input', - 'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input', - 'locations.update': 'action:locations.update.input', - 'locations.add': 'action:locations.add.input', - 'jobs.resume': 'action:jobs.resume.input', - 'files.createFolder': 'action:files.createFolder.input', - 'spaces.delete': 'action:spaces.delete.input', - 'spaces.add_item': 'action:spaces.add_item.input', - 'jobs.cancel': 'action:jobs.cancel.input', - 'files.delete': 'action:files.delete.input', - 'volumes.refresh': 'action:volumes.refresh.input', - 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', - 'indexing.verify': 'action:indexing.verify.input', - 'media.ocr.extract': 'action:media.ocr.extract.input', - 'locations.rescan': 'action:locations.rescan.input', - 'spaces.update': 'action:spaces.update.input', + 'spaces.create': 'action:spaces.create.input', 'files.rename': 'action:files.rename.input', - 'libraries.rename': 'action:libraries.rename.input', - 'volumes.add_cloud': 'action:volumes.add_cloud.input', - 'media.proxy.generate': 'action:media.proxy.generate.input', - 'libraries.export': 'action:libraries.export.input', - 'locations.remove': 'action:locations.remove.input', + 'files.createFolder': 'action:files.createFolder.input', + 'locations.export': 'action:locations.export.input', + 'jobs.pause': 'action:jobs.pause.input', + 'spaces.add_item': 'action:spaces.add_item.input', + 'volumes.track': 'action:volumes.track.input', + 'media.speech.transcribe': 'action:media.speech.transcribe.input', 'indexing.start': 'action:indexing.start.input', 'volumes.untrack': 'action:volumes.untrack.input', - 'volumes.speed_test': 'action:volumes.speed_test.input', - 'tags.create': 'action:tags.create.input', - 'spaces.update_group': 'action:spaces.update_group.input', - 'volumes.track': 'action:volumes.track.input', - 'spaces.delete_item': 'action:spaces.delete_item.input', - 'spaces.add_group': 'action:spaces.add_group.input', - 'media.speech.transcribe': 'action:media.speech.transcribe.input', - 'locations.export': 'action:locations.export.input', - 'spaces.create': 'action:spaces.create.input', - 'locations.import': 'action:locations.import.input', - 'jobs.pause': 'action:jobs.pause.input', + 'media.ocr.extract': 'action:media.ocr.extract.input', + 'jobs.cancel': 'action:jobs.cancel.input', 'spaces.reorder_items': 'action:spaces.reorder_items.input', 'spaces.reorder_groups': 'action:spaces.reorder_groups.input', + 'media.proxy.generate': 'action:media.proxy.generate.input', + 'volumes.remove_cloud': 'action:volumes.remove_cloud.input', + 'spaces.delete_group': 'action:spaces.delete_group.input', + 'libraries.export': 'action:libraries.export.input', + 'libraries.rename': 'action:libraries.rename.input', + 'spaces.update_group': 'action:spaces.update_group.input', + 'tags.create': 'action:tags.create.input', + 'spaces.add_group': 'action:spaces.add_group.input', + 'locations.add': 'action:locations.add.input', + 'volumes.speed_test': 'action:volumes.speed_test.input', + 'spaces.delete': 'action:spaces.delete.input', + 'indexing.verify': 'action:indexing.verify.input', + 'volumes.index': 'action:volumes.index.input', + 'media.splat.generate': 'action:media.splat.generate.input', + 'jobs.resume': 'action:jobs.resume.input', + 'locations.remove': 'action:locations.remove.input', + 'spaces.update': 'action:spaces.update.input', + 'locations.triggerJob': 'action:locations.triggerJob.input', + 'files.delete': 'action:files.delete.input', + 'files.copy': 'action:files.copy.input', + 'media.thumbnail.regenerate': 'action:media.thumbnail.regenerate.input', + 'media.thumbnail': 'action:media.thumbnail.input', + 'volumes.refresh': 'action:volumes.refresh.input', + 'locations.update': 'action:locations.update.input', + 'locations.enable_indexing': 'action:locations.enable_indexing.input', + 'locations.rescan': 'action:locations.rescan.input', + 'locations.import': 'action:locations.import.input', + 'volumes.add_cloud': 'action:volumes.add_cloud.input', + 'media.thumbstrip.generate': 'action:media.thumbstrip.generate.input', + 'spaces.delete_item': 'action:spaces.delete_item.input', }, coreQueries: { - 'core.events.list': 'query:core.events.list', - 'core.ephemeral_status': 'query:core.ephemeral_status', - 'network.sync_setup.discover': 'query:network.sync_setup.discover', - 'models.whisper.list': 'query:models.whisper.list', - 'network.pair.status': 'query:network.pair.status', - 'core.status': 'query:core.status', - 'network.status': 'query:network.status', - 'network.devices.list': 'query:network.devices.list', - 'libraries.list': 'query:libraries.list', 'jobs.remote.all_devices': 'query:jobs.remote.all_devices', 'jobs.remote.for_device': 'query:jobs.remote.for_device', + 'network.devices.list': 'query:network.devices.list', + 'network.status': 'query:network.status', + 'libraries.list': 'query:libraries.list', + 'models.whisper.list': 'query:models.whisper.list', + 'network.sync_setup.discover': 'query:network.sync_setup.discover', + 'core.events.list': 'query:core.events.list', + 'core.status': 'query:core.status', + 'core.ephemeral_status': 'query:core.ephemeral_status', + 'network.pair.status': 'query:network.pair.status', }, libraryQueries: { - 'locations.suggested': 'query:locations.suggested', - 'sync.activity': 'query:sync.activity', - 'files.by_path': 'query:files.by_path', - 'files.media_listing': 'query:files.media_listing', - 'files.directory_listing': 'query:files.directory_listing', - 'test.ping': 'query:test.ping', - 'sync.metrics': 'query:sync.metrics', 'sync.eventLog': 'query:sync.eventLog', - 'tags.search': 'query:tags.search', 'jobs.list': 'query:jobs.list', - 'volumes.list': 'query:volumes.list', - 'devices.list': 'query:devices.list', - 'search.files': 'query:search.files', - 'spaces.get_layout': 'query:spaces.get_layout', 'jobs.active': 'query:jobs.active', - 'jobs.info': 'query:jobs.info', - 'libraries.info': 'query:libraries.info', - 'files.content_kind_stats': 'query:files.content_kind_stats', - 'locations.validate_path': 'query:locations.validate_path', + 'tags.search': 'query:tags.search', + 'test.ping': 'query:test.ping', + 'sync.activity': 'query:sync.activity', + 'devices.list': 'query:devices.list', 'files.by_id': 'query:files.by_id', + 'locations.validate_path': 'query:locations.validate_path', + 'spaces.get_layout': 'query:spaces.get_layout', + 'search.files': 'query:search.files', + 'files.media_listing': 'query:files.media_listing', + 'sync.metrics': 'query:sync.metrics', + 'files.content_kind_stats': 'query:files.content_kind_stats', + 'locations.suggested': 'query:locations.suggested', + 'jobs.info': 'query:jobs.info', + 'files.by_path': 'query:files.by_path', 'files.unique_to_location': 'query:files.unique_to_location', + 'files.directory_listing': 'query:files.directory_listing', + 'volumes.list': 'query:volumes.list', 'spaces.list': 'query:spaces.list', + 'libraries.info': 'query:libraries.info', + 'files.alternate_instances': 'query:files.alternate_instances', 'spaces.get': 'query:spaces.get', 'locations.list': 'query:locations.list', },