wip
parent
93ec8b7ecf
commit
42854cad56
|
|
@ -290,7 +290,7 @@ export class StorageCore {
|
|||
private savePath(pathType: PathType, id: string, newPath: string) {
|
||||
switch (pathType) {
|
||||
case AssetPathType.Original: {
|
||||
return this.assetRepository.update({ id, originalPath: newPath });
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.Original, path: newPath });
|
||||
}
|
||||
case AssetPathType.FullSize: {
|
||||
return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.FullSize, path: newPath });
|
||||
|
|
|
|||
|
|
@ -120,7 +120,6 @@ export type Asset = {
|
|||
livePhotoVideoId: string | null;
|
||||
localDateTime: Date;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
type: AssetType;
|
||||
};
|
||||
|
|
@ -344,7 +343,6 @@ export const columns = {
|
|||
'asset.livePhotoVideoId',
|
||||
'asset.localDateTime',
|
||||
'asset.originalFileName',
|
||||
'asset.originalPath',
|
||||
'asset.ownerId',
|
||||
'asset.type',
|
||||
],
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@ export type MapAsset = {
|
|||
livePhotoVideoId: string | null;
|
||||
localDateTime: Date;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
owner?: User | null;
|
||||
ownerId: string;
|
||||
stack?: Stack | null;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ export enum AssetType {
|
|||
}
|
||||
|
||||
export enum AssetFileType {
|
||||
Original = 'original',
|
||||
/**
|
||||
* An full/large-size image extracted/converted from RAW photos
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ import {
|
|||
withFacesAndPeople,
|
||||
withFilePath,
|
||||
withFiles,
|
||||
withOriginals,
|
||||
withSidecars,
|
||||
} from 'src/utils/database';
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -40,8 +42,9 @@ export class AssetJobRepository {
|
|||
return this.db
|
||||
.selectFrom('asset')
|
||||
.where('asset.id', '=', asUuid(id))
|
||||
.select(['id', 'originalPath'])
|
||||
.select((eb) => withFiles(eb, AssetFileType.Sidecar))
|
||||
.select('id')
|
||||
.select(withSidecars)
|
||||
.select(withOriginals)
|
||||
.select((eb) =>
|
||||
jsonArrayFrom(
|
||||
eb
|
||||
|
|
@ -60,8 +63,9 @@ export class AssetJobRepository {
|
|||
return this.db
|
||||
.selectFrom('asset')
|
||||
.where('asset.id', '=', asUuid(id))
|
||||
.select(['id', 'originalPath'])
|
||||
.select((eb) => withFiles(eb, AssetFileType.Sidecar))
|
||||
.select('id')
|
||||
.select(withSidecars)
|
||||
.select(withOriginals)
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
|
@ -107,7 +111,6 @@ export class AssetJobRepository {
|
|||
'asset.id',
|
||||
'asset.visibility',
|
||||
'asset.originalFileName',
|
||||
'asset.originalPath',
|
||||
'asset.ownerId',
|
||||
'asset.thumbhash',
|
||||
'asset.type',
|
||||
|
|
@ -124,7 +127,8 @@ export class AssetJobRepository {
|
|||
.selectFrom('asset')
|
||||
.select(columns.asset)
|
||||
.select(withFaces)
|
||||
.select((eb) => withFiles(eb, AssetFileType.Sidecar))
|
||||
.select(withOriginals)
|
||||
.select(withSidecars)
|
||||
.where('asset.id', '=', id)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
|
@ -209,14 +213,8 @@ export class AssetJobRepository {
|
|||
getForSyncAssets(ids: string[]) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select([
|
||||
'asset.id',
|
||||
'asset.isOffline',
|
||||
'asset.libraryId',
|
||||
'asset.originalPath',
|
||||
'asset.status',
|
||||
'asset.fileModifiedAt',
|
||||
])
|
||||
.select(['asset.id', 'asset.isOffline', 'asset.libraryId', 'asset.status', 'asset.fileModifiedAt'])
|
||||
.select(withOriginals)
|
||||
.where('asset.id', '=', anyUuid(ids))
|
||||
.execute();
|
||||
}
|
||||
|
|
@ -232,7 +230,6 @@ export class AssetJobRepository {
|
|||
'asset.ownerId',
|
||||
'asset.livePhotoVideoId',
|
||||
'asset.encodedVideoPath',
|
||||
'asset.originalPath',
|
||||
])
|
||||
.$call(withExif)
|
||||
.select(withFacesAndPeople)
|
||||
|
|
@ -275,7 +272,8 @@ export class AssetJobRepository {
|
|||
getForVideoConversion(id: string) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.select(['asset.id', 'asset.ownerId', 'asset.originalPath', 'asset.encodedVideoPath'])
|
||||
.select(['asset.id', 'asset.ownerId', 'asset.encodedVideoPath'])
|
||||
.select(withOriginals)
|
||||
.where('asset.id', '=', id)
|
||||
.where('asset.type', '=', AssetType.Video)
|
||||
.executeTakeFirst();
|
||||
|
|
@ -306,7 +304,6 @@ export class AssetJobRepository {
|
|||
'asset.ownerId',
|
||||
'asset.type',
|
||||
'asset.checksum',
|
||||
'asset.originalPath',
|
||||
'asset.isExternal',
|
||||
'asset.originalFileName',
|
||||
'asset.livePhotoVideoId',
|
||||
|
|
@ -320,8 +317,9 @@ export class AssetJobRepository {
|
|||
.whereRef('asset_file.assetId', '=', 'asset.id')
|
||||
.where('asset_file.type', '=', AssetFileType.Sidecar)
|
||||
.limit(1)
|
||||
.as('sidecarPath'),
|
||||
.as('sidecarPath'), // TODO: change to withSidecars
|
||||
])
|
||||
.select(withOriginals)
|
||||
.where('asset.deletedAt', 'is', null);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ import {
|
|||
withFacesAndPeople,
|
||||
withFiles,
|
||||
withLibrary,
|
||||
withOriginals,
|
||||
withOwner,
|
||||
withSidecars,
|
||||
withSmartSearch,
|
||||
withTagId,
|
||||
withTags,
|
||||
|
|
@ -112,6 +114,8 @@ interface GetByIdsRelations {
|
|||
smartSearch?: boolean;
|
||||
stack?: { assets?: boolean };
|
||||
tags?: boolean;
|
||||
originals?: boolean;
|
||||
sidecars?: boolean;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
|
|
@ -355,8 +359,10 @@ export class AssetRepository {
|
|||
return this.db
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
.where('libraryId', '=', asUuid(libraryId))
|
||||
.where('originalPath', '=', originalPath)
|
||||
.innerJoin('asset_file', 'asset.id', 'asset_file.assetId')
|
||||
.where('asset.libraryId', '=', asUuid(libraryId))
|
||||
.where('asset_file.path', '=', originalPath)
|
||||
.where('asset_file.type', '=', AssetFileType.Original)
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
}
|
||||
|
|
@ -398,7 +404,10 @@ export class AssetRepository {
|
|||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID] })
|
||||
getById(id: string, { exifInfo, faces, files, library, owner, smartSearch, stack, tags }: GetByIdsRelations = {}) {
|
||||
getById(
|
||||
id: string,
|
||||
{ exifInfo, faces, files, library, owner, smartSearch, stack, tags, sidecars, originals }: GetByIdsRelations = {},
|
||||
) {
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.selectAll('asset')
|
||||
|
|
@ -434,6 +443,8 @@ export class AssetRepository {
|
|||
),
|
||||
)
|
||||
.$if(!!files, (qb) => qb.select(withFiles))
|
||||
.$if(!!sidecars, (qb) => qb.select(withSidecars))
|
||||
.$if(!!originals, (qb) => qb.select(withOriginals))
|
||||
.$if(!!tags, (qb) => qb.select(withTags))
|
||||
.limit(1)
|
||||
.executeTakeFirst();
|
||||
|
|
@ -877,14 +888,22 @@ export class AssetRepository {
|
|||
isOffline: true,
|
||||
deletedAt: new Date(),
|
||||
})
|
||||
.where('isOffline', '=', false)
|
||||
.where('isExternal', '=', true)
|
||||
.where('libraryId', '=', asUuid(libraryId))
|
||||
.where('asset.isOffline', '=', false)
|
||||
.where('asset.isExternal', '=', true)
|
||||
.where('asset.libraryId', '=', asUuid(libraryId))
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.not(eb.or(paths.map((path) => eb('originalPath', 'like', path)))),
|
||||
eb.or(exclusions.map((path) => eb('originalPath', 'like', path))),
|
||||
]),
|
||||
eb.exists(
|
||||
eb
|
||||
.selectFrom('asset_file')
|
||||
.whereRef('asset_file.assetId', '=', 'asset.id')
|
||||
.where('asset_file.type', '=', AssetFileType.Original)
|
||||
.where((eb) =>
|
||||
eb.or([
|
||||
eb.not(eb.or(paths.map((path) => eb('asset_file.path', 'like', path)))),
|
||||
eb.or(exclusions.map((path) => eb('asset_file.path', 'like', path))),
|
||||
]),
|
||||
),
|
||||
),
|
||||
)
|
||||
.executeTakeFirstOrThrow();
|
||||
}
|
||||
|
|
@ -899,10 +918,12 @@ export class AssetRepository {
|
|||
eb.exists(
|
||||
this.db
|
||||
.selectFrom('asset')
|
||||
.select('originalPath')
|
||||
.whereRef('asset.originalPath', '=', eb.ref('path'))
|
||||
.where('libraryId', '=', asUuid(libraryId))
|
||||
.where('isExternal', '=', true),
|
||||
.innerJoin('asset_file', 'asset.id', 'asset_file.assetId')
|
||||
.select('asset_file.path')
|
||||
.whereRef('asset_file.path', '=', eb.ref('path'))
|
||||
.where('asset_file.type', '=', AssetFileType.Original)
|
||||
.where('asset.libraryId', '=', asUuid(libraryId))
|
||||
.where('asset.isExternal', '=', true),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { Kysely } from 'kysely';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||
import { AssetVisibility } from 'src/enum';
|
||||
import { AssetFileType, AssetVisibility } from 'src/enum';
|
||||
import { DB } from 'src/schema';
|
||||
import { asUuid, withExif } from 'src/utils/database';
|
||||
|
||||
|
|
@ -12,14 +12,18 @@ export class ViewRepository {
|
|||
async getUniqueOriginalPaths(userId: string) {
|
||||
const results = await this.db
|
||||
.selectFrom('asset')
|
||||
.select((eb) => eb.fn<string>('substring', ['asset.originalPath', eb.val('^(.*/)[^/]*$')]).as('directoryPath'))
|
||||
.innerJoin('asset_file', 'asset.id', 'asset_file.assetId')
|
||||
.select((eb) =>
|
||||
eb.fn<string>('substring', [eb.ref('asset_file.path'), eb.val('^(.*/)[^/]*$')]).as('directoryPath'),
|
||||
)
|
||||
.distinct()
|
||||
.where('ownerId', '=', asUuid(userId))
|
||||
.where('visibility', '=', AssetVisibility.Timeline)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('fileCreatedAt', 'is not', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('localDateTime', 'is not', null)
|
||||
.where('asset.ownerId', '=', asUuid(userId))
|
||||
.where('asset.visibility', '=', AssetVisibility.Timeline)
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.where('asset.fileCreatedAt', 'is not', null)
|
||||
.where('asset.fileModifiedAt', 'is not', null)
|
||||
.where('asset.localDateTime', 'is not', null)
|
||||
.where((eb) => eb(eb.ref('asset_file.type'), '=', AssetFileType.Original))
|
||||
.orderBy('directoryPath', 'asc')
|
||||
.execute();
|
||||
|
||||
|
|
@ -32,20 +36,22 @@ export class ViewRepository {
|
|||
|
||||
return this.db
|
||||
.selectFrom('asset')
|
||||
.innerJoin('asset_file', 'asset.id', 'asset_file.assetId')
|
||||
.selectAll('asset')
|
||||
.$call(withExif)
|
||||
.where('ownerId', '=', asUuid(userId))
|
||||
.where('visibility', '=', AssetVisibility.Timeline)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('fileCreatedAt', 'is not', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('localDateTime', 'is not', null)
|
||||
.where('originalPath', 'like', `%${normalizedPath}/%`)
|
||||
.where('originalPath', 'not like', `%${normalizedPath}/%/%`)
|
||||
.where('asset.ownerId', '=', asUuid(userId))
|
||||
.where('asset.visibility', '=', AssetVisibility.Timeline)
|
||||
.where('asset.deletedAt', 'is', null)
|
||||
.where('asset.fileCreatedAt', 'is not', null)
|
||||
.where('asset.fileModifiedAt', 'is not', null)
|
||||
.where('asset.localDateTime', 'is not', null)
|
||||
.where((eb) => eb(eb.ref('asset_file.type'), '=', AssetFileType.Original))
|
||||
.where((eb) => eb(eb.ref('asset_file.path'), 'like', `%${normalizedPath}/%`))
|
||||
.where((eb) => eb(eb.ref('asset_file.path'), 'not like', `%${normalizedPath}/%/%`))
|
||||
.orderBy(
|
||||
(eb) => eb.fn('regexp_replace', ['asset.originalPath', eb.val('.*/(.+)'), eb.val(String.raw`\1`)]),
|
||||
(eb) => eb.fn('regexp_replace', [eb.ref('asset_file.path'), eb.val('.*/(.+)'), eb.val(String.raw`\\1`)]),
|
||||
'asc',
|
||||
)
|
||||
.$call(withExif)
|
||||
.execute();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -72,9 +72,6 @@ export class AssetTable {
|
|||
@Column()
|
||||
type!: AssetType;
|
||||
|
||||
@Column()
|
||||
originalPath!: string;
|
||||
|
||||
@Column({ type: 'timestamp with time zone', index: true })
|
||||
fileCreatedAt!: Timestamp;
|
||||
|
||||
|
|
|
|||
|
|
@ -224,13 +224,19 @@ export class MetadataService extends BaseService {
|
|||
return;
|
||||
}
|
||||
|
||||
const originalFile = asset.files?.find((file) => file.type === AssetFileType.Original) ?? null;
|
||||
if (!originalFile) {
|
||||
this.logger.warn(`Asset ${asset.id} has no original file, skipping metadata extraction`);
|
||||
return;
|
||||
}
|
||||
|
||||
const [exifTags, stats] = await Promise.all([
|
||||
this.getExifTags(asset),
|
||||
this.storageRepository.stat(asset.originalPath),
|
||||
this.storageRepository.stat(originalFile.path),
|
||||
]);
|
||||
this.logger.verbose('Exif Tags', exifTags);
|
||||
|
||||
const dates = this.getDates(asset, exifTags, stats);
|
||||
const dates = this.getDates(asset, originalFile.path, exifTags, stats);
|
||||
|
||||
const { width, height } = this.getImageDimensions(exifTags);
|
||||
let geo: ReverseGeocodeResult = { country: null, state: null, city: null },
|
||||
|
|
@ -351,7 +357,7 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
|
||||
let sidecarPath = null;
|
||||
for (const candidate of this.getSidecarCandidates(asset)) {
|
||||
for (const candidate of this.getSidecarCandidates(asset.files)) {
|
||||
const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
|
||||
if (!exists) {
|
||||
continue;
|
||||
|
|
@ -400,7 +406,14 @@ export class MetadataService extends BaseService {
|
|||
|
||||
const tagsList = (asset.tags || []).map((tag) => tag.value);
|
||||
|
||||
const sidecarPath = asset.files[0]?.path || `${asset.originalPath}.xmp`;
|
||||
const existingSidecar = asset.files?.find((file) => file.type === AssetFileType.Sidecar) ?? null;
|
||||
const original = asset.files?.find((file) => file.type === AssetFileType.Original) ?? null;
|
||||
|
||||
if (!original) {
|
||||
throw new Error(`Asset ${asset.id} has no original file`);
|
||||
}
|
||||
|
||||
const sidecarPath = existingSidecar?.path || `${original.path}.xmp`; // prefer file.jpg.xmp by default
|
||||
const exif = _.omitBy(
|
||||
<Tags>{
|
||||
Description: description,
|
||||
|
|
@ -427,7 +440,12 @@ export class MetadataService extends BaseService {
|
|||
return JobStatus.Success;
|
||||
}
|
||||
|
||||
private getSidecarCandidates({ files, originalPath }: { files: AssetFile[] | null; originalPath: string }) {
|
||||
private getSidecarCandidates(files: AssetFile[] | null) {
|
||||
const original = files?.find((file) => file.type === AssetFileType.Original);
|
||||
if (!original) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const candidates: string[] = [];
|
||||
|
||||
const existingSidecar = files?.find((file) => file.type === AssetFileType.Sidecar);
|
||||
|
|
@ -436,11 +454,11 @@ export class MetadataService extends BaseService {
|
|||
candidates.push(existingSidecar.path);
|
||||
}
|
||||
|
||||
const assetPath = parse(originalPath);
|
||||
const assetPath = parse(original.path);
|
||||
|
||||
candidates.push(
|
||||
// IMG_123.jpg.xmp
|
||||
`${originalPath}.xmp`,
|
||||
`${assetPath}.xmp`,
|
||||
// IMG_123.xmp
|
||||
`${join(assetPath.dir, assetPath.name)}.xmp`,
|
||||
);
|
||||
|
|
@ -464,25 +482,31 @@ export class MetadataService extends BaseService {
|
|||
return { width, height };
|
||||
}
|
||||
|
||||
private async getExifTags(asset: { originalPath: string; files: AssetFile[]; type: AssetType }): Promise<ImmichTags> {
|
||||
private async getExifTags(asset: { id; files: AssetFile[]; type: AssetType }): Promise<ImmichTags> {
|
||||
const originalFile = asset.files?.find((file) => file.type === AssetFileType.Original) ?? null;
|
||||
|
||||
if (!originalFile) {
|
||||
throw new Error(`Asset ${asset.id} has no original file`);
|
||||
}
|
||||
|
||||
if (asset.type === AssetType.Image) {
|
||||
const hasSidecar = asset.files?.some(({ type }) => type === AssetFileType.Sidecar);
|
||||
|
||||
if (!hasSidecar) {
|
||||
return this.metadataRepository.readTags(asset.originalPath);
|
||||
return this.metadataRepository.readTags(originalFile.path);
|
||||
}
|
||||
}
|
||||
|
||||
if (asset.files && asset.files.length > 1) {
|
||||
throw new Error(`Asset ${asset.originalPath} has multiple sidecar files`);
|
||||
throw new Error(`Asset ${originalFile.path} has multiple sidecar files`);
|
||||
}
|
||||
|
||||
const sidecarFile = asset.files ? getAssetFiles(asset.files).sidecarFile : undefined;
|
||||
|
||||
const [mediaTags, sidecarTags, videoTags] = await Promise.all([
|
||||
this.metadataRepository.readTags(asset.originalPath),
|
||||
this.metadataRepository.readTags(originalFile.path),
|
||||
sidecarFile ? this.metadataRepository.readTags(sidecarFile.path) : null,
|
||||
asset.type === AssetType.Video ? this.getVideoTags(asset.originalPath) : null,
|
||||
asset.type === AssetType.Video ? this.getVideoTags(originalFile.path) : null,
|
||||
]);
|
||||
|
||||
// prefer dates from sidecar tags
|
||||
|
|
@ -546,7 +570,7 @@ export class MetadataService extends BaseService {
|
|||
return asset.type === AssetType.Image && !!(tags.MotionPhoto || tags.MicroVideo);
|
||||
}
|
||||
|
||||
private async applyMotionPhotos(asset: Asset, tags: ImmichTags, dates: Dates, stats: Stats) {
|
||||
private async applyMotionPhotos(asset: Asset, originalPath: string, tags: ImmichTags, dates: Dates, stats: Stats) {
|
||||
const isMotionPhoto = tags.MotionPhoto;
|
||||
const isMicroVideo = tags.MicroVideo;
|
||||
const videoOffset = tags.MicroVideoOffset;
|
||||
|
|
@ -577,7 +601,7 @@ export class MetadataService extends BaseService {
|
|||
return;
|
||||
}
|
||||
|
||||
this.logger.debug(`Starting motion photo video extraction for asset ${asset.id}: ${asset.originalPath}`);
|
||||
this.logger.debug(`Starting motion photo video extraction for asset ${asset.id}: ${originalPath}`);
|
||||
|
||||
try {
|
||||
const position = stats.size - length - padding;
|
||||
|
|
@ -585,15 +609,15 @@ export class MetadataService extends BaseService {
|
|||
// Samsung MotionPhoto video extraction
|
||||
// HEIC-encoded
|
||||
if (hasMotionPhotoVideo) {
|
||||
video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'MotionPhotoVideo');
|
||||
video = await this.metadataRepository.extractBinaryTag(originalPath, 'MotionPhotoVideo');
|
||||
}
|
||||
// JPEG-encoded; HEIC also contains these tags, so this conditional must come second
|
||||
else if (hasEmbeddedVideoFile) {
|
||||
video = await this.metadataRepository.extractBinaryTag(asset.originalPath, 'EmbeddedVideoFile');
|
||||
video = await this.metadataRepository.extractBinaryTag(originalPath, 'EmbeddedVideoFile');
|
||||
}
|
||||
// Default video extraction
|
||||
else {
|
||||
video = await this.storageRepository.readFile(asset.originalPath, {
|
||||
video = await this.storageRepository.readFile(originalPath, {
|
||||
buffer: Buffer.alloc(length),
|
||||
position,
|
||||
length,
|
||||
|
|
@ -617,7 +641,7 @@ export class MetadataService extends BaseService {
|
|||
localDateTime: dates.localDateTime,
|
||||
checksum,
|
||||
ownerId: asset.ownerId,
|
||||
originalPath: StorageCore.getAndroidMotionPath(asset, motionAssetId),
|
||||
files: [{ type: AssetFileType.Original, path: StorageCore.getAndroidMotionPath(asset, motionAssetId) }],
|
||||
originalFileName: `${parse(asset.originalFileName).name}.mp4`,
|
||||
visibility: AssetVisibility.Hidden,
|
||||
deviceAssetId: 'NONE',
|
||||
|
|
@ -848,7 +872,8 @@ export class MetadataService extends BaseService {
|
|||
}
|
||||
|
||||
private getDates(
|
||||
asset: { id: string; originalPath: string; fileCreatedAt: Date },
|
||||
asset: { id: string; fileCreatedAt: Date },
|
||||
originalPath: string,
|
||||
exifTags: ImmichTags,
|
||||
stats: Stats,
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -161,12 +161,18 @@ export const onBeforeUnlink = async (
|
|||
{ asset: assetRepository }: AssetHookRepositories,
|
||||
{ livePhotoVideoId }: { livePhotoVideoId: string },
|
||||
) => {
|
||||
const motion = await assetRepository.getById(livePhotoVideoId);
|
||||
const motion = await assetRepository.getById(livePhotoVideoId, { files: true });
|
||||
if (!motion) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (StorageCore.isAndroidMotionPath(motion.originalPath)) {
|
||||
const motionPath = motion.files?.find((file) => file.type === AssetFileType.Original)?.path;
|
||||
|
||||
if (!motionPath) {
|
||||
throw new BadRequestException('Live photo video original file not found');
|
||||
}
|
||||
|
||||
if (StorageCore.isAndroidMotionPath(motionPath)) {
|
||||
throw new BadRequestException('Cannot unlink Android motion photos');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -191,13 +191,23 @@ export function withFaces(eb: ExpressionBuilder<DB, 'asset'>, withDeletedFace?:
|
|||
}
|
||||
|
||||
export function withFiles(eb: ExpressionBuilder<DB, 'asset'>, type?: AssetFileType) {
|
||||
return jsonArrayFrom(
|
||||
const files = jsonArrayFrom(
|
||||
eb
|
||||
.selectFrom('asset_file')
|
||||
.select(columns.assetFiles)
|
||||
.whereRef('asset_file.assetId', '=', 'asset.id')
|
||||
.$if(!!type, (qb) => qb.where('asset_file.type', '=', type!)),
|
||||
).as('files');
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
export function withSidecars(eb: ExpressionBuilder<DB, 'asset'>, type?: AssetFileType) {
|
||||
return withFiles(eb, AssetFileType.Sidecar);
|
||||
}
|
||||
|
||||
export function withOriginals(eb: ExpressionBuilder<DB, 'asset'>, type?: AssetFileType) {
|
||||
return withFiles(eb, AssetFileType.Original);
|
||||
}
|
||||
|
||||
export function withFilePath(eb: ExpressionBuilder<DB, 'asset'>, type: AssetFileType) {
|
||||
|
|
@ -208,6 +218,10 @@ export function withFilePath(eb: ExpressionBuilder<DB, 'asset'>, type: AssetFile
|
|||
.where('asset_file.type', '=', type);
|
||||
}
|
||||
|
||||
export function withOriginalPath(eb: ExpressionBuilder<DB, 'asset'>) {
|
||||
return withFilePath(eb, AssetFileType.Original).as('originalPath');
|
||||
}
|
||||
|
||||
export function withFacesAndPeople(eb: ExpressionBuilder<DB, 'asset'>, withDeletedFace?: boolean) {
|
||||
return jsonArrayFrom(
|
||||
eb
|
||||
|
|
|
|||
Loading…
Reference in New Issue