mirror of
https://github.com/immich-app/immich.git
synced 2025-04-21 15:36:26 +02:00
feat(server): Nullable asset dates (#15669)
* nullable dates * wip * don't search for null dates * Add placeholder type * cleanup
This commit is contained in:
parent
f5edc87e4d
commit
5407a28533
13 changed files with 100 additions and 11 deletions
e2e/src/api/specs
server/src
|
@ -298,6 +298,7 @@ describe('/libraries', () => {
|
|||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
|
|
6
server/src/db.d.ts
vendored
6
server/src/db.d.ts
vendored
|
@ -122,8 +122,8 @@ export interface Assets {
|
|||
duplicateId: string | null;
|
||||
duration: string | null;
|
||||
encodedVideoPath: Generated<string | null>;
|
||||
fileCreatedAt: Timestamp;
|
||||
fileModifiedAt: Timestamp;
|
||||
fileCreatedAt: Timestamp | null;
|
||||
fileModifiedAt: Timestamp | null;
|
||||
id: Generated<string>;
|
||||
isArchived: Generated<boolean>;
|
||||
isExternal: Generated<boolean>;
|
||||
|
@ -132,7 +132,7 @@ export interface Assets {
|
|||
isVisible: Generated<boolean>;
|
||||
libraryId: string | null;
|
||||
livePhotoVideoId: string | null;
|
||||
localDateTime: Timestamp;
|
||||
localDateTime: Timestamp | null;
|
||||
originalFileName: string;
|
||||
originalPath: string;
|
||||
ownerId: string;
|
||||
|
|
|
@ -100,13 +100,13 @@ export class AssetEntity {
|
|||
deletedAt!: Date | null;
|
||||
|
||||
@Index('idx_asset_file_created_at')
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
localDateTime!: Date;
|
||||
|
||||
@Column({ type: 'timestamptz' })
|
||||
@Column({ type: 'timestamptz', nullable: true, default: null })
|
||||
fileModifiedAt!: Date;
|
||||
|
||||
@Column({ type: 'boolean', default: false })
|
||||
|
@ -180,6 +180,12 @@ export class AssetEntity {
|
|||
duplicateId!: string | null;
|
||||
}
|
||||
|
||||
export type AssetEntityPlaceholder = AssetEntity & {
|
||||
fileCreatedAt: Date | null;
|
||||
fileModifiedAt: Date | null;
|
||||
localDateTime: Date | null;
|
||||
};
|
||||
|
||||
export function withExif<O>(qb: SelectQueryBuilder<DB, 'assets', O>) {
|
||||
return qb.leftJoin('exif', 'assets.id', 'exif.assetId').select((eb) => eb.fn.toJson(eb.table('exif')).as('exifInfo'));
|
||||
}
|
||||
|
@ -419,5 +425,8 @@ export function searchAssetBuilder(kysely: Kysely<DB>, options: AssetSearchBuild
|
|||
)
|
||||
.$if(!!options.withExif, withExifInner)
|
||||
.$if(!!(options.withFaces || options.withPeople || options.personIds), (qb) => qb.select(withFacesAndPeople))
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null));
|
||||
.$if(!options.withDeleted, (qb) => qb.where('assets.deletedAt', 'is', null))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null);
|
||||
}
|
||||
|
|
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
18
server/src/migrations/1737845696644-NullableDates.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class NullableDates1737845696644 implements MigrationInterface {
|
||||
name = 'NullableDates1737845696644'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" DROP NOT NULL`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileModifiedAt" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "fileCreatedAt" SET NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
|
@ -43,3 +43,6 @@ where
|
|||
and "activity"."albumId" = $2
|
||||
and "activity"."isLiked" = $3
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
|
|
|
@ -159,6 +159,9 @@ where
|
|||
"ownerId" = $1::uuid
|
||||
and "deviceId" = $2
|
||||
and "isVisible" = $3
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "deletedAt" is null
|
||||
|
||||
-- AssetRepository.getLivePhotoCount
|
||||
|
@ -260,6 +263,9 @@ with
|
|||
where
|
||||
"assets"."deletedAt" is null
|
||||
and "assets"."isVisible" = $2
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
)
|
||||
select
|
||||
"timeBucket",
|
||||
|
|
|
@ -13,6 +13,9 @@ where
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
"assets"."fileCreatedAt" desc
|
||||
limit
|
||||
|
@ -34,6 +37,9 @@ offset
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" < $6
|
||||
order by
|
||||
random()
|
||||
|
@ -54,6 +60,9 @@ union all
|
|||
and "assets"."isFavorite" = $11
|
||||
and "assets"."isArchived" = $12
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
and "assets"."id" > $13
|
||||
order by
|
||||
random()
|
||||
|
@ -77,6 +86,9 @@ where
|
|||
and "assets"."isFavorite" = $4
|
||||
and "assets"."isArchived" = $5
|
||||
and "assets"."deletedAt" is null
|
||||
and "assets"."fileCreatedAt" is not null
|
||||
and "assets"."fileModifiedAt" is not null
|
||||
and "assets"."localDateTime" is not null
|
||||
order by
|
||||
smart_search.embedding <=> $6
|
||||
limit
|
||||
|
|
|
@ -10,6 +10,9 @@ where
|
|||
and "isVisible" = $3
|
||||
and "isArchived" = $4
|
||||
and "deletedAt" is null
|
||||
and "fileModifiedAt" is not null
|
||||
and "fileModifiedAt" is not null
|
||||
and "localDateTime" is not null
|
||||
|
||||
-- ViewRepository.getAssetsByOriginalPath
|
||||
select
|
||||
|
@ -23,6 +26,9 @@ where
|
|||
and "isVisible" = $2
|
||||
and "isArchived" = $3
|
||||
and "deletedAt" is null
|
||||
and "fileModifiedAt" is not null
|
||||
and "fileModifiedAt" is not null
|
||||
and "localDateTime" is not null
|
||||
and "originalPath" like $4
|
||||
and "originalPath" not like $5
|
||||
order by
|
||||
|
|
|
@ -65,6 +65,9 @@ export class ActivityRepository {
|
|||
.where('activity.albumId', '=', albumId)
|
||||
.where('activity.isLiked', '=', false)
|
||||
.where('assets.deletedAt', 'is', null)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
return count as number;
|
||||
|
|
|
@ -7,6 +7,7 @@ import { AssetFiles, AssetJobStatus, Assets, DB, Exif } from 'src/db';
|
|||
import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||
import {
|
||||
AssetEntity,
|
||||
AssetEntityPlaceholder,
|
||||
hasPeople,
|
||||
searchAssetBuilder,
|
||||
truncatedDate,
|
||||
|
@ -183,8 +184,12 @@ export class AssetRepository {
|
|||
.execute();
|
||||
}
|
||||
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntity> {
|
||||
return this.db.insertInto('assets').values(asset).returningAll().executeTakeFirst() as any as Promise<AssetEntity>;
|
||||
create(asset: Insertable<Assets>): Promise<AssetEntityPlaceholder> {
|
||||
return this.db
|
||||
.insertInto('assets')
|
||||
.values(asset)
|
||||
.returningAll()
|
||||
.executeTakeFirst() as any as Promise<AssetEntityPlaceholder>;
|
||||
}
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, { day: 1, month: 1 }] })
|
||||
|
@ -395,6 +400,9 @@ export class AssetRepository {
|
|||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('deviceId', '=', deviceId)
|
||||
.where('isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where('deletedAt', 'is', null)
|
||||
.execute();
|
||||
|
||||
|
@ -562,7 +570,10 @@ export class AssetRepository {
|
|||
.where('job_status.duplicatesDetectedAt', 'is', null)
|
||||
.where('job_status.previewAt', 'is not', null)
|
||||
.where((eb) => eb.exists(eb.selectFrom('smart_search').where('assetId', '=', eb.ref('assets.id'))))
|
||||
.where('assets.isVisible', '=', true),
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null),
|
||||
)
|
||||
.$if(property === WithoutProperty.ENCODED_VIDEO, (qb) =>
|
||||
qb
|
||||
|
@ -656,6 +667,9 @@ export class AssetRepository {
|
|||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.VIDEO).as(AssetType.VIDEO))
|
||||
.select((eb) => eb.fn.countAll().filterWhere('type', '=', AssetType.OTHER).as(AssetType.OTHER))
|
||||
.where('ownerId', '=', asUuid(ownerId))
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.where('isVisible', '=', true)
|
||||
.$if(isArchived !== undefined, (qb) => qb.where('isArchived', '=', isArchived!))
|
||||
.$if(isFavorite !== undefined, (qb) => qb.where('isFavorite', '=', isFavorite!))
|
||||
|
@ -688,6 +702,9 @@ export class AssetRepository {
|
|||
.$if(!!options.isTrashed, (qb) => qb.where('assets.status', '!=', AssetStatus.DELETED))
|
||||
.where('assets.deletedAt', options.isTrashed ? 'is not' : 'is', null)
|
||||
.where('assets.isVisible', '=', true)
|
||||
.where('assets.fileCreatedAt', 'is not', null)
|
||||
.where('assets.fileModifiedAt', 'is not', null)
|
||||
.where('assets.localDateTime', 'is not', null)
|
||||
.$if(!!options.albumId, (qb) =>
|
||||
qb
|
||||
.innerJoin('albums_assets_assets', 'assets.id', 'albums_assets_assets.assetsId')
|
||||
|
|
|
@ -18,6 +18,9 @@ export class ViewRepository {
|
|||
.where('isVisible', '=', true)
|
||||
.where('isArchived', '=', false)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('localDateTime', 'is not', null)
|
||||
.execute();
|
||||
|
||||
return results.map((row) => row.directoryPath.replaceAll(/^\/|\/$/g, ''));
|
||||
|
@ -35,6 +38,9 @@ export class ViewRepository {
|
|||
.where('isVisible', '=', true)
|
||||
.where('isArchived', '=', false)
|
||||
.where('deletedAt', 'is', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('fileModifiedAt', 'is not', null)
|
||||
.where('localDateTime', 'is not', null)
|
||||
.where('originalPath', 'like', `%${normalizedPath}/%`)
|
||||
.where('originalPath', 'not like', `%${normalizedPath}/%/%`)
|
||||
.orderBy(
|
||||
|
|
|
@ -503,7 +503,7 @@ export class LibraryService extends BaseService {
|
|||
}
|
||||
|
||||
const mtime = stat.mtime;
|
||||
const isAssetModified = mtime.toISOString() !== asset.fileModifiedAt.toISOString();
|
||||
const isAssetModified = !asset.fileModifiedAt || mtime.toISOString() !== asset.fileModifiedAt.toISOString();
|
||||
|
||||
if (asset.isOffline || isAssetModified) {
|
||||
this.logger.debug(`Asset was offline or modified, updating asset record ${asset.originalPath}`);
|
||||
|
|
|
@ -171,6 +171,14 @@ export class MetadataService extends BaseService {
|
|||
|
||||
this.logger.verbose('Exif Tags', exifTags);
|
||||
|
||||
if (!asset.fileCreatedAt) {
|
||||
asset.fileCreatedAt = stats.mtime;
|
||||
}
|
||||
|
||||
if (!asset.fileModifiedAt) {
|
||||
asset.fileModifiedAt = stats.mtime;
|
||||
}
|
||||
|
||||
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
||||
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue