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.
This commit is contained in:
Jamie Pine
2025-12-26 11:20:01 -08:00
parent 8d0ae5065f
commit 996df041a4
6 changed files with 767 additions and 168 deletions

View File

@@ -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<File>,
/// 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<Self> {
Ok(Self { input })
}
async fn execute(
self,
context: Arc<CoreContext>,
session: crate::infra::api::SessionContext,
) -> QueryResult<Self::Output> {
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<Uuid> = alternate_entries.iter().filter_map(|e| e.uuid).collect();
let mut tags_by_entry: HashMap<Uuid, Vec<crate::domain::tag::Tag>> = 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<i32> = 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<i32> = 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<i32, crate::domain::tag::Tag> = 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<Tag> mapping
let mut tags_by_metadata: HashMap<i32, Vec<crate::domain::tag::Tag>> =
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<SdPath> {
// 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");

View File

@@ -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<Vec<SdPath>> {
// 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,

View File

@@ -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<Vec<SdPath>> {
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,

View File

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

View File

@@ -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 (
<div className="flex items-center justify-center py-8 text-xs text-sidebar-inkDull">
Loading instances...
</div>
);
}
if (!file.content_identity) {
return (
<div className="no-scrollbar mask-fade-out flex flex-col space-y-4 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
<p className="text-xs text-sidebar-inkDull">
This file has not been content-hashed yet. Instances will
appear after indexing completes.
</p>
</div>
);
}
return (
<div className="no-scrollbar mask-fade-out flex flex-col space-y-4 overflow-x-hidden overflow-y-scroll pb-10 px-2 pt-2">
<p className="text-xs text-sidebar-inkDull">
All copies of this file across your devices and locations
</p>
{allPaths.length === 1 ? (
{instances.length === 0 || instances.length === 1 ? (
<div className="flex items-center justify-center py-8 text-xs text-sidebar-inkDull">
No alternate instances found
</div>
) : (
<div className="space-y-2">
{allPaths.map((sdPath, i) => (
<div className="space-y-2.5">
{instances.map((instance, i) => (
<div
key={i}
className="p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 space-y-2"
className="p-2.5 bg-app-box/40 rounded-lg border border-app-line/50 hover:bg-app-box/60 transition-colors"
>
<div className="flex items-start gap-2">
<span className="text-accent shrink-0 mt-0.5">
<MapPin size={16} weight="bold" />
</span>
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-sidebar-ink truncate font-mono">
{getPathDisplay(sdPath)}
<div className="flex items-start gap-3">
{/* Thumbnail */}
<div className="shrink-0">
<FileComponent.Thumb
file={instance}
size={64}
iconScale={0.5}
className="rounded overflow-hidden"
/>
</div>
{/* Info */}
<div className="flex-1 min-w-0 space-y-1.5">
<div className="flex items-start justify-between gap-2">
<div className="flex-1 min-w-0">
<div className="text-xs font-medium text-sidebar-ink truncate">
{instance.name}
{instance.extension &&
`.${instance.extension}`}
</div>
<div className="text-[11px] text-sidebar-inkDull mt-0.5">
{formatBytes(instance.size)}
</div>
</div>
<div
className={clsx(
"size-2 rounded-full shrink-0 mt-1",
instance.is_local
? "bg-accent"
: "bg-sidebar-inkDull/40",
)}
title={
instance.is_local
? "Available locally"
: "Remote"
}
/>
</div>
<div className="text-[11px] text-sidebar-inkDull mt-1">
{"Physical" in sdPath && "Local Device"}
{"Cloud" in sdPath && "Cloud Storage"}
{"Content" in sdPath &&
"Content Addressed"}
<div className="flex items-center gap-1.5 text-[11px] text-sidebar-inkDull">
<MapPin size={12} weight="bold" />
<span className="truncate">
{getDeviceDisplay(
instance.sd_path,
)}
</span>
</div>
<div className="text-[10px] text-sidebar-inkDull/70 font-mono truncate">
{getPathDisplay(instance.sd_path)}
</div>
<div className="text-[10px] text-sidebar-inkDull/70">
Modified{" "}
{formatDate(instance.modified_at)}
</div>
</div>
<div className="size-2 rounded-full shrink-0 mt-1 bg-accent" />
</div>
</div>
))}

View File

@@ -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',
},