fix: time buckets (#4358)

* fix: time buckets

* chore: update entity metadata

* fix: set correct localDateTime

* fix: display without timezone shifting

* fix: handle non-utc databases

* fix: scrollbar

* docs: comment how buckets are sorted

* chore: remove test/log

* chore: lint

---------

Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
This commit is contained in:
Jason Rasmussen
2023-10-06 08:12:09 -04:00
committed by GitHub
parent 4a8887f37b
commit 35fa6397ea
8 changed files with 40 additions and 35 deletions

View File

@@ -157,9 +157,10 @@ export class MetadataService {
await this.applyMotionPhotos(asset, tags);
await this.applyReverseGeocoding(asset, exifData);
await this.assetRepository.upsertExif(exifData);
let localDateTime = exifData.dateTimeOriginal ?? undefined;
const dateTimeOriginal = exifDate(firstDateTime(tags as Tags)) ?? exifData.dateTimeOriginal;
let localDateTime = dateTimeOriginal ?? undefined;
const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0;
if (dateTimeOriginal && timeZoneOffset) {

View File

@@ -84,7 +84,7 @@ export class AssetEntity {
@Column({ type: 'timestamptz' })
fileCreatedAt!: Date;
@Column({ type: 'timestamp' })
@Column({ type: 'timestamptz' })
localDateTime!: Date;
@Column({ type: 'timestamptz' })

View File

@@ -4,22 +4,15 @@ export class AddLocalDateTime1694525143117 implements MigrationInterface {
name = 'AddLocalDateTime1694525143117';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "assets" ADD "localDateTime" TIMESTAMP`);
await queryRunner.query(`
update "assets"
set "localDateTime" = "fileCreatedAt"`);
await queryRunner.query(`
update "assets"
set "localDateTime" = "fileCreatedAt" at TIME ZONE "exif"."timeZone"
from "exif"
where
"exif"."assetId" = "assets"."id" and
"exif"."timeZone" is not null`);
await queryRunner.query(`ALTER TABLE "assets" ADD "localDateTime" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`UPDATE "assets" SET "localDateTime" = "fileCreatedAt"`);
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "localDateTime" SET NOT NULL`);
await queryRunner.query(`CREATE INDEX "IDX_day_of_month" ON assets (EXTRACT(DAY FROM "localDateTime"))`);
await queryRunner.query(`CREATE INDEX "IDX_month" ON assets (EXTRACT(MONTH FROM "localDateTime"))`);
await queryRunner.query(
`CREATE INDEX "IDX_day_of_month" ON assets (EXTRACT(DAY FROM "localDateTime" AT TIME ZONE 'UTC'))`,
);
await queryRunner.query(
`CREATE INDEX "IDX_month" ON assets (EXTRACT(MONTH FROM "localDateTime" AT TIME ZONE 'UTC'))`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {

View File

@@ -29,6 +29,8 @@ const truncateMap: Record<TimeBucketSize, string> = {
[TimeBucketSize.MONTH]: 'month',
};
const TIME_BUCKET_COLUMN = 'localDateTime';
@Injectable()
export class AssetRepository implements IAssetRepository {
constructor(
@@ -86,8 +88,8 @@ export class AssetRepository implements IAssetRepository {
AND entity.isVisible = true
AND entity.isArchived = false
AND entity.resizePath IS NOT NULL
AND EXTRACT(DAY FROM entity.localDateTime) = :day
AND EXTRACT(MONTH FROM entity.localDateTime) = :month`,
AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day
AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`,
{
ownerId,
day,
@@ -480,19 +482,25 @@ export class AssetRepository implements IAssetRepository {
return this.getBuilder(options)
.select(`COUNT(asset.id)::int`, 'count')
.addSelect(`date_trunc('${truncateValue}', "fileCreatedAt")`, 'timeBucket')
.groupBy(`date_trunc('${truncateValue}', "fileCreatedAt")`)
.orderBy(`date_trunc('${truncateValue}', "fileCreatedAt")`, 'DESC')
.addSelect(`date_trunc('${truncateValue}', "${TIME_BUCKET_COLUMN}" at time zone 'UTC')`, 'timeBucket')
.groupBy(`date_trunc('${truncateValue}', "${TIME_BUCKET_COLUMN}" at time zone 'UTC')`)
.orderBy(`date_trunc('${truncateValue}', "${TIME_BUCKET_COLUMN}" at time zone 'UTC')`, 'DESC')
.getRawMany();
}
getByTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]> {
const truncateValue = truncateMap[options.size];
return this.getBuilder(options)
.andWhere(`date_trunc('${truncateValue}', "fileCreatedAt") = :timeBucket`, { timeBucket })
.orderBy(`date_trunc('day', "localDateTime")`, 'DESC')
.addOrderBy('asset.fileCreatedAt', 'DESC')
.getMany();
return (
this.getBuilder(options)
.andWhere(`date_trunc('${truncateValue}', "${TIME_BUCKET_COLUMN}" at time zone 'UTC') = :timeBucket`, {
timeBucket,
})
// First sort by the day in localtime (put it in the right bucket)
.orderBy(`date_trunc('day', "${TIME_BUCKET_COLUMN}" at time zone 'UTC')`, 'DESC')
// and then sort by the actual time
.addOrderBy('asset.fileCreatedAt', 'DESC')
.getMany()
);
}
private getBuilder(options: TimeBucketOptions) {