diff --git a/mobile/openapi/lib/api/upload_api.dart b/mobile/openapi/lib/api/upload_api.dart index 0c9b698025..858efb6e29 100644 --- a/mobile/openapi/lib/api/upload_api.dart +++ b/mobile/openapi/lib/api/upload_api.dart @@ -293,7 +293,7 @@ class UploadApi { /// Indicates the version of the RUFH protocol supported by the client. /// /// * [String] xImmichAssetData (required): - /// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - icloud-id (string, optional): iCloud identifier for assets from iOS devices + /// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - live-photo-video-id (string, optional): Live photo ID for assets from iOS devices - icloud-id (string, optional): iCloud identifier for assets from iOS devices /// /// * [String] key: /// @@ -353,7 +353,7 @@ class UploadApi { /// Indicates the version of the RUFH protocol supported by the client. /// /// * [String] xImmichAssetData (required): - /// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - icloud-id (string, optional): iCloud identifier for assets from iOS devices + /// RFC 9651 structured dictionary containing asset metadata with the following keys: - device-asset-id (string, required): Unique device asset identifier - device-id (string, required): Device identifier - file-created-at (string/date, required): ISO 8601 date string or Unix timestamp - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status - live-photo-video-id (string, optional): Live photo ID for assets from iOS devices - icloud-id (string, optional): iCloud identifier for assets from iOS devices /// /// * [String] key: /// diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4847f8dd16..43ea99c7fe 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9296,7 +9296,7 @@ { "name": "x-immich-asset-data", "in": "header", - "description": "RFC 9651 structured dictionary containing asset metadata with the following keys:\n- device-asset-id (string, required): Unique device asset identifier\n- device-id (string, required): Device identifier\n- file-created-at (string/date, required): ISO 8601 date string or Unix timestamp\n- file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp\n- filename (string, required): Original filename\n- is-favorite (boolean, optional): Favorite status\n- icloud-id (string, optional): iCloud identifier for assets from iOS devices", + "description": "RFC 9651 structured dictionary containing asset metadata with the following keys:\n- device-asset-id (string, required): Unique device asset identifier\n- device-id (string, required): Device identifier\n- file-created-at (string/date, required): ISO 8601 date string or Unix timestamp\n- file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp\n- filename (string, required): Original filename\n- is-favorite (boolean, optional): Favorite status\n- live-photo-video-id (string, optional): Live photo ID for assets from iOS devices\n- icloud-id (string, optional): iCloud identifier for assets from iOS devices", "required": true, "schema": { "type": "string" diff --git a/server/src/controllers/asset-upload.controller.ts b/server/src/controllers/asset-upload.controller.ts index e306f77e66..58cc604fa2 100644 --- a/server/src/controllers/asset-upload.controller.ts +++ b/server/src/controllers/asset-upload.controller.ts @@ -14,7 +14,13 @@ import { } from '@nestjs/common'; import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { Request, Response } from 'express'; -import { GetUploadStatusDto, ResumeUploadDto, StartUploadDto, UploadHeader, UploadOkDto } from 'src/dtos/asset-upload'; +import { + GetUploadStatusDto, + ResumeUploadDto, + StartUploadDto, + UploadHeader, + UploadOkDto, +} from 'src/dtos/asset-upload.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ImmichHeader, Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; @@ -60,6 +66,7 @@ export class AssetUploadController { - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp - filename (string, required): Original filename - is-favorite (boolean, optional): Favorite status +- live-photo-video-id (string, optional): Live photo ID for assets from iOS devices - icloud-id (string, optional): iCloud identifier for assets from iOS devices`, required: true, example: diff --git a/server/src/dtos/asset-upload.ts b/server/src/dtos/asset-upload.dto.ts similarity index 95% rename from server/src/dtos/asset-upload.ts rename to server/src/dtos/asset-upload.dto.ts index ae39c669d0..c49f62d061 100644 --- a/server/src/dtos/asset-upload.ts +++ b/server/src/dtos/asset-upload.dto.ts @@ -28,6 +28,11 @@ export class UploadAssetDataDto { @ValidateBoolean({ optional: true }) isFavorite?: boolean; + @Optional() + @IsString() + @IsNotEmpty() + livePhotoVideoId?: string; + @Optional() @IsString() @IsNotEmpty() @@ -101,6 +106,7 @@ export class StartUploadDto extends BaseUploadHeadersDto { fileCreatedAt: dict.get('file-created-at')?.[0], fileModifiedAt: dict.get('file-modified-at')?.[0], isFavorite: dict.get('is-favorite')?.[0], + livePhotoVideoId: dict.get('live-photo-video-id')?.[0], iCloudId: dict.get('icloud-id')?.[0], }); } catch { @@ -126,7 +132,7 @@ export class StartUploadDto extends BaseUploadHeadersDto { @Expose({ name: UploadHeader.UploadLength }) @Min(0) @IsInt() - @Type(() => Number) + @Transform(({ obj, value }) => Number(value === undefined ? obj['x-upload-length'] : value)) uploadLength!: number; @Expose({ name: UploadHeader.UploadOffset }) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index b040f5909f..4173bd376e 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -64,10 +64,16 @@ where -- AssetRepository.setComplete update "asset" set - "status" = $1, - "visibility" = $2 + "status" = 'active', + "visibility" = ( + case + when type = 'VIDEO' + and "livePhotoVideoId" is not null then 'hidden' + else 'timeline' + end + )::asset_visibility_enum where - "id" = $3 + "id" = $1 and "status" = 'partial' -- AssetRepository.removeAndDecrementQuota diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 9484254eee..feed6cff62 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -292,7 +292,10 @@ export class AssetRepository { async setComplete(assetId: string) { await this.db .updateTable('asset') - .set({ status: AssetStatus.Active, visibility: AssetVisibility.Timeline }) + .set({ + status: sql.lit(AssetStatus.Active), + visibility: sql`(case when type = 'VIDEO' and "livePhotoVideoId" is not null then 'hidden' else 'timeline' end)::asset_visibility_enum`, + }) .where('id', '=', assetId) .where('status', '=', sql.lit(AssetStatus.Partial)) .execute(); diff --git a/server/src/services/asset-upload.service.spec.ts b/server/src/services/asset-upload.service.spec.ts index 180e0e10c3..423a321eb5 100644 --- a/server/src/services/asset-upload.service.spec.ts +++ b/server/src/services/asset-upload.service.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; -import { StructuredBoolean } from 'src/dtos/asset-upload'; +import { StructuredBoolean } from 'src/dtos/asset-upload.dto'; import { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum'; import { AssetUploadService } from 'src/services/asset-upload.service'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; diff --git a/server/src/services/asset-upload.service.ts b/server/src/services/asset-upload.service.ts index b9c4c2f3ed..26e5f92012 100644 --- a/server/src/services/asset-upload.service.ts +++ b/server/src/services/asset-upload.service.ts @@ -7,7 +7,7 @@ import { Readable } from 'node:stream'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { StorageCore } from 'src/cores/storage.core'; import { OnEvent, OnJob } from 'src/decorators'; -import { GetUploadStatusDto, ResumeUploadDto, StartUploadDto } from 'src/dtos/asset-upload'; +import { GetUploadStatusDto, ResumeUploadDto, StartUploadDto } from 'src/dtos/asset-upload.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetMetadataKey, @@ -269,6 +269,7 @@ export class AssetUploadService extends BaseService { localDateTime: assetData.fileCreatedAt, type, isFavorite: assetData.isFavorite, + livePhotoVideoId: assetData.livePhotoVideoId, visibility: AssetVisibility.Hidden, originalFileName: assetData.filename, status: AssetStatus.Partial,