handle live photos

This commit is contained in:
mertalev 2025-10-09 16:12:47 -04:00
parent a3d10ff46a
commit 883eb15ecb
No known key found for this signature in database
GPG key ID: DF6ABC77AAD98C95
8 changed files with 34 additions and 11 deletions

View file

@ -293,7 +293,7 @@ class UploadApi {
/// Indicates the version of the RUFH protocol supported by the client. /// Indicates the version of the RUFH protocol supported by the client.
/// ///
/// * [String] xImmichAssetData (required): /// * [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: /// * [String] key:
/// ///
@ -353,7 +353,7 @@ class UploadApi {
/// Indicates the version of the RUFH protocol supported by the client. /// Indicates the version of the RUFH protocol supported by the client.
/// ///
/// * [String] xImmichAssetData (required): /// * [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: /// * [String] key:
/// ///

View file

@ -9296,7 +9296,7 @@
{ {
"name": "x-immich-asset-data", "name": "x-immich-asset-data",
"in": "header", "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, "required": true,
"schema": { "schema": {
"type": "string" "type": "string"

View file

@ -14,7 +14,13 @@ import {
} from '@nestjs/common'; } from '@nestjs/common';
import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger'; import { ApiHeader, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express'; 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 { AuthDto } from 'src/dtos/auth.dto';
import { ImmichHeader, Permission } from 'src/enum'; import { ImmichHeader, Permission } from 'src/enum';
import { Auth, Authenticated } from 'src/middleware/auth.guard'; 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 - file-modified-at (string/date, required): ISO 8601 date string or Unix timestamp
- filename (string, required): Original filename - filename (string, required): Original filename
- is-favorite (boolean, optional): Favorite status - 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`, - icloud-id (string, optional): iCloud identifier for assets from iOS devices`,
required: true, required: true,
example: example:

View file

@ -28,6 +28,11 @@ export class UploadAssetDataDto {
@ValidateBoolean({ optional: true }) @ValidateBoolean({ optional: true })
isFavorite?: boolean; isFavorite?: boolean;
@Optional()
@IsString()
@IsNotEmpty()
livePhotoVideoId?: string;
@Optional() @Optional()
@IsString() @IsString()
@IsNotEmpty() @IsNotEmpty()
@ -101,6 +106,7 @@ export class StartUploadDto extends BaseUploadHeadersDto {
fileCreatedAt: dict.get('file-created-at')?.[0], fileCreatedAt: dict.get('file-created-at')?.[0],
fileModifiedAt: dict.get('file-modified-at')?.[0], fileModifiedAt: dict.get('file-modified-at')?.[0],
isFavorite: dict.get('is-favorite')?.[0], isFavorite: dict.get('is-favorite')?.[0],
livePhotoVideoId: dict.get('live-photo-video-id')?.[0],
iCloudId: dict.get('icloud-id')?.[0], iCloudId: dict.get('icloud-id')?.[0],
}); });
} catch { } catch {
@ -126,7 +132,7 @@ export class StartUploadDto extends BaseUploadHeadersDto {
@Expose({ name: UploadHeader.UploadLength }) @Expose({ name: UploadHeader.UploadLength })
@Min(0) @Min(0)
@IsInt() @IsInt()
@Type(() => Number) @Transform(({ obj, value }) => Number(value === undefined ? obj['x-upload-length'] : value))
uploadLength!: number; uploadLength!: number;
@Expose({ name: UploadHeader.UploadOffset }) @Expose({ name: UploadHeader.UploadOffset })

View file

@ -64,10 +64,16 @@ where
-- AssetRepository.setComplete -- AssetRepository.setComplete
update "asset" update "asset"
set set
"status" = $1, "status" = 'active',
"visibility" = $2 "visibility" = (
case
when type = 'VIDEO'
and "livePhotoVideoId" is not null then 'hidden'
else 'timeline'
end
)::asset_visibility_enum
where where
"id" = $3 "id" = $1
and "status" = 'partial' and "status" = 'partial'
-- AssetRepository.removeAndDecrementQuota -- AssetRepository.removeAndDecrementQuota

View file

@ -292,7 +292,10 @@ export class AssetRepository {
async setComplete(assetId: string) { async setComplete(assetId: string) {
await this.db await this.db
.updateTable('asset') .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('id', '=', assetId)
.where('status', '=', sql.lit(AssetStatus.Partial)) .where('status', '=', sql.lit(AssetStatus.Partial))
.execute(); .execute();

View file

@ -1,5 +1,5 @@
import { BadRequestException, InternalServerErrorException } from '@nestjs/common'; 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 { AssetMetadataKey, AssetStatus, AssetType, AssetVisibility, JobName, JobStatus } from 'src/enum';
import { AssetUploadService } from 'src/services/asset-upload.service'; import { AssetUploadService } from 'src/services/asset-upload.service';
import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database'; import { ASSET_CHECKSUM_CONSTRAINT } from 'src/utils/database';

View file

@ -7,7 +7,7 @@ import { Readable } from 'node:stream';
import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants'; import { JOBS_ASSET_PAGINATION_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core'; import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators'; 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 { AuthDto } from 'src/dtos/auth.dto';
import { import {
AssetMetadataKey, AssetMetadataKey,
@ -269,6 +269,7 @@ export class AssetUploadService extends BaseService {
localDateTime: assetData.fileCreatedAt, localDateTime: assetData.fileCreatedAt,
type, type,
isFavorite: assetData.isFavorite, isFavorite: assetData.isFavorite,
livePhotoVideoId: assetData.livePhotoVideoId,
visibility: AssetVisibility.Hidden, visibility: AssetVisibility.Hidden,
originalFileName: assetData.filename, originalFileName: assetData.filename,
status: AssetStatus.Partial, status: AssetStatus.Partial,