From aea2c9506d7214859932fdf0d2f2fe8cb41e5a44 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Sat, 3 May 2025 02:06:34 +0000 Subject: [PATCH] Use nulls, make-sql --- e2e/src/api/specs/timeline.e2e-spec.ts | 17 ++- mobile/openapi/README.md | 2 +- mobile/openapi/lib/api.dart | 2 +- mobile/openapi/lib/api_client.dart | 4 +- .../model/time_bucket_asset_response_dto.dart | 4 +- ...bucket_asset_response_dto_stack_inner.dart | 91 ++++++++++++++ .../model/timeline_stack_response_dto.dart | 115 ------------------ open-api/immich-openapi-specs.json | 36 ++---- open-api/typescript-sdk/src/fetch-client.ts | 15 +-- server/src/bin/sync-sql.ts | 4 +- server/src/dtos/asset-response.dto.ts | 2 +- server/src/dtos/time-bucket.dto.ts | 32 +++-- server/src/queries/asset.repository.sql | 19 ++- server/src/services/sync.service.ts | 4 +- server/src/services/timeline.service.ts | 12 +- server/src/services/timeline.service.types.ts | 10 +- server/src/utils/bytes.ts | 5 +- .../gallery-viewer/gallery-viewer.svelte | 5 - web/src/lib/stores/assets-store.svelte.ts | 12 +- 19 files changed, 186 insertions(+), 205 deletions(-) create mode 100644 mobile/openapi/lib/model/time_bucket_asset_response_dto_stack_inner.dart delete mode 100644 mobile/openapi/lib/model/timeline_stack_response_dto.dart diff --git a/e2e/src/api/specs/timeline.e2e-spec.ts b/e2e/src/api/specs/timeline.e2e-spec.ts index bf330e994a..16bddac645 100644 --- a/e2e/src/api/specs/timeline.e2e-spec.ts +++ b/e2e/src/api/specs/timeline.e2e-spec.ts @@ -104,7 +104,7 @@ describe('/timeline', () => { const req1 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true }); + .query({ withPartners: true, isArchived: true }); expect(req1.status).toBe(400); expect(req1.body).toEqual(errorDto.badRequest()); @@ -112,7 +112,7 @@ describe('/timeline', () => { const req2 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${user.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined }); + .query({ withPartners: true, isArchived: undefined }); expect(req2.status).toBe(400); expect(req2.body).toEqual(errorDto.badRequest()); @@ -122,7 +122,7 @@ describe('/timeline', () => { const req1 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true }); + .query({ withPartners: true, isFavorite: true }); expect(req1.status).toBe(400); expect(req1.body).toEqual(errorDto.badRequest()); @@ -130,7 +130,7 @@ describe('/timeline', () => { const req2 = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false }); + .query({ withPartners: true, isFavorite: false }); expect(req2.status).toBe(400); expect(req2.body).toEqual(errorDto.badRequest()); @@ -140,7 +140,7 @@ describe('/timeline', () => { const req = await request(app) .get('/timeline/buckets') .set('Authorization', `Bearer ${user.accessToken}`) - .query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true }); + .query({ withPartners: true, isTrashed: true }); expect(req.status).toBe(400); expect(req.body).toEqual(errorDto.badRequest()); @@ -150,7 +150,6 @@ describe('/timeline', () => { describe('GET /timeline/bucket', () => { it('should require authentication', async () => { const { status, body } = await request(app).get('/timeline/bucket').query({ - size: TimeBucketSize.Month, timeBucket: '1900-01-01', }); @@ -161,7 +160,7 @@ describe('/timeline', () => { it('should handle 5 digit years', async () => { const { status, body } = await request(app) .get('/timeline/bucket') - .query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' }) + .query({ timeBucket: '012345-01-01' }) .set('Authorization', `Bearer ${timeBucketUser.accessToken}`); expect(status).toBe(200); @@ -173,7 +172,7 @@ describe('/timeline', () => { // const { status, body } = await request(app) // .get('/timeline/bucket') // .set('Authorization', `Bearer ${user.accessToken}`) - // .query({ size: TimeBucketSize.Month, timeBucket: 'foo' }); + // .query({ timeBucket: 'foo' }); // expect(status).toBe(400); // expect(body).toEqual(errorDto.badRequest); @@ -183,7 +182,7 @@ describe('/timeline', () => { const { status, body } = await request(app) .get('/timeline/bucket') .set('Authorization', `Bearer ${timeBucketUser.accessToken}`) - .query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' }); + .query({ timeBucket: '1970-02-10' }); expect(status).toBe(200); expect(body).toEqual([]); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f828c7c784..8ab2fa1c4e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -479,10 +479,10 @@ Class | Method | HTTP request | Description - [TestEmailResponseDto](doc//TestEmailResponseDto.md) - [TimeBucketAssetResponseDto](doc//TimeBucketAssetResponseDto.md) - [TimeBucketAssetResponseDtoDurationInner](doc//TimeBucketAssetResponseDtoDurationInner.md) + - [TimeBucketAssetResponseDtoStackInner](doc//TimeBucketAssetResponseDtoStackInner.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketsResponseDto](doc//TimeBucketsResponseDto.md) - [TimelineAssetDescriptionDto](doc//TimelineAssetDescriptionDto.md) - - [TimelineStackResponseDto](doc//TimelineStackResponseDto.md) - [ToneMapping](doc//ToneMapping.md) - [TranscodeHWAccel](doc//TranscodeHWAccel.md) - [TranscodePolicy](doc//TranscodePolicy.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 23c4dd7d26..c0ac7557d9 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -283,10 +283,10 @@ part 'model/template_response_dto.dart'; part 'model/test_email_response_dto.dart'; part 'model/time_bucket_asset_response_dto.dart'; part 'model/time_bucket_asset_response_dto_duration_inner.dart'; +part 'model/time_bucket_asset_response_dto_stack_inner.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_buckets_response_dto.dart'; part 'model/timeline_asset_description_dto.dart'; -part 'model/timeline_stack_response_dto.dart'; part 'model/tone_mapping.dart'; part 'model/transcode_hw_accel.dart'; part 'model/transcode_policy.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index ca02898f2f..4a0e0cc219 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -622,14 +622,14 @@ class ApiClient { return TimeBucketAssetResponseDto.fromJson(value); case 'TimeBucketAssetResponseDtoDurationInner': return TimeBucketAssetResponseDtoDurationInner.fromJson(value); + case 'TimeBucketAssetResponseDtoStackInner': + return TimeBucketAssetResponseDtoStackInner.fromJson(value); case 'TimeBucketResponseDto': return TimeBucketResponseDto.fromJson(value); case 'TimeBucketsResponseDto': return TimeBucketsResponseDto.fromJson(value); case 'TimelineAssetDescriptionDto': return TimelineAssetDescriptionDto.fromJson(value); - case 'TimelineStackResponseDto': - return TimelineStackResponseDto.fromJson(value); case 'ToneMapping': return ToneMappingTypeTransformer().decode(value); case 'TranscodeHWAccel': diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart index 14c5b0549b..1255f636b7 100644 --- a/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto.dart @@ -56,7 +56,7 @@ class TimeBucketAssetResponseDto { List ratio; - List stack; + List stack; List thumbhash; @@ -158,7 +158,7 @@ class TimeBucketAssetResponseDto { ratio: json[r'ratio'] is Iterable ? (json[r'ratio'] as Iterable).cast().toList(growable: false) : const [], - stack: TimelineStackResponseDto.listFromJson(json[r'stack']), + stack: TimeBucketAssetResponseDtoStackInner.listFromJson(json[r'stack']), thumbhash: TimeBucketAssetResponseDtoDurationInner.listFromJson(json[r'thumbhash']), ); } diff --git a/mobile/openapi/lib/model/time_bucket_asset_response_dto_stack_inner.dart b/mobile/openapi/lib/model/time_bucket_asset_response_dto_stack_inner.dart new file mode 100644 index 0000000000..e351edc3b6 --- /dev/null +++ b/mobile/openapi/lib/model/time_bucket_asset_response_dto_stack_inner.dart @@ -0,0 +1,91 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TimeBucketAssetResponseDtoStackInner { + /// Returns a new [TimeBucketAssetResponseDtoStackInner] instance. + TimeBucketAssetResponseDtoStackInner({ + }); + + @override + bool operator ==(Object other) => identical(this, other) || other is TimeBucketAssetResponseDtoStackInner && + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + + @override + String toString() => 'TimeBucketAssetResponseDtoStackInner[]'; + + Map toJson() { + final json = {}; + return json; + } + + /// Returns a new [TimeBucketAssetResponseDtoStackInner] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TimeBucketAssetResponseDtoStackInner? fromJson(dynamic value) { + upgradeDto(value, "TimeBucketAssetResponseDtoStackInner"); + if (value is Map) { + final json = value.cast(); + + return TimeBucketAssetResponseDtoStackInner( + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TimeBucketAssetResponseDtoStackInner.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TimeBucketAssetResponseDtoStackInner.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TimeBucketAssetResponseDtoStackInner-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TimeBucketAssetResponseDtoStackInner.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/timeline_stack_response_dto.dart b/mobile/openapi/lib/model/timeline_stack_response_dto.dart deleted file mode 100644 index 132790ef03..0000000000 --- a/mobile/openapi/lib/model/timeline_stack_response_dto.dart +++ /dev/null @@ -1,115 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class TimelineStackResponseDto { - /// Returns a new [TimelineStackResponseDto] instance. - TimelineStackResponseDto({ - required this.assetCount, - required this.id, - required this.primaryAssetId, - }); - - num assetCount; - - String id; - - String primaryAssetId; - - @override - bool operator ==(Object other) => identical(this, other) || other is TimelineStackResponseDto && - other.assetCount == assetCount && - other.id == id && - other.primaryAssetId == primaryAssetId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (assetCount.hashCode) + - (id.hashCode) + - (primaryAssetId.hashCode); - - @override - String toString() => 'TimelineStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]'; - - Map toJson() { - final json = {}; - json[r'assetCount'] = this.assetCount; - json[r'id'] = this.id; - json[r'primaryAssetId'] = this.primaryAssetId; - return json; - } - - /// Returns a new [TimelineStackResponseDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static TimelineStackResponseDto? fromJson(dynamic value) { - upgradeDto(value, "TimelineStackResponseDto"); - if (value is Map) { - final json = value.cast(); - - return TimelineStackResponseDto( - assetCount: num.parse('${json[r'assetCount']}'), - id: mapValueOfType(json, r'id')!, - primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = TimelineStackResponseDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = TimelineStackResponseDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of TimelineStackResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = TimelineStackResponseDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'assetCount', - 'id', - 'primaryAssetId', - }; -} - diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index dac407ff12..db435795a2 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -13576,7 +13576,7 @@ "type": "string" }, { - "type": "number" + "type": "null" } ] }, @@ -13632,7 +13632,7 @@ "type": "string" }, { - "type": "number" + "type": "null" } ] }, @@ -13661,7 +13661,7 @@ "type": "string" }, { - "type": "number" + "type": "null" } ] }, @@ -13677,7 +13677,14 @@ "stack": { "default": [], "items": { - "$ref": "#/components/schemas/TimelineStackResponseDto" + "oneOf": [ + { + "type": "TimelineStackResponseDto" + }, + { + "type": "null" + } + ] }, "type": "array" }, @@ -13689,7 +13696,7 @@ "type": "string" }, { - "type": "number" + "type": "null" } ] }, @@ -13762,25 +13769,6 @@ ], "type": "object" }, - "TimelineStackResponseDto": { - "properties": { - "assetCount": { - "type": "number" - }, - "id": { - "type": "string" - }, - "primaryAssetId": { - "type": "string" - } - }, - "required": [ - "assetCount", - "id", - "primaryAssetId" - ], - "type": "object" - }, "ToneMapping": { "enum": [ "hable", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 12797619d3..3e2fc81a2b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1388,27 +1388,22 @@ export type TimelineAssetDescriptionDto = { city: string | null; country: string | null; }; -export type TimelineStackResponseDto = { - assetCount: number; - id: string; - primaryAssetId: string; -}; export type TimeBucketAssetResponseDto = { description: TimelineAssetDescriptionDto[]; - duration: (string | number)[]; + duration: (string | null)[]; id: string[]; isArchived: number[]; isFavorite: number[]; isImage: number[]; isTrashed: number[]; isVideo: number[]; - livePhotoVideoId: (string | number)[]; + livePhotoVideoId: (string | null)[]; localDateTime: string[]; ownerId: string[]; - projectionType: (string | number)[]; + projectionType: (string | null)[]; ratio: number[]; - stack: TimelineStackResponseDto[]; - thumbhash: (string | number)[]; + stack: (any | null)[]; + thumbhash: (string | null)[]; }; export type TimeBucketResponseDto = { bucketAssets: TimeBucketAssetResponseDto; diff --git a/server/src/bin/sync-sql.ts b/server/src/bin/sync-sql.ts index b791358a90..a114830e09 100644 --- a/server/src/bin/sync-sql.ts +++ b/server/src/bin/sync-sql.ts @@ -72,7 +72,9 @@ class SqlGenerator { await rm(this.options.targetDir, { force: true, recursive: true }); await mkdir(this.options.targetDir); - process.env.DB_HOSTNAME = 'localhost'; + if (!process.env.DB_HOSTNAME) { + process.env.DB_HOSTNAME = 'localhost'; + } const { database, cls, otel } = new ConfigRepository().getEnv(); const moduleFixture = await Test.createTestingModule({ diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 199491c06d..44de4bcf83 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -183,7 +183,7 @@ export function mapAsset(entity: MapAsset, options: AssetMapOptions = {}): Asset 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), + checksum: hexOrBufferToBase64(entity.checksum)!, stack: withStack ? mapStack(entity) : undefined, isOffline: entity.isOffline, hasMetadata: true, diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index c5a7d95a5c..b55e4daa7c 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -104,12 +104,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets { type: 'string', }, { - type: 'number', + type: 'null', }, ], }, }) - thumbhash: (string | number)[] = []; + thumbhash: (string | null)[] = []; @ApiProperty() localDateTime: Date[] = []; @@ -122,15 +122,27 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets { type: 'string', }, { - type: 'number', + type: 'null', }, ], }, }) - duration: (string | number)[] = []; + duration: (string | null)[] = []; - @ApiProperty({ type: [TimelineStackResponseDto] }) - stack: (TimelineStackResponseDto | number)[] = []; + @ApiProperty({ + type: 'array', + items: { + oneOf: [ + { + type: 'TimelineStackResponseDto', + }, + { + type: 'null', + }, + ], + }, + }) + stack: (TimelineStackResponseDto | null)[] = []; @ApiProperty({ type: 'array', @@ -140,12 +152,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets { type: 'string', }, { - type: 'number', + type: 'null', }, ], }, }) - projectionType: (string | number)[] = []; + projectionType: (string | null)[] = []; @ApiProperty({ type: 'array', @@ -155,12 +167,12 @@ export class TimeBucketAssetResponseDto implements TimeBucketAssets { type: 'string', }, { - type: 'number', + type: 'null', }, ], }, }) - livePhotoVideoId: (string | number)[] = []; + livePhotoVideoId: (string | null)[] = []; @ApiProperty() description: TimelineAssetDescriptionDto[] = []; diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index cb438e1c6d..e616118f2f 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -255,8 +255,23 @@ order by -- AssetRepository.getTimeBucket select - "assets".*, - to_json("exif") as "exifInfo", + "assets"."id" as "id", + "assets"."ownerId", + "assets"."status", + "deletedAt", + "type", + "duration", + "isFavorite", + "isArchived", + "thumbhash", + "localDateTime", + "livePhotoVideoId", + "exif"."exifImageHeight" as "height", + "exifImageWidth" as "width", + "exif"."orientation", + "exif"."projectionType", + "exif"."city" as "city", + "exif"."country" as "country", to_json("stacked_assets") as "stack" from "assets" diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index c42770eff2..99a3c38c25 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -142,7 +142,7 @@ export class SyncService extends BaseService { updateId, data: { ...data, - checksum: hexOrBufferToBase64(checksum), + checksum: hexOrBufferToBase64(checksum)!, thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null, }, }), @@ -172,7 +172,7 @@ export class SyncService extends BaseService { updateId, data: { ...data, - checksum: hexOrBufferToBase64(checksum), + checksum: hexOrBufferToBase64(checksum)!, thumbhash: thumbhash ? hexOrBufferToBase64(thumbhash) : null, }, }), diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index 8bb698d459..1615537dee 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -75,12 +75,12 @@ export class TimelineService extends BaseService { bucketAssets.isArchived.push(item.isArchived ? 1 : 0); bucketAssets.isFavorite.push(item.isFavorite ? 1 : 0); bucketAssets.isTrashed.push(item.deletedAt === null ? 0 : 1); - bucketAssets.thumbhash.push(item.thumbhash ? hexOrBufferToBase64(item.thumbhash) : 0); + bucketAssets.thumbhash.push(hexOrBufferToBase64(item.thumbhash)); bucketAssets.localDateTime.push(item.localDateTime); - bucketAssets.stack.push(this.mapStack(item.stack) || 0); - bucketAssets.duration.push(item.duration || 0); - bucketAssets.projectionType.push(item.projectionType || 0); - bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId || 0); + bucketAssets.stack.push(this.mapStack(item.stack)); + bucketAssets.duration.push(item.duration); + bucketAssets.projectionType.push(item.projectionType); + bucketAssets.livePhotoVideoId.push(item.livePhotoVideoId); bucketAssets.isImage.push(item.type === AssetType.IMAGE ? 1 : 0); bucketAssets.isVideo.push(item.type === AssetType.VIDEO ? 1 : 0); bucketAssets.description.push({ @@ -97,7 +97,7 @@ export class TimelineService extends BaseService { mapStack(entity?: Stack | null) { if (!entity) { - return; + return null; } return { diff --git a/server/src/services/timeline.service.types.ts b/server/src/services/timeline.service.types.ts index 0ef344f21c..21ecadf847 100644 --- a/server/src/services/timeline.service.types.ts +++ b/server/src/services/timeline.service.types.ts @@ -18,11 +18,11 @@ export type TimeBucketAssets = { isTrashed: number[]; isVideo: number[]; isImage: number[]; - thumbhash: (string | number)[]; + thumbhash: (string | null)[]; localDateTime: Date[]; - stack: (TimelineStack | number)[]; - duration: (string | number)[]; - projectionType: (string | number)[]; - livePhotoVideoId: (string | number)[]; + stack: (TimelineStack | null)[]; + duration: (string | null)[]; + projectionType: (string | null)[]; + livePhotoVideoId: (string | null)[]; description: AssetDescription[]; }; diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts index 5e476f4dea..1f858df84f 100644 --- a/server/src/utils/bytes.ts +++ b/server/src/utils/bytes.ts @@ -24,7 +24,10 @@ export function asHumanReadable(bytes: number, precision = 1): string { } // 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) => { +export const hexOrBufferToBase64 = (encoded: string | Buffer | null) => { + if (!encoded) { + return null; + } if (typeof encoded === 'string') { return Buffer.from(encoded.slice(2), 'hex').toString('base64'); } diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 56a2144323..50c0173ef9 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -409,10 +409,6 @@ } }; - const assetOnFocusHandler = (asset: TimelineAsset) => { - assetInteraction.focussedAssetId = asset.id; - }; - let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash); let idsSelectedAssets = $derived(assetInteraction.selectedAssets.map(({ id }) => id)); @@ -481,7 +477,6 @@ }} onSelect={() => handleSelectAssets(toTimelineAsset(asset))} onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))} - handleFocus={() => assetOnFocusHandler(toTimelineAsset(asset))} {showArchiveIcon} asset={toTimelineAsset(asset)} selected={assetInteraction.hasSelectedAsset(asset.id)} diff --git a/web/src/lib/stores/assets-store.svelte.ts b/web/src/lib/stores/assets-store.svelte.ts index 484f305d03..80ee3dbddb 100644 --- a/web/src/lib/stores/assets-store.svelte.ts +++ b/web/src/lib/stores/assets-store.svelte.ts @@ -419,10 +419,6 @@ export class AssetBucket { }; } - #decodeString(stringOrNumber: string | number) { - return typeof stringOrNumber === 'number' ? null : (stringOrNumber as string); - } - // note - if the assets are not part of this bucket, they will not be added addAssets(bucketResponse: TimeBucketResponseDto) { const addContext = new AddContext(); @@ -432,20 +428,20 @@ export class AssetBucket { ...bucketResponse.bucketAssets.description[i], people: [], }, - duration: this.#decodeString(bucketResponse.bucketAssets.duration[i]), + duration: bucketResponse.bucketAssets.duration[i], id: bucketResponse.bucketAssets.id[i], isArchived: !!bucketResponse.bucketAssets.isArchived[i], isFavorite: !!bucketResponse.bucketAssets.isFavorite[i], isImage: !!bucketResponse.bucketAssets.isImage[i], isTrashed: !!bucketResponse.bucketAssets.isTrashed[i], isVideo: !!bucketResponse.bucketAssets.isVideo[i], - livePhotoVideoId: this.#decodeString(bucketResponse.bucketAssets.livePhotoVideoId[i]), + livePhotoVideoId: bucketResponse.bucketAssets.livePhotoVideoId[i], localDateTime: bucketResponse.bucketAssets.localDateTime[i], ownerId: bucketResponse.bucketAssets.ownerId[i], - projectionType: this.#decodeString(bucketResponse.bucketAssets.projectionType[i]), + projectionType: bucketResponse.bucketAssets.projectionType[i], ratio: bucketResponse.bucketAssets.ratio[i], stack: bucketResponse.bucketAssets.stack[i], - thumbhash: this.#decodeString(bucketResponse.bucketAssets.thumbhash[i]), + thumbhash: bucketResponse.bucketAssets.thumbhash[i], }; this.addTimelineAsset(timelineAsset, addContext); }