mirror of
https://github.com/immich-app/immich.git
synced 2026-04-29 04:20:38 -04:00
server changes
This commit is contained in:
parent
fe9e5afcf4
commit
0bcd85eb2c
@ -16258,8 +16258,10 @@
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration (for videos)",
|
||||
"type": "string"
|
||||
"description": "Duration in milliseconds (for videos)",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"type": "integer"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"description": "File creation date",
|
||||
@ -16627,9 +16629,11 @@
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Video/gif duration in hh:mm:ss.SSS format (null for static images)",
|
||||
"description": "Video/gif duration in milliseconds (null for static images)",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"exifInfo": {
|
||||
"$ref": "#/components/schemas/ExifResponseDto"
|
||||
@ -23146,6 +23150,135 @@
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAssetV2": {
|
||||
"properties": {
|
||||
"checksum": {
|
||||
"description": "Checksum",
|
||||
"type": "string"
|
||||
},
|
||||
"deletedAt": {
|
||||
"description": "Deleted at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Duration",
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"fileCreatedAt": {
|
||||
"description": "File created at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"fileModifiedAt": {
|
||||
"description": "File modified at",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"height": {
|
||||
"description": "Asset height",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
},
|
||||
"id": {
|
||||
"description": "Asset ID",
|
||||
"type": "string"
|
||||
},
|
||||
"isEdited": {
|
||||
"description": "Is edited",
|
||||
"type": "boolean"
|
||||
},
|
||||
"isFavorite": {
|
||||
"description": "Is favorite",
|
||||
"type": "boolean"
|
||||
},
|
||||
"libraryId": {
|
||||
"description": "Library ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"livePhotoVideoId": {
|
||||
"description": "Live photo video ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"localDateTime": {
|
||||
"description": "Local date time",
|
||||
"example": "2024-01-01T00:00:00.000Z",
|
||||
"format": "date-time",
|
||||
"nullable": true,
|
||||
"pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$",
|
||||
"type": "string"
|
||||
},
|
||||
"originalFileName": {
|
||||
"description": "Original file name",
|
||||
"type": "string"
|
||||
},
|
||||
"ownerId": {
|
||||
"description": "Owner ID",
|
||||
"type": "string"
|
||||
},
|
||||
"stackId": {
|
||||
"description": "Stack ID",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"thumbhash": {
|
||||
"description": "Thumbhash",
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"$ref": "#/components/schemas/AssetTypeEnum"
|
||||
},
|
||||
"visibility": {
|
||||
"$ref": "#/components/schemas/AssetVisibility"
|
||||
},
|
||||
"width": {
|
||||
"description": "Asset width",
|
||||
"maximum": 9007199254740991,
|
||||
"minimum": -9007199254740991,
|
||||
"nullable": true,
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"checksum",
|
||||
"deletedAt",
|
||||
"duration",
|
||||
"fileCreatedAt",
|
||||
"fileModifiedAt",
|
||||
"height",
|
||||
"id",
|
||||
"isEdited",
|
||||
"isFavorite",
|
||||
"libraryId",
|
||||
"livePhotoVideoId",
|
||||
"localDateTime",
|
||||
"originalFileName",
|
||||
"ownerId",
|
||||
"stackId",
|
||||
"thumbhash",
|
||||
"type",
|
||||
"visibility",
|
||||
"width"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"SyncAuthUserV1": {
|
||||
"properties": {
|
||||
"avatarColor": {
|
||||
@ -23246,6 +23379,7 @@
|
||||
"UserV1",
|
||||
"UserDeleteV1",
|
||||
"AssetV1",
|
||||
"AssetV2",
|
||||
"AssetDeleteV1",
|
||||
"AssetExifV1",
|
||||
"AssetEditV1",
|
||||
@ -23255,7 +23389,9 @@
|
||||
"PartnerV1",
|
||||
"PartnerDeleteV1",
|
||||
"PartnerAssetV1",
|
||||
"PartnerAssetV2",
|
||||
"PartnerAssetBackfillV1",
|
||||
"PartnerAssetBackfillV2",
|
||||
"PartnerAssetDeleteV1",
|
||||
"PartnerAssetExifV1",
|
||||
"PartnerAssetExifBackfillV1",
|
||||
@ -23269,8 +23405,11 @@
|
||||
"AlbumUserBackfillV1",
|
||||
"AlbumUserDeleteV1",
|
||||
"AlbumAssetCreateV1",
|
||||
"AlbumAssetCreateV2",
|
||||
"AlbumAssetUpdateV1",
|
||||
"AlbumAssetUpdateV2",
|
||||
"AlbumAssetBackfillV1",
|
||||
"AlbumAssetBackfillV2",
|
||||
"AlbumAssetExifCreateV1",
|
||||
"AlbumAssetExifUpdateV1",
|
||||
"AlbumAssetExifBackfillV1",
|
||||
@ -24964,10 +25103,12 @@
|
||||
"type": "array"
|
||||
},
|
||||
"duration": {
|
||||
"description": "Array of video/gif durations in hh:mm:ss.SSS format (null for static images)",
|
||||
"description": "Array of video/gif durations in milliseconds (null for static images)",
|
||||
"items": {
|
||||
"maximum": 2147483647,
|
||||
"minimum": 0,
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
"type": "integer"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
|
||||
@ -38,7 +38,7 @@ export enum UploadFieldName {
|
||||
const AssetMediaBaseSchema = z.object({
|
||||
fileCreatedAt: isoDatetimeToDate.describe('File creation date'),
|
||||
fileModifiedAt: isoDatetimeToDate.describe('File modification date'),
|
||||
duration: z.string().optional().describe('Duration (for videos)'),
|
||||
duration: z.int32().min(0).optional().describe('Duration in milliseconds (for videos)'),
|
||||
filename: z.string().optional().describe('Filename'),
|
||||
/** The properties below are added to correctly generate the API docs and client SDKs. Validation should be handled in the controller. */
|
||||
[UploadFieldName.ASSET_DATA]: z.any().describe('Asset file data').meta({ type: 'string', format: 'binary' }),
|
||||
|
||||
@ -47,7 +47,7 @@ const SanitizedAssetResponseSchema = z
|
||||
.describe(
|
||||
'The local date and time when the photo/video was taken, derived from EXIF metadata. This represents the photographer\'s local time regardless of timezone, stored as a timezone-agnostic timestamp. Used for timeline grouping by "local" days and months.',
|
||||
),
|
||||
duration: z.string().nullable().describe('Video/gif duration in hh:mm:ss.SSS format (null for static images)'),
|
||||
duration: z.int32().min(0).nullable().describe('Video/gif duration in milliseconds (null for static images)'),
|
||||
livePhotoVideoId: z.string().nullish().describe('Live photo video ID'),
|
||||
hasMetadata: z.boolean().describe('Whether asset has metadata'),
|
||||
width: z.number().min(0).nullable().describe('Asset width'),
|
||||
@ -136,7 +136,7 @@ export type MapAsset = {
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
checksumAlgorithm: ChecksumAlgorithm;
|
||||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
duration: number | null;
|
||||
edits?: ShallowDehydrateObject<AssetEditActionItem>[];
|
||||
exifInfo?: ShallowDehydrateObject<Selectable<Exif>> | null;
|
||||
faces?: ShallowDehydrateObject<AssetFace>[];
|
||||
|
||||
@ -90,6 +90,30 @@ const SyncAssetV1Schema = z
|
||||
})
|
||||
.meta({ id: 'SyncAssetV1' });
|
||||
|
||||
const SyncAssetV2Schema = z
|
||||
.object({
|
||||
id: z.string().describe('Asset ID'),
|
||||
ownerId: z.string().describe('Owner ID'),
|
||||
originalFileName: z.string().describe('Original file name'),
|
||||
thumbhash: z.string().nullable().describe('Thumbhash'),
|
||||
checksum: z.string().describe('Checksum'),
|
||||
fileCreatedAt: isoDatetimeToDate.nullable().describe('File created at'),
|
||||
fileModifiedAt: isoDatetimeToDate.nullable().describe('File modified at'),
|
||||
localDateTime: isoDatetimeToDate.nullable().describe('Local date time'),
|
||||
duration: z.int32().min(0).nullable().describe('Duration'),
|
||||
type: AssetTypeSchema,
|
||||
deletedAt: isoDatetimeToDate.nullable().describe('Deleted at'),
|
||||
isFavorite: z.boolean().describe('Is favorite'),
|
||||
visibility: AssetVisibilitySchema,
|
||||
livePhotoVideoId: z.string().nullable().describe('Live photo video ID'),
|
||||
stackId: z.string().nullable().describe('Stack ID'),
|
||||
libraryId: z.string().nullable().describe('Library ID'),
|
||||
width: z.int().nullable().describe('Asset width'),
|
||||
height: z.int().nullable().describe('Asset height'),
|
||||
isEdited: z.boolean().describe('Is edited'),
|
||||
})
|
||||
.meta({ id: 'SyncAssetV2' });
|
||||
|
||||
@ExtraModel()
|
||||
class SyncUserV1 extends createZodDto(SyncUserV1Schema) {}
|
||||
@ExtraModel()
|
||||
@ -102,6 +126,8 @@ class SyncPartnerV1 extends createZodDto(SyncPartnerV1Schema) {}
|
||||
class SyncPartnerDeleteV1 extends createZodDto(SyncPartnerDeleteV1Schema) {}
|
||||
@ExtraModel()
|
||||
export class SyncAssetV1 extends createZodDto(SyncAssetV1Schema) {}
|
||||
@ExtraModel()
|
||||
export class SyncAssetV2 extends createZodDto(SyncAssetV2Schema) {}
|
||||
|
||||
const SyncAssetDeleteV1Schema = z
|
||||
.object({ assetId: z.string().describe('Asset ID') })
|
||||
@ -420,6 +446,7 @@ export type SyncItem = {
|
||||
[SyncEntityType.PartnerV1]: SyncPartnerV1;
|
||||
[SyncEntityType.PartnerDeleteV1]: SyncPartnerDeleteV1;
|
||||
[SyncEntityType.AssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.AssetV2]: SyncAssetV2;
|
||||
[SyncEntityType.AssetDeleteV1]: SyncAssetDeleteV1;
|
||||
[SyncEntityType.AssetMetadataV1]: SyncAssetMetadataV1;
|
||||
[SyncEntityType.AssetMetadataDeleteV1]: SyncAssetMetadataDeleteV1;
|
||||
@ -427,7 +454,9 @@ export type SyncItem = {
|
||||
[SyncEntityType.AssetEditV1]: SyncAssetEditV1;
|
||||
[SyncEntityType.AssetEditDeleteV1]: SyncAssetEditDeleteV1;
|
||||
[SyncEntityType.PartnerAssetV1]: SyncAssetV1;
|
||||
[SyncEntityType.PartnerAssetV2]: SyncAssetV2;
|
||||
[SyncEntityType.PartnerAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.PartnerAssetBackfillV2]: SyncAssetV2;
|
||||
[SyncEntityType.PartnerAssetDeleteV1]: SyncAssetDeleteV1;
|
||||
[SyncEntityType.PartnerAssetExifV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.PartnerAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
@ -438,8 +467,11 @@ export type SyncItem = {
|
||||
[SyncEntityType.AlbumUserBackfillV1]: SyncAlbumUserV1;
|
||||
[SyncEntityType.AlbumUserDeleteV1]: SyncAlbumUserDeleteV1;
|
||||
[SyncEntityType.AlbumAssetCreateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetCreateV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetUpdateV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetUpdateV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetBackfillV1]: SyncAssetV1;
|
||||
[SyncEntityType.AlbumAssetBackfillV2]: SyncAssetV2;
|
||||
[SyncEntityType.AlbumAssetExifCreateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifUpdateV1]: SyncAssetExifV1;
|
||||
[SyncEntityType.AlbumAssetExifBackfillV1]: SyncAssetExifV1;
|
||||
|
||||
@ -89,8 +89,8 @@ const TimeBucketAssetResponseSchema = z
|
||||
"Array of UTC offset hours at the time each photo was taken. Positive values are east of UTC, negative values are west of UTC. Values may be fractional (e.g., 5.5 for +05:30, -9.75 for -09:45). Applying this offset to 'fileCreatedAt' will give you the time the photo was taken from the photographer's perspective.",
|
||||
),
|
||||
duration: z
|
||||
.array(z.string().nullable())
|
||||
.describe('Array of video/gif durations in hh:mm:ss.SSS format (null for static images)'),
|
||||
.array(z.int32().min(0).nullable())
|
||||
.describe('Array of video/gif durations in milliseconds (null for static images)'),
|
||||
stack: z
|
||||
.array(stackTupleSchema)
|
||||
.optional()
|
||||
|
||||
@ -802,8 +802,10 @@ export enum SyncRequestType {
|
||||
AlbumUsersV1 = 'AlbumUsersV1',
|
||||
AlbumToAssetsV1 = 'AlbumToAssetsV1',
|
||||
AlbumAssetsV1 = 'AlbumAssetsV1',
|
||||
AlbumAssetsV2 = 'AlbumAssetsV2',
|
||||
AlbumAssetExifsV1 = 'AlbumAssetExifsV1',
|
||||
AssetsV1 = 'AssetsV1',
|
||||
AssetsV2 = 'AssetsV2',
|
||||
AssetExifsV1 = 'AssetExifsV1',
|
||||
AssetEditsV1 = 'AssetEditsV1',
|
||||
AssetMetadataV1 = 'AssetMetadataV1',
|
||||
@ -812,6 +814,7 @@ export enum SyncRequestType {
|
||||
MemoryToAssetsV1 = 'MemoryToAssetsV1',
|
||||
PartnersV1 = 'PartnersV1',
|
||||
PartnerAssetsV1 = 'PartnerAssetsV1',
|
||||
PartnerAssetsV2 = 'PartnerAssetsV2',
|
||||
PartnerAssetExifsV1 = 'PartnerAssetExifsV1',
|
||||
PartnerStacksV1 = 'PartnerStacksV1',
|
||||
StacksV1 = 'StacksV1',
|
||||
@ -834,6 +837,7 @@ export enum SyncEntityType {
|
||||
UserDeleteV1 = 'UserDeleteV1',
|
||||
|
||||
AssetV1 = 'AssetV1',
|
||||
AssetV2 = 'AssetV2',
|
||||
AssetDeleteV1 = 'AssetDeleteV1',
|
||||
AssetExifV1 = 'AssetExifV1',
|
||||
AssetEditV1 = 'AssetEditV1',
|
||||
@ -845,7 +849,9 @@ export enum SyncEntityType {
|
||||
PartnerDeleteV1 = 'PartnerDeleteV1',
|
||||
|
||||
PartnerAssetV1 = 'PartnerAssetV1',
|
||||
PartnerAssetV2 = 'PartnerAssetV2',
|
||||
PartnerAssetBackfillV1 = 'PartnerAssetBackfillV1',
|
||||
PartnerAssetBackfillV2 = 'PartnerAssetBackfillV2',
|
||||
PartnerAssetDeleteV1 = 'PartnerAssetDeleteV1',
|
||||
PartnerAssetExifV1 = 'PartnerAssetExifV1',
|
||||
PartnerAssetExifBackfillV1 = 'PartnerAssetExifBackfillV1',
|
||||
@ -862,8 +868,11 @@ export enum SyncEntityType {
|
||||
AlbumUserDeleteV1 = 'AlbumUserDeleteV1',
|
||||
|
||||
AlbumAssetCreateV1 = 'AlbumAssetCreateV1',
|
||||
AlbumAssetCreateV2 = 'AlbumAssetCreateV2',
|
||||
AlbumAssetUpdateV1 = 'AlbumAssetUpdateV1',
|
||||
AlbumAssetUpdateV2 = 'AlbumAssetUpdateV2',
|
||||
AlbumAssetBackfillV1 = 'AlbumAssetBackfillV1',
|
||||
AlbumAssetBackfillV2 = 'AlbumAssetBackfillV2',
|
||||
AlbumAssetExifCreateV1 = 'AlbumAssetExifCreateV1',
|
||||
AlbumAssetExifUpdateV1 = 'AlbumAssetExifUpdateV1',
|
||||
AlbumAssetExifBackfillV1 = 'AlbumAssetExifBackfillV1',
|
||||
|
||||
@ -11,7 +11,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { NotificationDto } from 'src/dtos/notification.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV1 } from 'src/dtos/sync.dto';
|
||||
import { SyncAssetEditV1, SyncAssetExifV1, SyncAssetV2 } from 'src/dtos/sync.dto';
|
||||
import { AppRestartEvent, ArgsOf, EventRepository } from 'src/repositories/event.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { handlePromiseError } from 'src/utils/misc';
|
||||
@ -35,9 +35,9 @@ export interface ClientEventMap {
|
||||
on_notification: [NotificationDto];
|
||||
on_session_delete: [string];
|
||||
|
||||
AssetUploadReadyV1: [{ asset: SyncAssetV1; exif: SyncAssetExifV1 }];
|
||||
AssetUploadReadyV2: [{ asset: SyncAssetV2; exif: SyncAssetExifV1 }];
|
||||
AppRestartV1: [AppRestartEvent];
|
||||
AssetEditReadyV1: [{ asset: SyncAssetV1; edit: SyncAssetEditV1[] }];
|
||||
AssetEditReadyV2: [{ asset: SyncAssetV2; edit: SyncAssetEditV1[] }];
|
||||
}
|
||||
|
||||
export type AuthFn = (client: Socket) => Promise<AuthDto>;
|
||||
|
||||
@ -0,0 +1,31 @@
|
||||
import { Kysely, sql } from 'kysely';
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await sql`
|
||||
ALTER TABLE asset
|
||||
ALTER COLUMN duration TYPE integer
|
||||
USING (
|
||||
CASE
|
||||
WHEN duration ~ '^\\d{2}:\\d{2}:\\d{2}\\.\\d{3}$'
|
||||
THEN substr(duration, 1, 2)::int * 3600000
|
||||
+ substr(duration, 4, 2)::int * 60000
|
||||
+ substr(duration, 7, 2)::int * 1000
|
||||
+ substr(duration, 10, 3)::int
|
||||
END
|
||||
);`.execute(db);
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await sql`
|
||||
ALTER TABLE asset
|
||||
ALTER COLUMN duration TYPE varchar
|
||||
USING (
|
||||
CASE
|
||||
WHEN duration IS NULL THEN NULL
|
||||
ELSE lpad((duration / 3600000)::text, 2, '0')
|
||||
|| ':' || lpad(((duration / 60000) % 60)::text, 2, '0')
|
||||
|| ':' || lpad(((duration / 1000) % 60)::text, 2, '0')
|
||||
|| '.' || lpad((duration % 1000)::text, 3, '0')
|
||||
END
|
||||
);`.execute(db);
|
||||
}
|
||||
@ -83,8 +83,8 @@ export class AssetTable {
|
||||
@Column({ type: 'boolean', default: false })
|
||||
isFavorite!: Generated<boolean>;
|
||||
|
||||
@Column({ type: 'character varying', nullable: true })
|
||||
duration!: string | null;
|
||||
@Column({ type: 'integer', nullable: true })
|
||||
duration!: number | null;
|
||||
|
||||
@Column({ type: 'bytea', index: true })
|
||||
checksum!: Buffer; // sha1 checksum
|
||||
|
||||
@ -101,7 +101,7 @@ export class JobService extends BaseService {
|
||||
const edits = await this.assetEditRepository.getWithSyncInfo(item.data.id);
|
||||
|
||||
if (asset) {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetEditReadyV2', asset.ownerId, {
|
||||
asset: {
|
||||
id: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
@ -156,7 +156,7 @@ export class JobService extends BaseService {
|
||||
this.websocketRepository.clientSend('on_upload_success', asset.ownerId, mapAsset(asset));
|
||||
if (asset.exifInfo) {
|
||||
const exif = asset.exifInfo;
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV1', asset.ownerId, {
|
||||
this.websocketRepository.clientSend('AssetUploadReadyV2', asset.ownerId, {
|
||||
// TODO remove `on_upload_success` and then modify the query to select only the required fields)
|
||||
asset: {
|
||||
id: asset.id,
|
||||
|
||||
@ -999,7 +999,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '00:00:06.210',
|
||||
duration: 6210,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -1067,7 +1067,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: asset.id,
|
||||
duration: '168:00:00.000',
|
||||
duration: 604_800_000,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@ -1080,7 +1080,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should prefer Duration from exif over sidecar', async () => {
|
||||
@ -1092,7 +1092,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:02:03.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 123_000 }));
|
||||
});
|
||||
|
||||
it('should ignore all Duration tags for definitely static images', async () => {
|
||||
@ -1121,7 +1121,7 @@ describe(MetadataService.name, () => {
|
||||
await sut.handleMetadataExtraction({ id: asset.id });
|
||||
|
||||
expect(mocks.metadata.readTags).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: '00:07:36.000' }));
|
||||
expect(mocks.asset.update).toHaveBeenCalledWith(expect.objectContaining({ duration: 456_000 }));
|
||||
});
|
||||
|
||||
it('should trim whitespace from description', async () => {
|
||||
|
||||
@ -1001,18 +1001,10 @@ export class MetadataService extends BaseService {
|
||||
return bitsPerSample;
|
||||
}
|
||||
|
||||
private getDuration(tags: ImmichTags): string | null {
|
||||
private getDuration(tags: ImmichTags): number | null {
|
||||
const duration = tags.Duration;
|
||||
|
||||
if (typeof duration === 'string') {
|
||||
return duration;
|
||||
}
|
||||
|
||||
if (typeof duration === 'number') {
|
||||
return Duration.fromObject({ seconds: duration }).toFormat('hh:mm:ss.SSS');
|
||||
}
|
||||
|
||||
return null;
|
||||
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
|
||||
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
|
||||
}
|
||||
|
||||
private async getVideoTags(originalPath: string) {
|
||||
|
||||
@ -10,6 +10,7 @@ import {
|
||||
syncAlbumV2ToV1,
|
||||
syncAssetFaceV2ToV1,
|
||||
SyncAssetV1,
|
||||
SyncAssetV2,
|
||||
SyncItem,
|
||||
SyncStreamDto,
|
||||
} from 'src/dtos/sync.dto';
|
||||
@ -22,7 +23,8 @@ import { hexOrBufferToBase64 } from 'src/utils/bytes';
|
||||
import { fromAck, serialize, SerializeOptions, toAck } from 'src/utils/sync';
|
||||
|
||||
type CheckpointMap = Partial<Record<SyncEntityType, SyncAck>>;
|
||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash'> & {
|
||||
type AssetLike = Omit<SyncAssetV1, 'checksum' | 'thumbhash' | 'duration'> & {
|
||||
duration: number | null;
|
||||
checksum: Buffer<ArrayBufferLike>;
|
||||
thumbhash: Buffer<ArrayBufferLike> | null;
|
||||
};
|
||||
@ -32,6 +34,13 @@ const MAX_DAYS = 30;
|
||||
const MAX_DURATION = Duration.fromObject({ days: MAX_DAYS });
|
||||
|
||||
const mapSyncAssetV1 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV1 => ({
|
||||
...data,
|
||||
duration: Duration.fromMillis(data.duration ?? 0).toFormat('hh:mm:ss.SSS'),
|
||||
checksum: hexOrBufferToBase64(checksum),
|
||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||
});
|
||||
|
||||
const mapSyncAssetV2 = ({ checksum, thumbhash, ...data }: AssetLike): SyncAssetV2 => ({
|
||||
...data,
|
||||
checksum: hexOrBufferToBase64(checksum),
|
||||
thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null,
|
||||
@ -56,10 +65,13 @@ export const SYNC_TYPES_ORDER = [
|
||||
SyncRequestType.UsersV1,
|
||||
SyncRequestType.PartnersV1,
|
||||
SyncRequestType.AssetsV1,
|
||||
SyncRequestType.AssetsV2,
|
||||
SyncRequestType.StacksV1,
|
||||
SyncRequestType.PartnerAssetsV1,
|
||||
SyncRequestType.PartnerAssetsV2,
|
||||
SyncRequestType.PartnerStacksV1,
|
||||
SyncRequestType.AlbumAssetsV1,
|
||||
SyncRequestType.AlbumAssetsV2,
|
||||
SyncRequestType.AlbumsV1,
|
||||
SyncRequestType.AlbumsV2,
|
||||
SyncRequestType.AlbumUsersV1,
|
||||
@ -160,9 +172,11 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.UsersV1]: () => this.syncUsersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnersV1]: () => this.syncPartnersV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV1]: () => this.syncAssetsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetsV2]: () => this.syncAssetsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetExifsV1]: () => this.syncAssetExifsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.AssetEditsV1]: () => this.syncAssetEditsV1(options, response, checkpointMap),
|
||||
[SyncRequestType.PartnerAssetsV1]: () => this.syncPartnerAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.PartnerAssetsV2]: () => this.syncPartnerAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AssetMetadataV1]: () => this.syncAssetMetadataV1(options, response, checkpointMap, auth),
|
||||
[SyncRequestType.PartnerAssetExifsV1]: () =>
|
||||
this.syncPartnerAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
@ -170,6 +184,7 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.AlbumsV2]: () => this.syncAlbumsV2(options, response, checkpointMap),
|
||||
[SyncRequestType.AlbumUsersV1]: () => this.syncAlbumUsersV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV1]: () => this.syncAlbumAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetsV2]: () => this.syncAlbumAssetsV2(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumToAssetsV1]: () => this.syncAlbumToAssetsV1(options, response, checkpointMap, session.id),
|
||||
[SyncRequestType.AlbumAssetExifsV1]: () =>
|
||||
this.syncAlbumAssetExifsV1(options, response, checkpointMap, session.id),
|
||||
@ -274,6 +289,20 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetsV2(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const deleteType = SyncEntityType.AssetDeleteV1;
|
||||
const deletes = this.syncRepository.asset.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const upsertType = SyncEntityType.AssetV2;
|
||||
const upserts = this.syncRepository.asset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV1(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
@ -333,6 +362,65 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncPartnerAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
sessionId: string,
|
||||
) {
|
||||
const deleteType = SyncEntityType.PartnerAssetDeleteV1;
|
||||
const deletes = this.syncRepository.partnerAsset.getDeletes({ ...options, ack: checkpointMap[deleteType] });
|
||||
for await (const { id, ...data } of deletes) {
|
||||
send(response, { type: deleteType, ids: [id], data });
|
||||
}
|
||||
|
||||
const backfillType = SyncEntityType.PartnerAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const partners = await this.syncRepository.partner.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const upsertType = SyncEntityType.PartnerAssetV2;
|
||||
const upsertCheckpoint = checkpointMap[upsertType];
|
||||
if (upsertCheckpoint) {
|
||||
const endId = upsertCheckpoint.updateId;
|
||||
|
||||
for (const partner of partners) {
|
||||
const createId = partner.createId;
|
||||
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.partnerAsset.getBackfill(
|
||||
{ ...options, afterUpdateId: startId, beforeUpdateId: endId },
|
||||
partner.sharedById,
|
||||
);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, {
|
||||
type: backfillType,
|
||||
ids: [createId, updateId],
|
||||
data: mapSyncAssetV2(data),
|
||||
});
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
}
|
||||
} else if (partners.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: partners.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
const upserts = this.syncRepository.partnerAsset.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
for await (const { updateId, ...data } of upserts) {
|
||||
send(response, { type: upsertType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAssetExifsV1(options: SyncQueryOptions, response: Writable, checkpointMap: CheckpointMap) {
|
||||
const upsertType = SyncEntityType.AssetExifV1;
|
||||
const upserts = this.syncRepository.assetExif.getUpserts({ ...options, ack: checkpointMap[upsertType] });
|
||||
@ -561,6 +649,77 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetsV2(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
checkpointMap: CheckpointMap,
|
||||
sessionId: string,
|
||||
) {
|
||||
const backfillType = SyncEntityType.AlbumAssetBackfillV2;
|
||||
const backfillCheckpoint = checkpointMap[backfillType];
|
||||
const albums = await this.syncRepository.album.getCreatedAfter({
|
||||
...options,
|
||||
afterCreateId: backfillCheckpoint?.updateId,
|
||||
});
|
||||
const updateType = SyncEntityType.AlbumAssetUpdateV2;
|
||||
const createType = SyncEntityType.AlbumAssetCreateV2;
|
||||
const updateCheckpoint = checkpointMap[updateType];
|
||||
const createCheckpoint = checkpointMap[createType];
|
||||
if (createCheckpoint) {
|
||||
const endId = createCheckpoint.updateId;
|
||||
|
||||
for (const album of albums) {
|
||||
const createId = album.createId;
|
||||
if (isEntityBackfillComplete(createId, backfillCheckpoint)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const startId = getStartId(createId, backfillCheckpoint);
|
||||
const backfill = this.syncRepository.albumAsset.getBackfill(
|
||||
{ ...options, afterUpdateId: startId, beforeUpdateId: endId },
|
||||
album.id,
|
||||
);
|
||||
|
||||
for await (const { updateId, ...data } of backfill) {
|
||||
send(response, { type: backfillType, ids: [createId, updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
|
||||
sendEntityBackfillCompleteAck(response, backfillType, createId);
|
||||
}
|
||||
} else if (albums.length > 0) {
|
||||
await this.upsertBackfillCheckpoint({
|
||||
type: backfillType,
|
||||
sessionId,
|
||||
createId: albums.at(-1)!.createId,
|
||||
});
|
||||
}
|
||||
|
||||
if (createCheckpoint) {
|
||||
const updates = this.syncRepository.albumAsset.getUpdates(
|
||||
{ ...options, ack: updateCheckpoint },
|
||||
createCheckpoint,
|
||||
);
|
||||
for await (const { updateId, ...data } of updates) {
|
||||
send(response, { type: updateType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
const creates = this.syncRepository.albumAsset.getCreates({ ...options, ack: createCheckpoint });
|
||||
let first = true;
|
||||
for await (const { updateId, ...data } of creates) {
|
||||
if (first) {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV1,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
}
|
||||
|
||||
private async syncAlbumAssetExifsV1(
|
||||
options: SyncQueryOptions,
|
||||
response: Writable,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user