Skip to content

Commit

Permalink
feat(server): Nullable asset dates (#15669)
Browse files Browse the repository at this point in the history
* nullable dates

* wip

* don't search for null dates

* Add placeholder type

* cleanup
  • Loading branch information
etnoy authored Feb 13, 2025
1 parent f5edc87 commit 5407a28
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 11 deletions.
1 change: 1 addition & 0 deletions e2e/src/api/specs/library.e2e-spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`,
Expand Down
6 changes: 3 additions & 3 deletions server/src/db.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand All @@ -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;
Expand Down
17 changes: 13 additions & 4 deletions server/src/entities/asset.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 })
Expand Down Expand Up @@ -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'));
}
Expand Down Expand Up @@ -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 changes: 18 additions & 0 deletions server/src/migrations/1737845696644-NullableDates.ts
Original file line number Diff line number Diff line change
@@ -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`);
}

}
3 changes: 3 additions & 0 deletions server/src/queries/activity.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 6 additions & 0 deletions server/src/queries/asset.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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",
Expand Down
12 changes: 12 additions & 0 deletions server/src/queries/search.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand All @@ -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()
Expand All @@ -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
Expand Down
6 changes: 6 additions & 0 deletions server/src/queries/view.repository.sql
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions server/src/repositories/activity.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
23 changes: 20 additions & 3 deletions server/src/repositories/asset.repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 }] })
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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!))
Expand Down Expand Up @@ -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')
Expand Down
6 changes: 6 additions & 0 deletions server/src/repositories/view-repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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, ''));
Expand All @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion server/src/services/library.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down
8 changes: 8 additions & 0 deletions server/src/services/metadata.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down

0 comments on commit 5407a28

Please sign in to comment.