mirror of
https://github.com/immich-app/immich.git
synced 2025-06-08 21:38:40 +02:00
* feat: private view * pr feedback * sql generation * feat: visibility column * fix: set visibility value as the same as the still part after unlinked live photos * fix: test * pr feedback
200 lines
6.1 KiB
TypeScript
200 lines
6.1 KiB
TypeScript
import { ApiProperty } from '@nestjs/swagger';
|
|
import { Selectable } from 'kysely';
|
|
import { AssetFace, AssetFile, Exif, Stack, Tag, User } from 'src/database';
|
|
import { PropertyLifecycle } from 'src/decorators';
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
|
|
import {
|
|
AssetFaceWithoutPersonResponseDto,
|
|
PersonWithFacesResponseDto,
|
|
mapFacesWithoutPerson,
|
|
mapPerson,
|
|
} from 'src/dtos/person.dto';
|
|
import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
|
|
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
|
import { AssetStatus, AssetType, AssetVisibility } from 'src/enum';
|
|
import { mimeTypes } from 'src/utils/mime-types';
|
|
|
|
export class SanitizedAssetResponseDto {
|
|
id!: string;
|
|
@ApiProperty({ enumName: 'AssetTypeEnum', enum: AssetType })
|
|
type!: AssetType;
|
|
thumbhash!: string | null;
|
|
originalMimeType?: string;
|
|
localDateTime!: Date;
|
|
duration!: string;
|
|
livePhotoVideoId?: string | null;
|
|
hasMetadata!: boolean;
|
|
}
|
|
|
|
export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|
deviceAssetId!: string;
|
|
deviceId!: string;
|
|
ownerId!: string;
|
|
owner?: UserResponseDto;
|
|
@PropertyLifecycle({ deprecatedAt: 'v1.106.0' })
|
|
libraryId?: string | null;
|
|
originalPath!: string;
|
|
originalFileName!: string;
|
|
fileCreatedAt!: Date;
|
|
fileModifiedAt!: Date;
|
|
updatedAt!: Date;
|
|
isFavorite!: boolean;
|
|
isArchived!: boolean;
|
|
isTrashed!: boolean;
|
|
isOffline!: boolean;
|
|
exifInfo?: ExifResponseDto;
|
|
tags?: TagResponseDto[];
|
|
people?: PersonWithFacesResponseDto[];
|
|
unassignedFaces?: AssetFaceWithoutPersonResponseDto[];
|
|
/**base64 encoded sha1 hash */
|
|
checksum!: string;
|
|
stack?: AssetStackResponseDto | null;
|
|
duplicateId?: string | null;
|
|
|
|
@PropertyLifecycle({ deprecatedAt: 'v1.113.0' })
|
|
resized?: boolean;
|
|
}
|
|
|
|
export type MapAsset = {
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
deletedAt: Date | null;
|
|
id: string;
|
|
updateId: string;
|
|
status: AssetStatus;
|
|
checksum: Buffer<ArrayBufferLike>;
|
|
deviceAssetId: string;
|
|
deviceId: string;
|
|
duplicateId: string | null;
|
|
duration: string | null;
|
|
encodedVideoPath: string | null;
|
|
exifInfo?: Selectable<Exif> | null;
|
|
faces?: AssetFace[];
|
|
fileCreatedAt: Date;
|
|
fileModifiedAt: Date;
|
|
files?: AssetFile[];
|
|
isExternal: boolean;
|
|
isFavorite: boolean;
|
|
isOffline: boolean;
|
|
visibility: AssetVisibility;
|
|
libraryId: string | null;
|
|
livePhotoVideoId: string | null;
|
|
localDateTime: Date;
|
|
originalFileName: string;
|
|
originalPath: string;
|
|
owner?: User | null;
|
|
ownerId: string;
|
|
sidecarPath: string | null;
|
|
stack?: Stack | null;
|
|
stackId: string | null;
|
|
tags?: Tag[];
|
|
thumbhash: Buffer<ArrayBufferLike> | null;
|
|
type: AssetType;
|
|
};
|
|
|
|
export class AssetStackResponseDto {
|
|
id!: string;
|
|
|
|
primaryAssetId!: string;
|
|
|
|
@ApiProperty({ type: 'integer' })
|
|
assetCount!: number;
|
|
}
|
|
|
|
export type AssetMapOptions = {
|
|
stripMetadata?: boolean;
|
|
withStack?: boolean;
|
|
auth?: AuthDto;
|
|
};
|
|
|
|
// TODO: this is inefficient
|
|
const peopleWithFaces = (faces?: AssetFace[]): PersonWithFacesResponseDto[] => {
|
|
const result: PersonWithFacesResponseDto[] = [];
|
|
if (faces) {
|
|
for (const face of faces) {
|
|
if (face.person) {
|
|
const existingPersonEntry = result.find((item) => item.id === face.person!.id);
|
|
if (existingPersonEntry) {
|
|
existingPersonEntry.faces.push(face);
|
|
} else {
|
|
result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] });
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return result;
|
|
};
|
|
|
|
const mapStack = (entity: { stack?: Stack | null }) => {
|
|
if (!entity.stack) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
id: entity.stack.id,
|
|
primaryAssetId: entity.stack.primaryAssetId,
|
|
assetCount: entity.stack.assetCount ?? entity.stack.assets.length + 1,
|
|
};
|
|
};
|
|
|
|
// if an asset is jsonified in the DB before being returned, its buffer fields will be hex-encoded strings
|
|
export const hexOrBufferToBase64 = (encoded: string | Buffer) => {
|
|
if (typeof encoded === 'string') {
|
|
return Buffer.from(encoded.slice(2), 'hex').toString('base64');
|
|
}
|
|
|
|
return encoded.toString('base64');
|
|
};
|
|
|
|
export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): AssetResponseDto {
|
|
const { stripMetadata = false, withStack = false } = options;
|
|
|
|
if (stripMetadata) {
|
|
const sanitizedAssetResponse: SanitizedAssetResponseDto = {
|
|
id: entity.id,
|
|
type: entity.type,
|
|
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
|
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
|
localDateTime: entity.localDateTime,
|
|
duration: entity.duration ?? '0:00:00.00000',
|
|
livePhotoVideoId: entity.livePhotoVideoId,
|
|
hasMetadata: false,
|
|
};
|
|
return sanitizedAssetResponse as AssetResponseDto;
|
|
}
|
|
|
|
return {
|
|
id: entity.id,
|
|
deviceAssetId: entity.deviceAssetId,
|
|
ownerId: entity.ownerId,
|
|
owner: entity.owner ? mapUser(entity.owner) : undefined,
|
|
deviceId: entity.deviceId,
|
|
libraryId: entity.libraryId,
|
|
type: entity.type,
|
|
originalPath: entity.originalPath,
|
|
originalFileName: entity.originalFileName,
|
|
originalMimeType: mimeTypes.lookup(entity.originalFileName),
|
|
thumbhash: entity.thumbhash ? hexOrBufferToBase64(entity.thumbhash) : null,
|
|
fileCreatedAt: entity.fileCreatedAt,
|
|
fileModifiedAt: entity.fileModifiedAt,
|
|
localDateTime: entity.localDateTime,
|
|
updatedAt: entity.updatedAt,
|
|
isFavorite: options.auth?.user.id === entity.ownerId ? entity.isFavorite : false,
|
|
isArchived: entity.visibility === AssetVisibility.ARCHIVE,
|
|
isTrashed: !!entity.deletedAt,
|
|
duration: entity.duration ?? '0:00:00.00000',
|
|
exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined,
|
|
livePhotoVideoId: entity.livePhotoVideoId,
|
|
tags: entity.tags?.map((tag) => mapTag(tag)),
|
|
people: peopleWithFaces(entity.faces),
|
|
unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)),
|
|
checksum: hexOrBufferToBase64(entity.checksum),
|
|
stack: withStack ? mapStack(entity) : undefined,
|
|
isOffline: entity.isOffline,
|
|
hasMetadata: true,
|
|
duplicateId: entity.duplicateId,
|
|
resized: true,
|
|
};
|
|
}
|