From e33fd40b4cd5b8fe48637e7fd00e863edc8a15ea Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 20 Feb 2024 21:13:43 -0500 Subject: [PATCH 01/59] fix(server): quote database name in migration (#7277) quote database name --- .../infra/migrations/1707000751533-AddVectorsToSearchPath.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts b/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts index e83e4b4fb0..11c84cf970 100644 --- a/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts +++ b/server/src/infra/migrations/1707000751533-AddVectorsToSearchPath.ts @@ -4,11 +4,11 @@ export class AddVectorsToSearchPath1707000751533 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { const res = await queryRunner.query(`SELECT current_database() as db`); const databaseName = res[0]['db']; - await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public, vectors`); + await queryRunner.query(`ALTER DATABASE "${databaseName}" SET search_path TO "$user", public, vectors`); } public async down(queryRunner: QueryRunner): Promise { const databaseName = await queryRunner.query(`SELECT current_database()`); - await queryRunner.query(`ALTER DATABASE ${databaseName} SET search_path TO "$user", public`); + await queryRunner.query(`ALTER DATABASE "${databaseName}" SET search_path TO "$user", public`); } } From bb5236ae65bb94160c4f6fd5db2237854b175463 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 20 Feb 2024 20:44:34 -0600 Subject: [PATCH 02/59] fix(server): not in album filter with context search (#7275) --- mobile/openapi/doc/SmartSearchDto.md | 1 + .../openapi/lib/model/smart_search_dto.dart | 19 ++++++++++++++++++- .../openapi/test/smart_search_dto_test.dart | 5 +++++ open-api/immich-openapi-specs.json | 3 +++ open-api/typescript-sdk/axios-client/api.ts | 6 ++++++ open-api/typescript-sdk/fetch-client.ts | 1 + server/src/domain/search/dto/search.dto.ts | 6 +++--- 7 files changed, 37 insertions(+), 4 deletions(-) diff --git a/mobile/openapi/doc/SmartSearchDto.md b/mobile/openapi/doc/SmartSearchDto.md index 5d34143df2..fd9bc35490 100644 --- a/mobile/openapi/doc/SmartSearchDto.md +++ b/mobile/openapi/doc/SmartSearchDto.md @@ -18,6 +18,7 @@ Name | Type | Description | Notes **isExternal** | **bool** | | [optional] **isFavorite** | **bool** | | [optional] **isMotion** | **bool** | | [optional] +**isNotInAlbum** | **bool** | | [optional] **isOffline** | **bool** | | [optional] **isReadOnly** | **bool** | | [optional] **isVisible** | **bool** | | [optional] diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index b82a3345fb..269a071020 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -23,6 +23,7 @@ class SmartSearchDto { this.isExternal, this.isFavorite, this.isMotion, + this.isNotInAlbum, this.isOffline, this.isReadOnly, this.isVisible, @@ -126,6 +127,14 @@ class SmartSearchDto { /// bool? isMotion; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? isNotInAlbum; + /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -300,6 +309,7 @@ class SmartSearchDto { other.isExternal == isExternal && other.isFavorite == isFavorite && other.isMotion == isMotion && + other.isNotInAlbum == isNotInAlbum && other.isOffline == isOffline && other.isReadOnly == isReadOnly && other.isVisible == isVisible && @@ -335,6 +345,7 @@ class SmartSearchDto { (isExternal == null ? 0 : isExternal!.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + (isMotion == null ? 0 : isMotion!.hashCode) + + (isNotInAlbum == null ? 0 : isNotInAlbum!.hashCode) + (isOffline == null ? 0 : isOffline!.hashCode) + (isReadOnly == null ? 0 : isReadOnly!.hashCode) + (isVisible == null ? 0 : isVisible!.hashCode) + @@ -358,7 +369,7 @@ class SmartSearchDto { (withExif == null ? 0 : withExif!.hashCode); @override - String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; + String toString() => 'SmartSearchDto[city=$city, country=$country, createdAfter=$createdAfter, createdBefore=$createdBefore, deviceId=$deviceId, isArchived=$isArchived, isEncoded=$isEncoded, isExternal=$isExternal, isFavorite=$isFavorite, isMotion=$isMotion, isNotInAlbum=$isNotInAlbum, isOffline=$isOffline, isReadOnly=$isReadOnly, isVisible=$isVisible, lensModel=$lensModel, libraryId=$libraryId, make=$make, model=$model, page=$page, query=$query, size=$size, state=$state, takenAfter=$takenAfter, takenBefore=$takenBefore, trashedAfter=$trashedAfter, trashedBefore=$trashedBefore, type=$type, updatedAfter=$updatedAfter, updatedBefore=$updatedBefore, withArchived=$withArchived, withDeleted=$withDeleted, withExif=$withExif]'; Map toJson() { final json = {}; @@ -412,6 +423,11 @@ class SmartSearchDto { } else { // json[r'isMotion'] = null; } + if (this.isNotInAlbum != null) { + json[r'isNotInAlbum'] = this.isNotInAlbum; + } else { + // json[r'isNotInAlbum'] = null; + } if (this.isOffline != null) { json[r'isOffline'] = this.isOffline; } else { @@ -534,6 +550,7 @@ class SmartSearchDto { isExternal: mapValueOfType(json, r'isExternal'), isFavorite: mapValueOfType(json, r'isFavorite'), isMotion: mapValueOfType(json, r'isMotion'), + isNotInAlbum: mapValueOfType(json, r'isNotInAlbum'), isOffline: mapValueOfType(json, r'isOffline'), isReadOnly: mapValueOfType(json, r'isReadOnly'), isVisible: mapValueOfType(json, r'isVisible'), diff --git a/mobile/openapi/test/smart_search_dto_test.dart b/mobile/openapi/test/smart_search_dto_test.dart index 858c7769c8..84a85cf208 100644 --- a/mobile/openapi/test/smart_search_dto_test.dart +++ b/mobile/openapi/test/smart_search_dto_test.dart @@ -66,6 +66,11 @@ void main() { // TODO }); + // bool isNotInAlbum + test('to test the property `isNotInAlbum`', () async { + // TODO + }); + // bool isOffline test('to test the property `isOffline`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6870e140ca..790fe8e8ec 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9435,6 +9435,9 @@ "isMotion": { "type": "boolean" }, + "isNotInAlbum": { + "type": "boolean" + }, "isOffline": { "type": "boolean" }, diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index 622820c752..e57a95a127 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -3880,6 +3880,12 @@ export interface SmartSearchDto { * @memberof SmartSearchDto */ 'isMotion'?: boolean; + /** + * + * @type {boolean} + * @memberof SmartSearchDto + */ + 'isNotInAlbum'?: boolean; /** * * @type {boolean} diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index 9b3a359863..2c14eb4fae 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -656,6 +656,7 @@ export type SmartSearchDto = { isExternal?: boolean; isFavorite?: boolean; isMotion?: boolean; + isNotInAlbum?: boolean; isOffline?: boolean; isReadOnly?: boolean; isVisible?: boolean; diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 5aa73433d9..519e39fd2e 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -118,6 +118,9 @@ class BaseSearchDto { @Type(() => Number) @Optional() size?: number; + + @QueryBoolean({ optional: true }) + isNotInAlbum?: boolean; } export class MetadataSearchDto extends BaseSearchDto { @@ -170,9 +173,6 @@ export class MetadataSearchDto extends BaseSearchDto { @ApiProperty({ enumName: 'AssetOrder', enum: AssetOrder }) order?: AssetOrder; - @QueryBoolean({ optional: true }) - isNotInAlbum?: boolean; - @Optional() personIds?: string[]; } From eb73f6605bc6260c358bf1a988538fc96ebbd73e Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 20 Feb 2024 22:59:26 -0500 Subject: [PATCH 03/59] fix(server): don't return archived assets by default (#7278) * don't show archived results by default * fix e2e * generate sql * set default in dto --------- Co-authored-by: Alex Tran --- mobile/openapi/doc/AssetApi.md | 2 +- mobile/openapi/doc/MetadataSearchDto.md | 2 +- mobile/openapi/doc/SmartSearchDto.md | 2 +- .../lib/model/metadata_search_dto.dart | 18 ++----- .../openapi/lib/model/smart_search_dto.dart | 18 ++----- .../test/metadata_search_dto_test.dart | 2 +- .../openapi/test/smart_search_dto_test.dart | 2 +- open-api/immich-openapi-specs.json | 3 ++ server/e2e/api/specs/asset.e2e-spec.ts | 51 ++++++++++++++----- server/src/domain/search/dto/search.dto.ts | 1 + server/src/infra/infra.utils.ts | 2 +- server/src/infra/sql/asset.repository.sql | 2 +- server/src/infra/sql/search.repository.sql | 14 +++-- 13 files changed, 68 insertions(+), 51 deletions(-) diff --git a/mobile/openapi/doc/AssetApi.md b/mobile/openapi/doc/AssetApi.md index 5691965bc7..93b758a595 100644 --- a/mobile/openapi/doc/AssetApi.md +++ b/mobile/openapi/doc/AssetApi.md @@ -1153,7 +1153,7 @@ Name | Type | Description | Notes **updatedAfter** | **DateTime**| | [optional] **updatedBefore** | **DateTime**| | [optional] **webpPath** | **String**| | [optional] - **withArchived** | **bool**| | [optional] + **withArchived** | **bool**| | [optional] [default to false] **withDeleted** | **bool**| | [optional] **withExif** | **bool**| | [optional] **withPeople** | **bool**| | [optional] diff --git a/mobile/openapi/doc/MetadataSearchDto.md b/mobile/openapi/doc/MetadataSearchDto.md index bfbf81749e..d1d098fb0e 100644 --- a/mobile/openapi/doc/MetadataSearchDto.md +++ b/mobile/openapi/doc/MetadataSearchDto.md @@ -46,7 +46,7 @@ Name | Type | Description | Notes **updatedAfter** | [**DateTime**](DateTime.md) | | [optional] **updatedBefore** | [**DateTime**](DateTime.md) | | [optional] **webpPath** | **String** | | [optional] -**withArchived** | **bool** | | [optional] +**withArchived** | **bool** | | [optional] [default to false] **withDeleted** | **bool** | | [optional] **withExif** | **bool** | | [optional] **withPeople** | **bool** | | [optional] diff --git a/mobile/openapi/doc/SmartSearchDto.md b/mobile/openapi/doc/SmartSearchDto.md index fd9bc35490..d4ec1a70f6 100644 --- a/mobile/openapi/doc/SmartSearchDto.md +++ b/mobile/openapi/doc/SmartSearchDto.md @@ -37,7 +37,7 @@ Name | Type | Description | Notes **type** | [**AssetTypeEnum**](AssetTypeEnum.md) | | [optional] **updatedAfter** | [**DateTime**](DateTime.md) | | [optional] **updatedBefore** | [**DateTime**](DateTime.md) | | [optional] -**withArchived** | **bool** | | [optional] +**withArchived** | **bool** | | [optional] [default to false] **withDeleted** | **bool** | | [optional] **withExif** | **bool** | | [optional] diff --git a/mobile/openapi/lib/model/metadata_search_dto.dart b/mobile/openapi/lib/model/metadata_search_dto.dart index 47756cd527..86a2856e66 100644 --- a/mobile/openapi/lib/model/metadata_search_dto.dart +++ b/mobile/openapi/lib/model/metadata_search_dto.dart @@ -51,7 +51,7 @@ class MetadataSearchDto { this.updatedAfter, this.updatedBefore, this.webpPath, - this.withArchived, + this.withArchived = false, this.withDeleted, this.withExif, this.withPeople, @@ -356,13 +356,7 @@ class MetadataSearchDto { /// String? webpPath; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? withArchived; + bool withArchived; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -483,7 +477,7 @@ class MetadataSearchDto { (updatedAfter == null ? 0 : updatedAfter!.hashCode) + (updatedBefore == null ? 0 : updatedBefore!.hashCode) + (webpPath == null ? 0 : webpPath!.hashCode) + - (withArchived == null ? 0 : withArchived!.hashCode) + + (withArchived.hashCode) + (withDeleted == null ? 0 : withDeleted!.hashCode) + (withExif == null ? 0 : withExif!.hashCode) + (withPeople == null ? 0 : withPeople!.hashCode) + @@ -680,11 +674,7 @@ class MetadataSearchDto { } else { // json[r'webpPath'] = null; } - if (this.withArchived != null) { json[r'withArchived'] = this.withArchived; - } else { - // json[r'withArchived'] = null; - } if (this.withDeleted != null) { json[r'withDeleted'] = this.withDeleted; } else { @@ -756,7 +746,7 @@ class MetadataSearchDto { updatedAfter: mapDateTime(json, r'updatedAfter', r''), updatedBefore: mapDateTime(json, r'updatedBefore', r''), webpPath: mapValueOfType(json, r'webpPath'), - withArchived: mapValueOfType(json, r'withArchived'), + withArchived: mapValueOfType(json, r'withArchived') ?? false, withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), withPeople: mapValueOfType(json, r'withPeople'), diff --git a/mobile/openapi/lib/model/smart_search_dto.dart b/mobile/openapi/lib/model/smart_search_dto.dart index 269a071020..664850db82 100644 --- a/mobile/openapi/lib/model/smart_search_dto.dart +++ b/mobile/openapi/lib/model/smart_search_dto.dart @@ -42,7 +42,7 @@ class SmartSearchDto { this.type, this.updatedAfter, this.updatedBefore, - this.withArchived, + this.withArchived = false, this.withDeleted, this.withExif, }); @@ -273,13 +273,7 @@ class SmartSearchDto { /// DateTime? updatedBefore; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? withArchived; + bool withArchived; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -364,7 +358,7 @@ class SmartSearchDto { (type == null ? 0 : type!.hashCode) + (updatedAfter == null ? 0 : updatedAfter!.hashCode) + (updatedBefore == null ? 0 : updatedBefore!.hashCode) + - (withArchived == null ? 0 : withArchived!.hashCode) + + (withArchived.hashCode) + (withDeleted == null ? 0 : withDeleted!.hashCode) + (withExif == null ? 0 : withExif!.hashCode); @@ -514,11 +508,7 @@ class SmartSearchDto { } else { // json[r'updatedBefore'] = null; } - if (this.withArchived != null) { json[r'withArchived'] = this.withArchived; - } else { - // json[r'withArchived'] = null; - } if (this.withDeleted != null) { json[r'withDeleted'] = this.withDeleted; } else { @@ -569,7 +559,7 @@ class SmartSearchDto { type: AssetTypeEnum.fromJson(json[r'type']), updatedAfter: mapDateTime(json, r'updatedAfter', r''), updatedBefore: mapDateTime(json, r'updatedBefore', r''), - withArchived: mapValueOfType(json, r'withArchived'), + withArchived: mapValueOfType(json, r'withArchived') ?? false, withDeleted: mapValueOfType(json, r'withDeleted'), withExif: mapValueOfType(json, r'withExif'), ); diff --git a/mobile/openapi/test/metadata_search_dto_test.dart b/mobile/openapi/test/metadata_search_dto_test.dart index f1635de4e0..f817b7da74 100644 --- a/mobile/openapi/test/metadata_search_dto_test.dart +++ b/mobile/openapi/test/metadata_search_dto_test.dart @@ -206,7 +206,7 @@ void main() { // TODO }); - // bool withArchived + // bool withArchived (default value: false) test('to test the property `withArchived`', () async { // TODO }); diff --git a/mobile/openapi/test/smart_search_dto_test.dart b/mobile/openapi/test/smart_search_dto_test.dart index 84a85cf208..4db3ac0808 100644 --- a/mobile/openapi/test/smart_search_dto_test.dart +++ b/mobile/openapi/test/smart_search_dto_test.dart @@ -161,7 +161,7 @@ void main() { // TODO }); - // bool withArchived + // bool withArchived (default value: false) test('to test the property `withArchived`', () async { // TODO }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 790fe8e8ec..d32d5b820d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -2463,6 +2463,7 @@ "required": false, "in": "query", "schema": { + "default": false, "type": "boolean" } }, @@ -8429,6 +8430,7 @@ "type": "string" }, "withArchived": { + "default": false, "type": "boolean" }, "withDeleted": { @@ -9500,6 +9502,7 @@ "type": "string" }, "withArchived": { + "default": false, "type": "boolean" }, "withDeleted": { diff --git a/server/e2e/api/specs/asset.e2e-spec.ts b/server/e2e/api/specs/asset.e2e-spec.ts index 5993a70400..7789881206 100644 --- a/server/e2e/api/specs/asset.e2e-spec.ts +++ b/server/e2e/api/specs/asset.e2e-spec.ts @@ -50,6 +50,7 @@ describe(`${AssetController.name} (e2e)`, () => { let asset3: AssetResponseDto; let asset4: AssetResponseDto; let asset5: AssetResponseDto; + let asset6: AssetResponseDto; const createAsset = async ( loginResponse: LoginResponseDto, @@ -96,12 +97,11 @@ describe(`${AssetController.name} (e2e)`, () => { beforeEach(async () => { await testApp.reset({ entities: [AssetEntity, AssetStackEntity] }); - [asset1, asset2, asset3, asset4, asset5] = await Promise.all([ + [asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([ createAsset(user1, new Date('1970-01-01')), createAsset(user1, new Date('1970-02-10')), createAsset(user1, new Date('1970-02-11'), { isFavorite: true, - isArchived: true, isExternal: true, isReadOnly: true, type: AssetType.VIDEO, @@ -118,6 +118,9 @@ describe(`${AssetController.name} (e2e)`, () => { createAsset(user1, new Date('1970-01-01'), { deletedAt: yesterday.toJSDate(), }), + createAsset(user1, new Date('1970-02-11'), { + isArchived: true, + }), ]); await assetRepository.upsertExif({ @@ -275,14 +278,14 @@ describe(`${AssetController.name} (e2e)`, () => { should: 'should search by isArchived (true)', deferred: () => ({ query: { isArchived: true }, - assets: [asset3], + assets: [asset6], }), }, { should: 'should search by isArchived (false)', deferred: () => ({ query: { isArchived: false }, - assets: [asset2, asset1], + assets: [asset3, asset2, asset1], }), }, { @@ -313,6 +316,20 @@ describe(`${AssetController.name} (e2e)`, () => { assets: [asset3], }), }, + { + should: 'should search by withArchived (true)', + deferred: () => ({ + query: { withArchived: true }, + assets: [asset3, asset6, asset2, asset1], + }), + }, + { + should: 'should search by withArchived (false)', + deferred: () => ({ + query: { withArchived: false }, + assets: [asset3, asset2, asset1], + }), + }, { should: 'should search by createdBefore', deferred: () => ({ @@ -902,7 +919,7 @@ describe(`${AssetController.name} (e2e)`, () => { .get('/asset/statistics') .set('Authorization', `Bearer ${user1.accessToken}`); - expect(body).toEqual({ images: 5, videos: 1, total: 6 }); + expect(body).toEqual({ images: 6, videos: 1, total: 7 }); expect(status).toBe(200); }); @@ -923,7 +940,7 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isArchived: true }); expect(status).toBe(200); - expect(body).toEqual({ images: 2, videos: 1, total: 3 }); + expect(body).toEqual({ images: 3, videos: 0, total: 3 }); }); it('should return stats of all favored and archived assets', async () => { @@ -933,7 +950,7 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isFavorite: true, isArchived: true }); expect(status).toBe(200); - expect(body).toEqual({ images: 1, videos: 1, total: 2 }); + expect(body).toEqual({ images: 1, videos: 0, total: 1 }); }); it('should return stats of all assets neither favored nor archived', async () => { @@ -1041,7 +1058,7 @@ describe(`${AssetController.name} (e2e)`, () => { expect.arrayContaining([ { count: 1, timeBucket: '2023-11-01T00:00:00.000Z' }, { count: 1, timeBucket: '1970-01-01T00:00:00.000Z' }, - { count: 1, timeBucket: '1970-02-01T00:00:00.000Z' }, + { count: 2, timeBucket: '1970-02-01T00:00:00.000Z' }, ]), ); }); @@ -1198,8 +1215,13 @@ describe(`${AssetController.name} (e2e)`, () => { .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body).toEqual(expect.arrayContaining([expect.objectContaining({ id: asset2.id })])); + expect(body).toHaveLength(2); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: asset2.id }), + expect.objectContaining({ id: asset3.id }), + ]), + ); }); it('should get all map markers', async () => { @@ -1209,8 +1231,13 @@ describe(`${AssetController.name} (e2e)`, () => { .query({ isArchived: false }); expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body).toEqual([expect.objectContaining({ id: asset2.id })]); + expect(body).toHaveLength(2); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: asset2.id }), + expect.objectContaining({ id: asset3.id }), + ]), + ); }); }); diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/domain/search/dto/search.dto.ts index 519e39fd2e..4f2aa18199 100644 --- a/server/src/domain/search/dto/search.dto.ts +++ b/server/src/domain/search/dto/search.dto.ts @@ -23,6 +23,7 @@ class BaseSearchDto { isArchived?: boolean; @QueryBoolean({ optional: true }) + @ApiProperty({ default: false }) withArchived?: boolean; @QueryBoolean({ optional: true }) diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index ab7d744317..1538958e0f 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -183,7 +183,7 @@ export function searchAssetBuilder( _.omitBy( { ...status, - isArchived: isArchived ?? withArchived, + isArchived: isArchived ?? (withArchived ? undefined : false), encodedVideoPath: isEncoded ? Not(IsNull()) : undefined, livePhotoVideoId: isMotion ? Not(IsNull()) : undefined, }, diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/infra/sql/asset.repository.sql index d971129e75..e5cf6771fd 100644 --- a/server/src/infra/sql/asset.repository.sql +++ b/server/src/infra/sql/asset.repository.sql @@ -434,7 +434,7 @@ WHERE AND 1 = 1 AND "asset"."ownerId" IN ($2) AND 1 = 1 - AND 1 = 1 + AND "asset"."isArchived" = $3 ) AND ("asset"."deletedAt" IS NULL) ORDER BY diff --git a/server/src/infra/sql/search.repository.sql b/server/src/infra/sql/search.repository.sql index ebae46f65b..a21697c268 100644 --- a/server/src/infra/sql/search.repository.sql +++ b/server/src/infra/sql/search.repository.sql @@ -79,7 +79,10 @@ FROM AND "exifInfo"."lensModel" = $2 AND 1 = 1 AND 1 = 1 - AND "asset"."isFavorite" = $3 + AND ( + "asset"."isFavorite" = $3 + AND "asset"."isArchived" = $4 + ) AND ( "stack"."primaryAssetId" = "asset"."id" OR "asset"."stackId" IS NULL @@ -177,16 +180,19 @@ WHERE AND "exifInfo"."lensModel" = $2 AND 1 = 1 AND 1 = 1 - AND "asset"."isFavorite" = $3 + AND ( + "asset"."isFavorite" = $3 + AND "asset"."isArchived" = $4 + ) AND ( "stack"."primaryAssetId" = "asset"."id" OR "asset"."stackId" IS NULL ) - AND "asset"."ownerId" IN ($4) + AND "asset"."ownerId" IN ($5) ) AND ("asset"."deletedAt" IS NULL) ORDER BY - "search"."embedding" <= > $5 ASC + "search"."embedding" <= > $6 ASC LIMIT 101 COMMIT From 7c34d0595e90d5abaf250d3a7395f7d0399b51a0 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Wed, 21 Feb 2024 04:02:43 +0000 Subject: [PATCH 04/59] Version v1.95.1 --- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/axios-client/api.ts | 2 +- open-api/typescript-sdk/axios-client/base.ts | 2 +- open-api/typescript-sdk/axios-client/common.ts | 2 +- open-api/typescript-sdk/axios-client/configuration.ts | 2 +- open-api/typescript-sdk/axios-client/index.ts | 2 +- open-api/typescript-sdk/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 4 ++-- web/package.json | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 7683a8d066..750ca65f26 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.95.0" +version = "1.95.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 0ac765a57d..fc63990518 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 122, - "android.injected.version.name" => "1.95.0", + "android.injected.version.code" => 123, + "android.injected.version.name" => "1.95.1", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d26e87c968..d5a42ad485 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.95.0" + version_number: "1.95.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index a2b155bcd9..5dd6d196d2 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.95.0 +- API version: 1.95.1 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9788077650..47a4d3805e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.95.0+122 +version: 1.95.1+123 isar_version: &isar_version 3.1.0+1 environment: diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index d32d5b820d..87f0fb4158 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6414,7 +6414,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.95.0", + "version": "1.95.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index e57a95a127..bef5ceab1b 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.0 + * The version of the OpenAPI document: 1.95.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/base.ts b/open-api/typescript-sdk/axios-client/base.ts index d16f428a39..d353309457 100644 --- a/open-api/typescript-sdk/axios-client/base.ts +++ b/open-api/typescript-sdk/axios-client/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.0 + * The version of the OpenAPI document: 1.95.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/common.ts b/open-api/typescript-sdk/axios-client/common.ts index 743c3cf16b..120ebca552 100644 --- a/open-api/typescript-sdk/axios-client/common.ts +++ b/open-api/typescript-sdk/axios-client/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.0 + * The version of the OpenAPI document: 1.95.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/configuration.ts b/open-api/typescript-sdk/axios-client/configuration.ts index 0e2dec06f5..cd67c859c7 100644 --- a/open-api/typescript-sdk/axios-client/configuration.ts +++ b/open-api/typescript-sdk/axios-client/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.0 + * The version of the OpenAPI document: 1.95.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/index.ts b/open-api/typescript-sdk/axios-client/index.ts index ccee244935..0918c8124d 100644 --- a/open-api/typescript-sdk/axios-client/index.ts +++ b/open-api/typescript-sdk/axios-client/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.0 + * The version of the OpenAPI document: 1.95.1 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index 2c14eb4fae..d7ecb906e3 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.95.0 + * 1.95.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index ae129549b3..332edfbe9b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.95.0", + "version": "1.95.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.95.0", + "version": "1.95.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", diff --git a/server/package.json b/server/package.json index 5ea2a345df..35eebde8a9 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.95.0", + "version": "1.95.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 654c7154cb..aa864c0a30 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.1.0", + "version": "1.1.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", diff --git a/web/package.json b/web/package.json index 361849eb73..1542acc2dc 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.1.0", + "version": "1.1.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 397570ad1a81afc7c691febeb47770108c61be94 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 21 Feb 2024 00:25:30 -0500 Subject: [PATCH 05/59] chore(server): change transcode default to accept all supported audio codecs (#7283) * change transcode defaults * don't untick accepted audio codecs * no need to change the transcode policy * fix tests * remove log --- server/src/domain/media/media.service.spec.ts | 82 ++++++++++--------- .../system-config/system-config.core.ts | 2 +- .../system-config.service.spec.ts | 2 +- .../infra/entities/system-config.entity.ts | 2 +- .../settings/ffmpeg/ffmpeg-settings.svelte | 5 +- 5 files changed, 50 insertions(+), 43 deletions(-) diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index aa48568b90..f4c9aa53e7 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -1,5 +1,6 @@ import { AssetType, + AudioCodec, Colorspace, ExifEntity, SystemConfigKey, @@ -475,7 +476,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -542,7 +543,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -571,7 +572,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -629,7 +630,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -706,7 +707,10 @@ describe(MediaService.name, () => { it('should copy video stream when video matches target', async () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); - configMock.load.mockResolvedValue([{ key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC }]); + configMock.load.mockResolvedValue([ + { key: SystemConfigKey.FFMPEG_TARGET_VIDEO_CODEC, value: VideoCodec.HEVC }, + { key: SystemConfigKey.FFMPEG_ACCEPTED_AUDIO_CODECS, value: [AudioCodec.AAC] }, + ]); assetMock.getByIds.mockResolvedValue([assetStub.video]); await sut.handleVideoConversion({ id: assetStub.video.id }); expect(mediaMock.transcode).toHaveBeenCalledWith( @@ -770,7 +774,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -836,7 +840,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -868,7 +872,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -897,7 +901,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -928,7 +932,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v vp9', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -962,7 +966,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v vp9', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -994,7 +998,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v vp9', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1026,7 +1030,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v vp9', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1057,7 +1061,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v vp9', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1087,7 +1091,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1117,7 +1121,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1147,7 +1151,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v hevc', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1181,7 +1185,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v hevc', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1248,7 +1252,7 @@ describe(MediaService.name, () => { '-rc-lookahead 20', '-i_qfactor 0.75', `-c:v h264_nvenc`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1286,7 +1290,7 @@ describe(MediaService.name, () => { '-rc-lookahead 20', '-i_qfactor 0.75', `-c:v h264_nvenc`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1320,7 +1324,7 @@ describe(MediaService.name, () => { '-rc-lookahead 20', '-i_qfactor 0.75', `-c:v h264_nvenc`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1355,7 +1359,7 @@ describe(MediaService.name, () => { '-rc-lookahead 20', '-i_qfactor 0.75', `-c:v h264_nvenc`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1386,7 +1390,7 @@ describe(MediaService.name, () => { '-rc-lookahead 20', '-i_qfactor 0.75', `-c:v h264_nvenc`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1418,7 +1422,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ `-c:v h264_qsv`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1455,7 +1459,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device qsv=hw,child_device=/dev/dri/renderD128', '-filter_hw_device hw'], outputOptions: [ `-c:v h264_qsv`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1491,7 +1495,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ `-c:v h264_qsv`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1524,7 +1528,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device qsv=hw', '-filter_hw_device hw'], outputOptions: [ `-c:v vp9_qsv`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1568,7 +1572,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1600,7 +1604,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1634,7 +1638,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1664,7 +1668,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/card1', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1690,7 +1694,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD129', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1724,7 +1728,7 @@ describe(MediaService.name, () => { inputOptions: ['-init_hw_device vaapi=accel:/dev/dri/renderD128', '-filter_hw_device accel'], outputOptions: [ `-c:v h264_vaapi`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1757,7 +1761,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1798,7 +1802,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ `-c:v hevc_rkmpp_encoder`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1838,7 +1842,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ `-c:v h264_rkmpp_encoder`, - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1872,7 +1876,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1899,7 +1903,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', @@ -1926,7 +1930,7 @@ describe(MediaService.name, () => { inputOptions: [], outputOptions: [ '-c:v h264', - '-c:a aac', + '-c:a copy', '-movflags faststart', '-fps_mode passthrough', '-map 0:0', diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index 1591e87d63..eb661133b2 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -33,7 +33,7 @@ export const defaults = Object.freeze({ targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], targetAudioCodec: AudioCodec.AAC, - acceptedAudioCodecs: [AudioCodec.AAC], + acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS], targetResolution: '720', maxBitrate: '0', bframes: -1, diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index 8addc63a0f..ec0b4b8f4f 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -43,7 +43,7 @@ const updatedConfig = Object.freeze({ threads: 0, preset: 'ultrafast', targetAudioCodec: AudioCodec.AAC, - acceptedAudioCodecs: [AudioCodec.AAC], + acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS], targetResolution: '720', targetVideoCodec: VideoCodec.H264, acceptedVideoCodecs: [VideoCodec.H264], diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 1515630cea..8307a0328e 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -7,7 +7,7 @@ export class SystemConfigEntity { key!: SystemConfigKey; @Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } }) - value!: T; + value!: T | T[]; } export type SystemConfigValue = string | number | boolean; diff --git a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte index cd73b77f44..496d579cae 100644 --- a/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte +++ b/web/src/lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte @@ -90,7 +90,10 @@ ]} name="acodec" isEdited={config.ffmpeg.targetAudioCodec !== savedConfig.ffmpeg.targetAudioCodec} - on:select={() => (config.ffmpeg.acceptedAudioCodecs = [config.ffmpeg.targetAudioCodec])} + on:select={() => + config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec) + ? null + : config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec)} /> Date: Wed, 21 Feb 2024 01:12:38 -0500 Subject: [PATCH 06/59] chore(deps): update machine-learning (#7225) --- machine-learning/poetry.lock | 376 +++++++++++++++++------------------ 1 file changed, 188 insertions(+), 188 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 160b0b8e46..c8ae6c7410 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -64,33 +64,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.1.1" +version = "24.2.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2588021038bd5ada078de606f2a804cadd0a3cc6a79cb3e9bb3a8bf581325a4c"}, - {file = "black-24.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1a95915c98d6e32ca43809d46d932e2abc5f1f7d582ffbe65a5b4d1588af7445"}, - {file = "black-24.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2fa6a0e965779c8f2afb286f9ef798df770ba2b6cee063c650b96adec22c056a"}, - {file = "black-24.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:5242ecd9e990aeb995b6d03dc3b2d112d4a78f2083e5a8e86d566340ae80fec4"}, - {file = "black-24.1.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fc1ec9aa6f4d98d022101e015261c056ddebe3da6a8ccfc2c792cbe0349d48b7"}, - {file = "black-24.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0269dfdea12442022e88043d2910429bed717b2d04523867a85dacce535916b8"}, - {file = "black-24.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3d64db762eae4a5ce04b6e3dd745dcca0fb9560eb931a5be97472e38652a161"}, - {file = "black-24.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:5d7b06ea8816cbd4becfe5f70accae953c53c0e53aa98730ceccb0395520ee5d"}, - {file = "black-24.1.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e2c8dfa14677f90d976f68e0c923947ae68fa3961d61ee30976c388adc0b02c8"}, - {file = "black-24.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a21725862d0e855ae05da1dd25e3825ed712eaaccef6b03017fe0853a01aa45e"}, - {file = "black-24.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:07204d078e25327aad9ed2c64790d681238686bce254c910de640c7cc4fc3aa6"}, - {file = "black-24.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:a83fe522d9698d8f9a101b860b1ee154c1d25f8a82ceb807d319f085b2627c5b"}, - {file = "black-24.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:08b34e85170d368c37ca7bf81cf67ac863c9d1963b2c1780c39102187ec8dd62"}, - {file = "black-24.1.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7258c27115c1e3b5de9ac6c4f9957e3ee2c02c0b39222a24dc7aa03ba0e986f5"}, - {file = "black-24.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40657e1b78212d582a0edecafef133cf1dd02e6677f539b669db4746150d38f6"}, - {file = "black-24.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:e298d588744efda02379521a19639ebcd314fba7a49be22136204d7ed1782717"}, - {file = "black-24.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:34afe9da5056aa123b8bfda1664bfe6fb4e9c6f311d8e4a6eb089da9a9173bf9"}, - {file = "black-24.1.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:854c06fb86fd854140f37fb24dbf10621f5dab9e3b0c29a690ba595e3d543024"}, - {file = "black-24.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3897ae5a21ca132efa219c029cce5e6bfc9c3d34ed7e892113d199c0b1b444a2"}, - {file = "black-24.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:ecba2a15dfb2d97105be74bbfe5128bc5e9fa8477d8c46766505c1dda5883aac"}, - {file = "black-24.1.1-py3-none-any.whl", hash = "sha256:5cdc2e2195212208fbcae579b931407c1fa9997584f0a415421748aeafff1168"}, - {file = "black-24.1.1.tar.gz", hash = "sha256:48b5760dcbfe5cf97fd4fba23946681f3a81514c6ab8a45b50da67ac8fbc6c7b"}, + {file = "black-24.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6981eae48b3b33399c8757036c7f5d48a535b962a7c2310d19361edeef64ce29"}, + {file = "black-24.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d533d5e3259720fdbc1b37444491b024003e012c5173f7d06825a77508085430"}, + {file = "black-24.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61a0391772490ddfb8a693c067df1ef5227257e72b0e4108482b8d41b5aee13f"}, + {file = "black-24.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:992e451b04667116680cb88f63449267c13e1ad134f30087dec8527242e9862a"}, + {file = "black-24.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:163baf4ef40e6897a2a9b83890e59141cc8c2a98f2dda5080dc15c00ee1e62cd"}, + {file = "black-24.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e37c99f89929af50ffaf912454b3e3b47fd64109659026b678c091a4cd450fb2"}, + {file = "black-24.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9de21bafcba9683853f6c96c2d515e364aee631b178eaa5145fc1c61a3cc92"}, + {file = "black-24.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:9db528bccb9e8e20c08e716b3b09c6bdd64da0dd129b11e160bf082d4642ac23"}, + {file = "black-24.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d84f29eb3ee44859052073b7636533ec995bd0f64e2fb43aeceefc70090e752b"}, + {file = "black-24.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e08fb9a15c914b81dd734ddd7fb10513016e5ce7e6704bdd5e1251ceee51ac9"}, + {file = "black-24.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:810d445ae6069ce64030c78ff6127cd9cd178a9ac3361435708b907d8a04c693"}, + {file = "black-24.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:ba15742a13de85e9b8f3239c8f807723991fbfae24bad92d34a2b12e81904982"}, + {file = "black-24.2.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7e53a8c630f71db01b28cd9602a1ada68c937cbf2c333e6ed041390d6968faf4"}, + {file = "black-24.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:93601c2deb321b4bad8f95df408e3fb3943d85012dddb6121336b8e24a0d1218"}, + {file = "black-24.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0057f800de6acc4407fe75bb147b0c2b5cbb7c3ed110d3e5999cd01184d53b0"}, + {file = "black-24.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:faf2ee02e6612577ba0181f4347bcbcf591eb122f7841ae5ba233d12c39dcb4d"}, + {file = "black-24.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:057c3dc602eaa6fdc451069bd027a1b2635028b575a6c3acfd63193ced20d9c8"}, + {file = "black-24.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:08654d0797e65f2423f850fc8e16a0ce50925f9337fb4a4a176a7aa4026e63f8"}, + {file = "black-24.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca610d29415ee1a30a3f30fab7a8f4144e9d34c89a235d81292a1edb2b55f540"}, + {file = "black-24.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:4dd76e9468d5536abd40ffbc7a247f83b2324f0c050556d9c371c2b9a9a95e31"}, + {file = "black-24.2.0-py3-none-any.whl", hash = "sha256:e8a6ae970537e67830776488bca52000eaa37fa63b9988e8c487458d9cd5ace6"}, + {file = "black-24.2.0.tar.gz", hash = "sha256:bce4f25c27c3435e4dace4815bcb2008b87e167e3bf4ee47ccdc5ce906eb4894"}, ] [package.dependencies] @@ -2101,61 +2101,61 @@ numpy = [ [[package]] name = "orjson" -version = "3.9.13" +version = "3.9.14" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.9.13-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fa6b67f8bef277c2a4aadd548d58796854e7d760964126c3209b19bccc6a74f1"}, - {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b812417199eeb169c25f67815cfb66fd8de7ff098bf57d065e8c1943a7ba5c8f"}, - {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7ccd5bd222e5041069ad9d9868ab59e6dbc53ecde8d8c82b919954fbba43b46b"}, - {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaaf80957c38e9d3f796f355a80fad945e72cd745e6b64c210e635b7043b673e"}, - {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:60da7316131185d0110a1848e9ad15311e6c8938ee0b5be8cbd7261e1d80ee8f"}, - {file = "orjson-3.9.13-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5b98cd948372f0eb219bc309dee4633db1278687161e3280d9e693b6076951d2"}, - {file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3869d65561f10071d3e7f35ae58fd377056f67d7aaed5222f318390c3ad30339"}, - {file = "orjson-3.9.13-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43fd6036b16bb6742d03dae62f7bdf8214d06dea47e4353cde7e2bd1358d186f"}, - {file = "orjson-3.9.13-cp310-none-win32.whl", hash = "sha256:0d3ba9d88e20765335260d7b25547d7c571eee2b698200f97afa7d8c7cd668fc"}, - {file = "orjson-3.9.13-cp310-none-win_amd64.whl", hash = "sha256:6e47153db080f5e87e8ba638f1a8b18995eede6b0abb93964d58cf11bcea362f"}, - {file = "orjson-3.9.13-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4584e8eb727bc431baaf1bf97e35a1d8a0109c924ec847395673dfd5f4ef6d6f"}, - {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f37f0cdd026ef777a4336e599d8194c8357fc14760c2a5ddcfdf1965d45504b"}, - {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d714595d81efab11b42bccd119977d94b25d12d3a806851ff6bfd286a4bce960"}, - {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9171e8e1a1f221953e38e84ae0abffe8759002fd8968106ee379febbb5358b33"}, - {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ab9dbdec3f13f3ea6f937564ce21651844cfbf2725099f2f490426acf683c23"}, - {file = "orjson-3.9.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:811ac076855e33e931549340288e0761873baf29276ad00f221709933c644330"}, - {file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:860d0f5b42d0c0afd73fa4177709f6e1b966ba691fcd72175affa902052a81d6"}, - {file = "orjson-3.9.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:838b898e8c1f26eb6b8d81b180981273f6f5110c76c22c384979aca854194f1b"}, - {file = "orjson-3.9.13-cp311-none-win32.whl", hash = "sha256:d3222db9df629ef3c3673124f2e05fb72bc4a320c117e953fec0d69dde82e36d"}, - {file = "orjson-3.9.13-cp311-none-win_amd64.whl", hash = "sha256:978117122ca4cc59b28af5322253017f6c5fc03dbdda78c7f4b94ae984c8dd43"}, - {file = "orjson-3.9.13-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:031df1026c7ea8303332d78711f180231e3ae8b564271fb748a03926587c5546"}, - {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd9a2101d04e85086ea6198786a3f016e45475f800712e6833e14bf9ce2832f"}, - {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:446d9ad04204e79229ae19502daeea56479e55cbc32634655d886f5a39e91b44"}, - {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b57c0954a9fdd2b05b9cec0f5a12a0bdce5bf021a5b3b09323041613972481ab"}, - {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:266e55c83f81248f63cc93d11c5e3a53df49a5d2598fa9e9db5f99837a802d5d"}, - {file = "orjson-3.9.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31372ba3a9fe8ad118e7d22fba46bbc18e89039e3bfa89db7bc8c18ee722dca8"}, - {file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e3b0c4da61f39899561e08e571f54472a09fa71717d9797928af558175ae5243"}, - {file = "orjson-3.9.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cc03a35bfc71c8ebf96ce49b82c2a7be6af4b3cd3ac34166fdb42ac510bbfff"}, - {file = "orjson-3.9.13-cp312-none-win_amd64.whl", hash = "sha256:49b7e3fe861cb246361825d1a238f2584ed8ea21e714bf6bb17cebb86772e61c"}, - {file = "orjson-3.9.13-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:62e9a99879c4d5a04926ac2518a992134bfa00d546ea5a4cae4b9be454d35a22"}, - {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d92a3e835a5100f1d5b566fff79217eab92223ca31900dba733902a182a35ab0"}, - {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23f21faf072ed3b60b5954686f98157e073f6a8068eaa58dbde83e87212eda84"}, - {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:828c502bb261588f7de897e06cb23c4b122997cb039d2014cb78e7dabe92ef0c"}, - {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16946d095212a3dec552572c5d9bca7afa40f3116ad49695a397be07d529f1fa"}, - {file = "orjson-3.9.13-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3deadd8dc0e9ff844b5b656fa30a48dbee1c3b332d8278302dd9637f6b09f627"}, - {file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9b1b5adc5adf596c59dca57156b71ad301d73956f5bab4039b0e34dbf50b9fa0"}, - {file = "orjson-3.9.13-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:ddc089315d030c54f0f03fb38286e2667c05009a78d659f108a8efcfbdf2e585"}, - {file = "orjson-3.9.13-cp38-none-win32.whl", hash = "sha256:ae77275a28667d9c82d4522b681504642055efa0368d73108511647c6499b31c"}, - {file = "orjson-3.9.13-cp38-none-win_amd64.whl", hash = "sha256:730385fdb99a21fce9bb84bb7fcbda72c88626facd74956bda712834b480729d"}, - {file = "orjson-3.9.13-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7e8e4a571d958910272af8d53a9cbe6599f9f5fd496a1bc51211183bb2072cbd"}, - {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfad553a36548262e7da0f3a7464270e13900b898800fb571a5d4b298c3f8356"}, - {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0d691c44604941945b00e0a13b19a7d9c1a19511abadf0080f373e98fdeb6b31"}, - {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a8c83718346de08d68b3cb1105c5d91e5fc39885d8610fdda16613d4e3941459"}, - {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ef57a53bfc2091a7cd50a640d9ae866bd7d92a5225a1bab6baa60ef62583f2"}, - {file = "orjson-3.9.13-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9156b96afa38db71344522f5517077eaedf62fcd2c9148392ff93d801128809c"}, - {file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31fb66b41fb2c4c817d9610f0bc7d31345728d7b5295ac78b63603407432a2b2"}, - {file = "orjson-3.9.13-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8a730bf07feacb0863974e67b206b7c503a62199de1cece2eb0d4c233ec29c11"}, - {file = "orjson-3.9.13-cp39-none-win32.whl", hash = "sha256:5ef58869f3399acbbe013518d8b374ee9558659eef14bca0984f67cb1fbd3c37"}, - {file = "orjson-3.9.13-cp39-none-win_amd64.whl", hash = "sha256:9bcf56efdb83244cde070e82a69c0f03c47c235f0a5cb6c81d9da23af7fbaae4"}, - {file = "orjson-3.9.13.tar.gz", hash = "sha256:fc6bc65b0cf524ee042e0bc2912b9206ef242edfba7426cf95763e4af01f527a"}, + {file = "orjson-3.9.14-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:793f6c9448ab6eb7d4974b4dde3f230345c08ca6c7995330fbceeb43a5c8aa5e"}, + {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bc7928d161840096adc956703494b5c0193ede887346f028216cac0af87500"}, + {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:58b36f54da759602d8e2f7dad958752d453dfe2c7122767bc7f765e17dc59959"}, + {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:abcda41ecdc950399c05eff761c3de91485d9a70d8227cb599ad3a66afe93bcc"}, + {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df76ecd17b1b3627bddfd689faaf206380a1a38cc9f6c4075bd884eaedcf46c2"}, + {file = "orjson-3.9.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d450a8e0656efb5d0fcb062157b918ab02dcca73278975b4ee9ea49e2fcf5bd5"}, + {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:95c03137b0cf66517c8baa65770507a756d3a89489d8ecf864ea92348e1beabe"}, + {file = "orjson-3.9.14-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:20837e10835c98973673406d6798e10f821e7744520633811a5a3d809762d8cc"}, + {file = "orjson-3.9.14-cp310-none-win32.whl", hash = "sha256:1f7b6f3ef10ae8e3558abb729873d033dbb5843507c66b1c0767e32502ba96bb"}, + {file = "orjson-3.9.14-cp310-none-win_amd64.whl", hash = "sha256:ea890e6dc1711aeec0a33b8520e395c2f3d59ead5b4351a788e06bf95fc7ba81"}, + {file = "orjson-3.9.14-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:c19009ff37f033c70acd04b636380379499dac2cba27ae7dfc24f304deabbc81"}, + {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19cdea0664aec0b7f385be84986d4defd3334e9c3c799407686ee1c26f7b8251"}, + {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:135d518f73787ce323b1a5e21fb854fe22258d7a8ae562b81a49d6c7f826f2a3"}, + {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2cf1d0557c61c75e18cf7d69fb689b77896e95553e212c0cc64cf2087944b84"}, + {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7c11667421df2d8b18b021223505dcc3ee51be518d54e4dc49161ac88ac2b87"}, + {file = "orjson-3.9.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eefc41ba42e75ed88bc396d8fe997beb20477f3e7efa000cd7a47eda452fbb2"}, + {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:917311d6a64d1c327c0dfda1e41f3966a7fb72b11ca7aa2e7a68fcccc7db35d9"}, + {file = "orjson-3.9.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4dc1c132259b38d12c6587d190cd09cd76e3b5273ce71fe1372437b4cbc65f6f"}, + {file = "orjson-3.9.14-cp311-none-win32.whl", hash = "sha256:6f39a10408478f4c05736a74da63727a1ae0e83e3533d07b19443400fe8591ca"}, + {file = "orjson-3.9.14-cp311-none-win_amd64.whl", hash = "sha256:26280a7fcb62d8257f634c16acebc3bec626454f9ab13558bbf7883b9140760e"}, + {file = "orjson-3.9.14-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:08e722a8d06b13b67a51f247a24938d1a94b4b3862e40e0eef3b2e98c99cd04c"}, + {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a2591faa0c031cf3f57e5bce1461cfbd6160f3f66b5a72609a130924917cb07d"}, + {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e2450d87dd7b4f277f4c5598faa8b49a0c197b91186c47a2c0b88e15531e4e3e"}, + {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:90903d2908158a2c9077a06f11e27545de610af690fb178fd3ba6b32492d4d1c"}, + {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce6f095eef0026eae76fc212f20f786011ecf482fc7df2f4c272a8ae6dd7b1ef"}, + {file = "orjson-3.9.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:751250a31fef2bac05a2da2449aae7142075ea26139271f169af60456d8ad27a"}, + {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a1af21160a38ee8be3f4fcf24ee4b99e6184cadc7f915d599f073f478a94d2c"}, + {file = "orjson-3.9.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:449bf090b2aa4e019371d7511a6ea8a5a248139205c27d1834bb4b1e3c44d936"}, + {file = "orjson-3.9.14-cp312-none-win_amd64.whl", hash = "sha256:a603161318ff699784943e71f53899983b7dee571b4dd07c336437c9c5a272b0"}, + {file = "orjson-3.9.14-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:814f288c011efdf8f115c5ebcc1ab94b11da64b207722917e0ceb42f52ef30a3"}, + {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a88cafb100af68af3b9b29b5ccd09fdf7a48c63327916c8c923a94c336d38dd3"}, + {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ba3518b999f88882ade6686f1b71e207b52e23546e180499be5bbb63a2f9c6e6"}, + {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978f416bbff9da8d2091e3cf011c92da68b13f2c453dcc2e8109099b2a19d234"}, + {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75fc593cf836f631153d0e21beaeb8d26e144445c73645889335c2247fcd71a0"}, + {file = "orjson-3.9.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:23d1528db3c7554f9d6eeb09df23cb80dd5177ec56eeb55cc5318826928de506"}, + {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:7183cc68ee2113b19b0b8714221e5e3b07b3ba10ca2bb108d78fd49cefaae101"}, + {file = "orjson-3.9.14-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:df3266d54246cb56b8bb17fa908660d2a0f2e3f63fbc32451ffc1b1505051d07"}, + {file = "orjson-3.9.14-cp38-none-win32.whl", hash = "sha256:7913079b029e1b3501854c9a78ad938ed40d61fe09bebab3c93e60ff1301b189"}, + {file = "orjson-3.9.14-cp38-none-win_amd64.whl", hash = "sha256:29512eb925b620e5da2fd7585814485c67cc6ba4fe739a0a700c50467a8a8065"}, + {file = "orjson-3.9.14-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:5bf597530544db27a8d76aced49cfc817ee9503e0a4ebf0109cd70331e7bbe0c"}, + {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac650d49366fa41fe702e054cb560171a8634e2865537e91f09a8d05ea5b1d37"}, + {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:236230433a9a4968ab895140514c308fdf9f607cb8bee178a04372b771123860"}, + {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3014ccbda9be0b1b5f8ea895121df7e6524496b3908f4397ff02e923bcd8f6dd"}, + {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ac0c7eae7ad3a223bde690565442f8a3d620056bd01196f191af8be58a5248e1"}, + {file = "orjson-3.9.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fca33fdd0b38839b01912c57546d4f412ba7bfa0faf9bf7453432219aec2df07"}, + {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f75823cc1674a840a151e999a7dfa0d86c911150dd6f951d0736ee9d383bf415"}, + {file = "orjson-3.9.14-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f52ac2eb49e99e7373f62e2a68428c6946cda52ce89aa8fe9f890c7278e2d3a"}, + {file = "orjson-3.9.14-cp39-none-win32.whl", hash = "sha256:0572f174f50b673b7df78680fb52cd0087a8585a6d06d295a5f790568e1064c6"}, + {file = "orjson-3.9.14-cp39-none-win_amd64.whl", hash = "sha256:ab90c02cb264250b8a58cedcc72ed78a4a257d956c8d3c8bebe9751b818dfad8"}, + {file = "orjson-3.9.14.tar.gz", hash = "sha256:06fb40f8e49088ecaa02f1162581d39e2cf3fd9dbbfe411eb2284147c99bad79"}, ] [[package]] @@ -3096,121 +3096,121 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib" [[package]] name = "tokenizers" -version = "0.15.1" +version = "0.15.2" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "tokenizers-0.15.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:32c9491dd1bcb33172c26b454dbd607276af959b9e78fa766e2694cafab3103c"}, - {file = "tokenizers-0.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29a1b784b870a097e7768f8c20c2dd851e2c75dad3efdae69a79d3e7f1d614d5"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0049fbe648af04148b08cb211994ce8365ee628ce49724b56aaefd09a3007a78"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e84b3c235219e75e24de6b71e6073cd2c8d740b14d88e4c6d131b90134e3a338"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8cc575769ea11d074308c6d71cb10b036cdaec941562c07fc7431d956c502f0e"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:22bf28f299c4158e6d0b5eaebddfd500c4973d947ffeaca8bcbe2e8c137dff0b"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:506555f98361db9c74e1323a862d77dcd7d64c2058829a368bf4159d986e339f"}, - {file = "tokenizers-0.15.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7061b0a28ade15906f5b2ec8c48d3bdd6e24eca6b427979af34954fbe31d5cef"}, - {file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ed5e35507b7a0e2aac3285c4f5e37d4ec5cfc0e5825b862b68a0aaf2757af52"}, - {file = "tokenizers-0.15.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:1c9df9247df0de6509dd751b1c086e5f124b220133b5c883bb691cb6fb3d786f"}, - {file = "tokenizers-0.15.1-cp310-none-win32.whl", hash = "sha256:dd999af1b4848bef1b11d289f04edaf189c269d5e6afa7a95fa1058644c3f021"}, - {file = "tokenizers-0.15.1-cp310-none-win_amd64.whl", hash = "sha256:39d06a57f7c06940d602fad98702cf7024c4eee7f6b9fe76b9f2197d5a4cc7e2"}, - {file = "tokenizers-0.15.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8ad034eb48bf728af06915e9294871f72fcc5254911eddec81d6df8dba1ce055"}, - {file = "tokenizers-0.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea9ede7c42f8fa90f31bfc40376fd91a7d83a4aa6ad38e6076de961d48585b26"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:b85d6fe1a20d903877aa0ef32ef6b96e81e0e48b71c206d6046ce16094de6970"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a7d44f656320137c7d643b9c7dcc1814763385de737fb98fd2643880910f597"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bd244bd0793cdacf27ee65ec3db88c21f5815460e8872bbeb32b040469d6774e"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0f3f4a36e371b3cb1123adac8aeeeeab207ad32f15ed686d9d71686a093bb140"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2921a53966afb29444da98d56a6ccbef23feb3b0c0f294b4e502370a0a64f25"}, - {file = "tokenizers-0.15.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f49068cf51f49c231067f1a8c9fc075ff960573f6b2a956e8e1b0154fb638ea5"}, - {file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0ab1a22f20eaaab832ab3b00a0709ca44a0eb04721e580277579411b622c741c"}, - {file = "tokenizers-0.15.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:671268f24b607c4adc6fa2b5b580fd4211b9f84b16bd7f46d62f8e5be0aa7ba4"}, - {file = "tokenizers-0.15.1-cp311-none-win32.whl", hash = "sha256:a4f03e33d2bf7df39c8894032aba599bf90f6f6378e683a19d28871f09bb07fc"}, - {file = "tokenizers-0.15.1-cp311-none-win_amd64.whl", hash = "sha256:30f689537bcc7576d8bd4daeeaa2cb8f36446ba2f13f421b173e88f2d8289c4e"}, - {file = "tokenizers-0.15.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:0f3a379dd0898a82ea3125e8f9c481373f73bffce6430d4315f0b6cd5547e409"}, - {file = "tokenizers-0.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d870ae58bba347d38ac3fc8b1f662f51e9c95272d776dd89f30035c83ee0a4f"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d6d28e0143ec2e253a8a39e94bf1d24776dbe73804fa748675dbffff4a5cd6d8"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61ae9ac9f44e2da128ee35db69489883b522f7abe033733fa54eb2de30dac23d"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d8e322a47e29128300b3f7749a03c0ec2bce0a3dc8539ebff738d3f59e233542"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:760334f475443bc13907b1a8e1cb0aeaf88aae489062546f9704dce6c498bfe2"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1b173753d4aca1e7d0d4cb52b5e3ffecfb0ca014e070e40391b6bb4c1d6af3f2"}, - {file = "tokenizers-0.15.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82c1f13d457c8f0ab17e32e787d03470067fe8a3b4d012e7cc57cb3264529f4a"}, - {file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:425b46ceff4505f20191df54b50ac818055d9d55023d58ae32a5d895b6f15bb0"}, - {file = "tokenizers-0.15.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:681ac6ba3b4fdaf868ead8971221a061f580961c386e9732ea54d46c7b72f286"}, - {file = "tokenizers-0.15.1-cp312-none-win32.whl", hash = "sha256:f2272656063ccfba2044df2115095223960d80525d208e7a32f6c01c351a6f4a"}, - {file = "tokenizers-0.15.1-cp312-none-win_amd64.whl", hash = "sha256:9abe103203b1c6a2435d248d5ff4cceebcf46771bfbc4957a98a74da6ed37674"}, - {file = "tokenizers-0.15.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:2ce9ed5c8ef26b026a66110e3c7b73d93ec2d26a0b1d0ea55ddce61c0e5f446f"}, - {file = "tokenizers-0.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:89b24d366137986c3647baac29ef902d2d5445003d11c30df52f1bd304689aeb"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0faebedd01b413ab777ca0ee85914ed8b031ea5762ab0ea60b707ce8b9be6842"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbd9dfcdad4f3b95d801f768e143165165055c18e44ca79a8a26de889cd8e85"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:97194324c12565b07e9993ca9aa813b939541185682e859fb45bb8d7d99b3193"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:485e43e2cc159580e0d83fc919ec3a45ae279097f634b1ffe371869ffda5802c"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:191d084d60e3589d6420caeb3f9966168269315f8ec7fbc3883122dc9d99759d"}, - {file = "tokenizers-0.15.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01c28cc8d7220634a75b14c53f4fc9d1b485f99a5a29306a999c115921de2897"}, - {file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:325212027745d3f8d5d5006bb9e5409d674eb80a184f19873f4f83494e1fdd26"}, - {file = "tokenizers-0.15.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3c5573603c36ce12dbe318bcfb490a94cad2d250f34deb2f06cb6937957bbb71"}, - {file = "tokenizers-0.15.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:1441161adb6d71a15a630d5c1d8659d5ebe41b6b209586fbeea64738e58fcbb2"}, - {file = "tokenizers-0.15.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:382a8d0c31afcfb86571afbfefa37186df90865ce3f5b731842dab4460e53a38"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e76959783e3f4ec73b3f3d24d4eec5aa9225f0bee565c48e77f806ed1e048f12"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:401df223e5eb927c5961a0fc6b171818a2bba01fb36ef18c3e1b69b8cd80e591"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52606c233c759561a16e81b2290a7738c3affac7a0b1f0a16fe58dc22e04c7d"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b72c658bbe5a05ed8bc2ac5ad782385bfd743ffa4bc87d9b5026341e709c6f44"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:25f5643a2f005c42f0737a326c6c6bdfedfdc9a994b10a1923d9c3e792e4d6a6"}, - {file = "tokenizers-0.15.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c5b6f633999d6b42466bbfe21be2e26ad1760b6f106967a591a41d8cbca980e"}, - {file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:ceb5c9ad11a015150b545c1a11210966a45b8c3d68a942e57cf8938c578a77ca"}, - {file = "tokenizers-0.15.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bedd4ce0c4872db193444c395b11c7697260ce86a635ab6d48102d76be07d324"}, - {file = "tokenizers-0.15.1-cp37-none-win32.whl", hash = "sha256:cd6caef6c14f5ed6d35f0ddb78eab8ca6306d0cd9870330bccff72ad014a6f42"}, - {file = "tokenizers-0.15.1-cp37-none-win_amd64.whl", hash = "sha256:d2bd7af78f58d75a55e5df61efae164ab9200c04b76025f9cc6eeb7aff3219c2"}, - {file = "tokenizers-0.15.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:59b3ca6c02e0bd5704caee274978bd055de2dff2e2f39dadf536c21032dfd432"}, - {file = "tokenizers-0.15.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:48fe21b67c22583bed71933a025fd66b1f5cfae1baefa423c3d40379b5a6e74e"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:3d190254c66a20fb1efbdf035e6333c5e1f1c73b1f7bfad88f9c31908ac2c2c4"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fef90c8f5abf17d48d6635f5fd92ad258acd1d0c2d920935c8bf261782cfe7c8"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fac011ef7da3357aa7eb19efeecf3d201ede9618f37ddedddc5eb809ea0963ca"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:574ec5b3e71d1feda6b0ecac0e0445875729b4899806efbe2b329909ec75cb50"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aca16c3c0637c051a59ea99c4253f16fbb43034fac849076a7e7913b2b9afd2d"}, - {file = "tokenizers-0.15.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a6f238fc2bbfd3e12e8529980ec1624c7e5b69d4e959edb3d902f36974f725a"}, - {file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:587e11a26835b73c31867a728f32ca8a93c9ded4a6cd746516e68b9d51418431"}, - {file = "tokenizers-0.15.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6456e7ad397352775e2efdf68a9ec5d6524bbc4543e926eef428d36de627aed4"}, - {file = "tokenizers-0.15.1-cp38-none-win32.whl", hash = "sha256:614f0da7dd73293214bd143e6221cafd3f7790d06b799f33a987e29d057ca658"}, - {file = "tokenizers-0.15.1-cp38-none-win_amd64.whl", hash = "sha256:a4fa0a20d9f69cc2bf1cfce41aa40588598e77ec1d6f56bf0eb99769969d1ede"}, - {file = "tokenizers-0.15.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:8d3f18a45e0cf03ce193d5900460dc2430eec4e14c786e5d79bddba7ea19034f"}, - {file = "tokenizers-0.15.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:38dbd6c38f88ad7d5dc5d70c764415d38fe3bcd99dc81638b572d093abc54170"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:777286b1f7e52de92aa4af49fe31046cfd32885d1bbaae918fab3bba52794c33"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58d4d550a3862a47dd249892d03a025e32286eb73cbd6bc887fb8fb64bc97165"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eda68ce0344f35042ae89220b40a0007f721776b727806b5c95497b35714bb7"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cd33d15f7a3a784c3b665cfe807b8de3c6779e060349bd5005bb4ae5bdcb437"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0a1aa370f978ac0bfb50374c3a40daa93fd56d47c0c70f0c79607fdac2ccbb42"}, - {file = "tokenizers-0.15.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:241482b940340fff26a2708cb9ba383a5bb8a2996d67a0ff2c4367bf4b86cc3a"}, - {file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:68f30b05f46a4d9aba88489eadd021904afe90e10a7950e28370d6e71b9db021"}, - {file = "tokenizers-0.15.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5a3c5d8025529670462b881b7b2527aacb6257398c9ec8e170070432c3ae3a82"}, - {file = "tokenizers-0.15.1-cp39-none-win32.whl", hash = "sha256:74d1827830f60a9d78da8f6d49a1fbea5422ce0eea42e2617877d23380a7efbc"}, - {file = "tokenizers-0.15.1-cp39-none-win_amd64.whl", hash = "sha256:9ff499923e4d6876d6b6a63ea84a56805eb35e91dd89b933a7aee0c56a3838c6"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b3aa007a0f4408f62a8471bdaa3faccad644cbf2622639f2906b4f9b5339e8b8"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f3d4176fa93d8b2070db8f3c70dc21106ae6624fcaaa334be6bdd3a0251e729e"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d0e463655ef8b2064df07bd4a445ed7f76f6da3b286b4590812587d42f80e89"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:089138fd0351b62215c462a501bd68b8df0e213edcf99ab9efd5dba7b4cb733e"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e563ac628f5175ed08e950430e2580e544b3e4b606a0995bb6b52b3a3165728"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:244dcc28c5fde221cb4373961b20da30097669005b122384d7f9f22752487a46"}, - {file = "tokenizers-0.15.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d82951d46052dddae1369e68ff799a0e6e29befa9a0b46e387ae710fd4daefb0"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7b14296bc9059849246ceb256ffbe97f8806a9b5d707e0095c22db312f4fc014"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0309357bb9b6c8d86cdf456053479d7112074b470651a997a058cd7ad1c4ea57"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083f06e9d8d01b70b67bcbcb7751b38b6005512cce95808be6bf34803534a7e7"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85288aea86ada579789447f0dcec108ebef8da4b450037eb4813d83e4da9371e"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:385e6fcb01e8de90c1d157ae2a5338b23368d0b1c4cc25088cdca90147e35d17"}, - {file = "tokenizers-0.15.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:60067edfcbf7d6cd448ac47af41ec6e84377efbef7be0c06f15a7c1dd069e044"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f7e37f89acfe237d4eaf93c3b69b0f01f407a7a5d0b5a8f06ba91943ea3cf10"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:6a63a15b523d42ebc1f4028e5a568013388c2aefa4053a263e511cb10aaa02f1"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2417d9e4958a6c2fbecc34c27269e74561c55d8823bf914b422e261a11fdd5fd"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8550974bace6210e41ab04231e06408cf99ea4279e0862c02b8d47e7c2b2828"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:194ba82129b171bcd29235a969e5859a93e491e9b0f8b2581f500f200c85cfdd"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1bfd95eef8b01e6c0805dbccc8eaf41d8c5a84f0cce72c0ab149fe76aae0bce6"}, - {file = "tokenizers-0.15.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b87a15dd72f8216b03c151e3dace00c75c3fe7b0ee9643c25943f31e582f1a34"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6ac22f358a0c2a6c685be49136ce7ea7054108986ad444f567712cf274b34cd8"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e9d1f046a9b9d9a95faa103f07db5921d2c1c50f0329ebba4359350ee02b18b"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2a0fd30a4b74485f6a7af89fffb5fb84d6d5f649b3e74f8d37f624cc9e9e97cf"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e45dc206b9447fa48795a1247c69a1732d890b53e2cc51ba42bc2fefa22407"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4eaff56ef3e218017fa1d72007184401f04cb3a289990d2b6a0a76ce71c95f96"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b41dc107e4a4e9c95934e79b025228bbdda37d9b153d8b084160e88d5e48ad6f"}, - {file = "tokenizers-0.15.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1922b8582d0c33488764bcf32e80ef6054f515369e70092729c928aae2284bc2"}, - {file = "tokenizers-0.15.1.tar.gz", hash = "sha256:c0a331d6d5a3d6e97b7f99f562cee8d56797180797bc55f12070e495e717c980"}, + {file = "tokenizers-0.15.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:52f6130c9cbf70544287575a985bf44ae1bda2da7e8c24e97716080593638012"}, + {file = "tokenizers-0.15.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:054c1cc9c6d68f7ffa4e810b3d5131e0ba511b6e4be34157aa08ee54c2f8d9ee"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a9b9b070fdad06e347563b88c278995735292ded1132f8657084989a4c84a6d5"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea621a7eef4b70e1f7a4e84dd989ae3f0eeb50fc8690254eacc08acb623e82f1"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf7fd9a5141634fa3aa8d6b7be362e6ae1b4cda60da81388fa533e0b552c98fd"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:44f2a832cd0825295f7179eaf173381dc45230f9227ec4b44378322d900447c9"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8b9ec69247a23747669ec4b0ca10f8e3dfb3545d550258129bd62291aabe8605"}, + {file = "tokenizers-0.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40b6a4c78da863ff26dbd5ad9a8ecc33d8a8d97b535172601cf00aee9d7ce9ce"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5ab2a4d21dcf76af60e05af8063138849eb1d6553a0d059f6534357bce8ba364"}, + {file = "tokenizers-0.15.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a47acfac7e511f6bbfcf2d3fb8c26979c780a91e06fb5b9a43831b2c0153d024"}, + {file = "tokenizers-0.15.2-cp310-none-win32.whl", hash = "sha256:064ff87bb6acdbd693666de9a4b692add41308a2c0ec0770d6385737117215f2"}, + {file = "tokenizers-0.15.2-cp310-none-win_amd64.whl", hash = "sha256:3b919afe4df7eb6ac7cafd2bd14fb507d3f408db7a68c43117f579c984a73843"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:89cd1cb93e4b12ff39bb2d626ad77e35209de9309a71e4d3d4672667b4b256e7"}, + {file = "tokenizers-0.15.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:cfed5c64e5be23d7ee0f0e98081a25c2a46b0b77ce99a4f0605b1ec43dd481fa"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:a907d76dcfda37023ba203ab4ceeb21bc5683436ebefbd895a0841fd52f6f6f2"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ea60479de6fc7b8ae756b4b097572372d7e4032e2521c1bbf3d90c90a99ff0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:48e2b9335be2bc0171df9281385c2ed06a15f5cf121c44094338306ab7b33f2c"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:112a1dd436d2cc06e6ffdc0b06d55ac019a35a63afd26475205cb4b1bf0bfbff"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4620cca5c2817177ee8706f860364cc3a8845bc1e291aaf661fb899e5d1c45b0"}, + {file = "tokenizers-0.15.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ccd73a82751c523b3fc31ff8194702e4af4db21dc20e55b30ecc2079c5d43cb7"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:107089f135b4ae7817affe6264f8c7a5c5b4fd9a90f9439ed495f54fcea56fb4"}, + {file = "tokenizers-0.15.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0ff110ecc57b7aa4a594396525a3451ad70988e517237fe91c540997c4e50e29"}, + {file = "tokenizers-0.15.2-cp311-none-win32.whl", hash = "sha256:6d76f00f5c32da36c61f41c58346a4fa7f0a61be02f4301fd30ad59834977cc3"}, + {file = "tokenizers-0.15.2-cp311-none-win_amd64.whl", hash = "sha256:cc90102ed17271cf0a1262babe5939e0134b3890345d11a19c3145184b706055"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f86593c18d2e6248e72fb91c77d413a815153b8ea4e31f7cd443bdf28e467670"}, + {file = "tokenizers-0.15.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0774bccc6608eca23eb9d620196687c8b2360624619623cf4ba9dc9bd53e8b51"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d0222c5b7c9b26c0b4822a82f6a7011de0a9d3060e1da176f66274b70f846b98"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3835738be1de66624fff2f4f6f6684775da4e9c00bde053be7564cbf3545cc66"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:0143e7d9dcd811855c1ce1ab9bf5d96d29bf5e528fd6c7824d0465741e8c10fd"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:db35825f6d54215f6b6009a7ff3eedee0848c99a6271c870d2826fbbedf31a38"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3f5e64b0389a2be47091d8cc53c87859783b837ea1a06edd9d8e04004df55a5c"}, + {file = "tokenizers-0.15.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e0480c452217edd35eca56fafe2029fb4d368b7c0475f8dfa3c5c9c400a7456"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a33ab881c8fe70474980577e033d0bc9a27b7ab8272896e500708b212995d834"}, + {file = "tokenizers-0.15.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a308a607ca9de2c64c1b9ba79ec9a403969715a1b8ba5f998a676826f1a7039d"}, + {file = "tokenizers-0.15.2-cp312-none-win32.whl", hash = "sha256:b8fcfa81bcb9447df582c5bc96a031e6df4da2a774b8080d4f02c0c16b42be0b"}, + {file = "tokenizers-0.15.2-cp312-none-win_amd64.whl", hash = "sha256:38d7ab43c6825abfc0b661d95f39c7f8af2449364f01d331f3b51c94dcff7221"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:38bfb0204ff3246ca4d5e726e8cc8403bfc931090151e6eede54d0e0cf162ef0"}, + {file = "tokenizers-0.15.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c861d35e8286a53e06e9e28d030b5a05bcbf5ac9d7229e561e53c352a85b1fc"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:936bf3842db5b2048eaa53dade907b1160f318e7c90c74bfab86f1e47720bdd6"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:620beacc3373277700d0e27718aa8b25f7b383eb8001fba94ee00aeea1459d89"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2735ecbbf37e52db4ea970e539fd2d450d213517b77745114f92867f3fc246eb"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:473c83c5e2359bb81b0b6fde870b41b2764fcdd36d997485e07e72cc3a62264a"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:968fa1fb3c27398b28a4eca1cbd1e19355c4d3a6007f7398d48826bbe3a0f728"}, + {file = "tokenizers-0.15.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:865c60ae6eaebdde7da66191ee9b7db52e542ed8ee9d2c653b6d190a9351b980"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7c0d8b52664ab2d4a8d6686eb5effc68b78608a9008f086a122a7b2996befbab"}, + {file = "tokenizers-0.15.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:f33dfbdec3784093a9aebb3680d1f91336c56d86cc70ddf88708251da1fe9064"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:d44ba80988ff9424e33e0a49445072ac7029d8c0e1601ad25a0ca5f41ed0c1d6"}, + {file = "tokenizers-0.15.2-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:dce74266919b892f82b1b86025a613956ea0ea62a4843d4c4237be2c5498ed3a"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0ef06b9707baeb98b316577acb04f4852239d856b93e9ec3a299622f6084e4be"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c73e2e74bbb07910da0d37c326869f34113137b23eadad3fc00856e6b3d9930c"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4eeb12daf02a59e29f578a865f55d87cd103ce62bd8a3a5874f8fdeaa82e336b"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ba9f6895af58487ca4f54e8a664a322f16c26bbb442effd01087eba391a719e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccec77aa7150e38eec6878a493bf8c263ff1fa8a62404e16c6203c64c1f16a26"}, + {file = "tokenizers-0.15.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3f40604f5042ff210ba82743dda2b6aa3e55aa12df4e9f2378ee01a17e2855e"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:5645938a42d78c4885086767c70923abad047163d809c16da75d6b290cb30bbe"}, + {file = "tokenizers-0.15.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:05a77cbfebe28a61ab5c3891f9939cc24798b63fa236d84e5f29f3a85a200c00"}, + {file = "tokenizers-0.15.2-cp37-none-win32.whl", hash = "sha256:361abdc068e8afe9c5b818769a48624687fb6aaed49636ee39bec4e95e1a215b"}, + {file = "tokenizers-0.15.2-cp37-none-win_amd64.whl", hash = "sha256:7ef789f83eb0f9baeb4d09a86cd639c0a5518528f9992f38b28e819df397eb06"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4fe1f74a902bee74a3b25aff180fbfbf4f8b444ab37c4d496af7afd13a784ed2"}, + {file = "tokenizers-0.15.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4c4b89038a684f40a6b15d6b09f49650ac64d951ad0f2a3ea9169687bbf2a8ba"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:d05a1b06f986d41aed5f2de464c003004b2df8aaf66f2b7628254bcbfb72a438"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:508711a108684111ec8af89d3a9e9e08755247eda27d0ba5e3c50e9da1600f6d"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:daa348f02d15160cb35439098ac96e3a53bacf35885072611cd9e5be7d333daa"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:494fdbe5932d3416de2a85fc2470b797e6f3226c12845cadf054dd906afd0442"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2d60f5246f4da9373f75ff18d64c69cbf60c3bca597290cea01059c336d2470"}, + {file = "tokenizers-0.15.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93268e788825f52de4c7bdcb6ebc1fcd4a5442c02e730faa9b6b08f23ead0e24"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6fc7083ab404019fc9acafe78662c192673c1e696bd598d16dc005bd663a5cf9"}, + {file = "tokenizers-0.15.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:41e39b41e5531d6b2122a77532dbea60e171ef87a3820b5a3888daa847df4153"}, + {file = "tokenizers-0.15.2-cp38-none-win32.whl", hash = "sha256:06cd0487b1cbfabefb2cc52fbd6b1f8d4c37799bd6c6e1641281adaa6b2504a7"}, + {file = "tokenizers-0.15.2-cp38-none-win_amd64.whl", hash = "sha256:5179c271aa5de9c71712e31cb5a79e436ecd0d7532a408fa42a8dbfa4bc23fd9"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:82f8652a74cc107052328b87ea8b34291c0f55b96d8fb261b3880216a9f9e48e"}, + {file = "tokenizers-0.15.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:02458bee6f5f3139f1ebbb6d042b283af712c0981f5bc50edf771d6b762d5e4f"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c9a09cd26cca2e1c349f91aa665309ddb48d71636370749414fbf67bc83c5343"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:158be8ea8554e5ed69acc1ce3fbb23a06060bd4bbb09029431ad6b9a466a7121"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ddba9a2b0c8c81633eca0bb2e1aa5b3a15362b1277f1ae64176d0f6eba78ab1"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ef5dd1d39797044642dbe53eb2bc56435308432e9c7907728da74c69ee2adca"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:454c203164e07a860dbeb3b1f4a733be52b0edbb4dd2e5bd75023ffa8b49403a"}, + {file = "tokenizers-0.15.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cf6b7f1d4dc59af960e6ffdc4faffe6460bbfa8dce27a58bf75755ffdb2526d"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2ef09bbc16519f6c25d0c7fc0c6a33a6f62923e263c9d7cca4e58b8c61572afb"}, + {file = "tokenizers-0.15.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c9a2ebdd2ad4ec7a68e7615086e633857c85e2f18025bd05d2a4399e6c5f7169"}, + {file = "tokenizers-0.15.2-cp39-none-win32.whl", hash = "sha256:918fbb0eab96fe08e72a8c2b5461e9cce95585d82a58688e7f01c2bd546c79d0"}, + {file = "tokenizers-0.15.2-cp39-none-win_amd64.whl", hash = "sha256:524e60da0135e106b254bd71f0659be9f89d83f006ea9093ce4d1fab498c6d0d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:6a9b648a58281c4672212fab04e60648fde574877d0139cd4b4f93fe28ca8944"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7c7d18b733be6bbca8a55084027f7be428c947ddf871c500ee603e375013ffba"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:13ca3611de8d9ddfbc4dc39ef54ab1d2d4aaa114ac8727dfdc6a6ec4be017378"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:237d1bf3361cf2e6463e6c140628e6406766e8b27274f5fcc62c747ae3c6f094"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67a0fe1e49e60c664915e9fb6b0cb19bac082ab1f309188230e4b2920230edb3"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4e022fe65e99230b8fd89ebdfea138c24421f91c1a4f4781a8f5016fd5cdfb4d"}, + {file = "tokenizers-0.15.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:d857be2df69763362ac699f8b251a8cd3fac9d21893de129bc788f8baaef2693"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:708bb3e4283177236309e698da5fcd0879ce8fd37457d7c266d16b550bcbbd18"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c35e09e9899b72a76e762f9854e8750213f67567787d45f37ce06daf57ca78"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c1257f4394be0d3b00de8c9e840ca5601d0a4a8438361ce9c2b05c7d25f6057b"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02272fe48280e0293a04245ca5d919b2c94a48b408b55e858feae9618138aeda"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dc3ad9ebc76eabe8b1d7c04d38be884b8f9d60c0cdc09b0aa4e3bcf746de0388"}, + {file = "tokenizers-0.15.2-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:32e16bdeffa7c4f46bf2152172ca511808b952701d13e7c18833c0b73cb5c23f"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fb16ba563d59003028b678d2361a27f7e4ae0ab29c7a80690efa20d829c81fdb"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:2277c36d2d6cdb7876c274547921a42425b6810d38354327dd65a8009acf870c"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1cf75d32e8d250781940d07f7eece253f2fe9ecdb1dc7ba6e3833fa17b82fcbc"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1b3b31884dc8e9b21508bb76da80ebf7308fdb947a17affce815665d5c4d028"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10122d8d8e30afb43bb1fe21a3619f62c3e2574bff2699cf8af8b0b6c5dc4a3"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d88b96ff0fe8e91f6ef01ba50b0d71db5017fa4e3b1d99681cec89a85faf7bf7"}, + {file = "tokenizers-0.15.2-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:37aaec5a52e959892870a7c47cef80c53797c0db9149d458460f4f31e2fb250e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e2ea752f2b0fe96eb6e2f3adbbf4d72aaa1272079b0dfa1145507bd6a5d537e6"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:4b19a808d8799fda23504a5cd31d2f58e6f52f140380082b352f877017d6342b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:64c86e5e068ac8b19204419ed8ca90f9d25db20578f5881e337d203b314f4104"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de19c4dc503c612847edf833c82e9f73cd79926a384af9d801dcf93f110cea4e"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea09acd2fe3324174063d61ad620dec3bcf042b495515f27f638270a7d466e8b"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:cf27fd43472e07b57cf420eee1e814549203d56de00b5af8659cb99885472f1f"}, + {file = "tokenizers-0.15.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:7ca22bd897537a0080521445d91a58886c8c04084a6a19e6c78c586e0cfa92a5"}, + {file = "tokenizers-0.15.2.tar.gz", hash = "sha256:e6e9c6e019dd5484be5beafc775ae6c925f4c69a3487040ed09b45e13df2cb91"}, ] [package.dependencies] @@ -3281,13 +3281,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.27.0.post1" +version = "0.27.1" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.27.0.post1-py3-none-any.whl", hash = "sha256:4b85ba02b8a20429b9b205d015cbeb788a12da527f731811b643fd739ef90d5f"}, - {file = "uvicorn-0.27.0.post1.tar.gz", hash = "sha256:54898fcd80c13ff1cd28bf77b04ec9dbd8ff60c5259b499b4b12bb0917f22907"}, + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, ] [package.dependencies] From e7995014f94690e5542a5835e44eb557ccc8d1ba Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:21:43 +0100 Subject: [PATCH 07/59] fix(web): search filter form events (#7285) --- .../search-bar/search-filter-box.svelte | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 73c115b3cf..c433be947f 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -324,7 +324,13 @@

FILTERS


-
+
@@ -566,8 +572,8 @@ id="button-row" class="flex justify-end gap-4 py-4 sticky bottom-0 dark:border-gray-800 dark:bg-immich-dark-gray" > - - + +
From ee3b3ca115ba06a1a64f5f7214be978355cbdbc3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:24:50 -0500 Subject: [PATCH 08/59] chore(deps): update dependency vite to v5.1.3 (#7247) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index aa864c0a30..84bd64d3e9 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8733,9 +8733,9 @@ } }, "node_modules/vite": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz", - "integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", "dev": true, "dependencies": { "esbuild": "^0.19.3", From 43f887e5f2204f379f6fa848d16ac30ae3fca511 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:25:28 -0500 Subject: [PATCH 09/59] chore(deps): update dependency @types/node to v20.11.19 (#7239) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 332edfbe9b..511105bcca 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -3179,9 +3179,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -14730,9 +14730,9 @@ } }, "@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "requires": { "undici-types": "~5.26.4" } From 01f682134aeb181dea23f2aa9a623c60b0254434 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:25:37 -0500 Subject: [PATCH 10/59] chore(deps): update dependency @types/node to v20.11.19 (#7238) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- open-api/typescript-sdk/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 5346a47086..a918e2d2c9 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -29,9 +29,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" From c5da317033aa919c01bf9dc49528dd121d161661 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:25:45 -0500 Subject: [PATCH 11/59] chore(deps): update dependency @types/node to v20.11.19 (#7236) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- e2e/package-lock.json | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 5e0ffb0e2b..9fe651a52a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -23,9 +23,13 @@ } }, "../cli": { + "name": "@immich/cli", "version": "2.0.8", "dev": true, "license": "GNU Affero General Public License version 3", + "dependencies": { + "lodash-es": "^4.17.21" + }, "bin": { "immich": "dist/index.js" }, @@ -34,6 +38,7 @@ "@testcontainers/postgresql": "^10.7.1", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", + "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", "@types/node": "^20.3.1", "@typescript-eslint/eslint-plugin": "^7.0.0", @@ -801,9 +806,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" From aeb7081af1b1c190b5a3dbd8ef154cb09da95017 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:25:59 -0500 Subject: [PATCH 12/59] chore(deps): update @immich/cli (#7235) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index c3c11f3307..22d8585704 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1325,9 +1325,9 @@ } }, "node_modules/@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "dependencies": { "undici-types": "~5.26.4" @@ -5240,9 +5240,9 @@ } }, "node_modules/vite": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz", - "integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", "dev": true, "dependencies": { "esbuild": "^0.19.3", @@ -6481,9 +6481,9 @@ } }, "@types/node": { - "version": "20.11.17", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.17.tgz", - "integrity": "sha512-QmgQZGWu1Yw9TDyAP9ZzpFJKynYNeOvwMJmaxABfieQoVoiVOS6MN1WSpqpRcbeA5+RW82kraAVxCCJg+780Qw==", + "version": "20.11.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", + "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", "dev": true, "requires": { "undici-types": "~5.26.4" @@ -9364,9 +9364,9 @@ } }, "vite": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.2.tgz", - "integrity": "sha512-uwiFebQbTWRIGbCaTEBVAfKqgqKNKMJ2uPXsXeLIZxM8MVMjoS3j0cG8NrPxdDIadaWnPSjrkLWffLSC+uiP3Q==", + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.3.tgz", + "integrity": "sha512-UfmUD36DKkqhi/F75RrxvPpry+9+tTkrXfMNZD+SboZqBCMsxKtO52XeGzzuh7ioz+Eo/SYDBbdb0Z7vgcDJew==", "dev": true, "requires": { "esbuild": "^0.19.3", From a1bc74cdd65134d9cf0179e39a4ac9f122574926 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Feb 2024 08:26:13 -0500 Subject: [PATCH 13/59] fix(deps): update exiftool (#7230) * fix(deps): update exiftool * documenting such changes would have been too easy --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler --- server/package-lock.json | 84 +++++++++---------- server/package.json | 4 +- .../src/domain/metadata/metadata.service.ts | 2 +- 3 files changed, 45 insertions(+), 45 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 511105bcca..97c9dca58e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -31,8 +31,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~24.4.0", - "exiftool-vendored.pl": "12.73", + "exiftool-vendored": "~24.5.0", + "exiftool-vendored.pl": "12.76", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", "glob": "^10.3.3", @@ -2705,9 +2705,9 @@ } }, "node_modules/@photostructure/tz-lookup": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.0.tgz", - "integrity": "sha512-gM3Xrs+XhD8ojDN0TgybuzSjsQb9UvF8j9DvR75E2zHlJQNiOztzILvfhVwadgA8JJbSMNzE+kYUnwP8aQnlXw==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz", + "integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog==" }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", @@ -4280,9 +4280,9 @@ } }, "node_modules/batch-cluster": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz", - "integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==", + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz", + "integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==", "engines": { "node": ">=14" } @@ -5998,34 +5998,34 @@ "dev": true }, "node_modules/exiftool-vendored": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.4.0.tgz", - "integrity": "sha512-n9GjZ+t0sD4mFGyxVCuyVKkyc4wDQraGE9XE3TbWqJBResbIggMBwcYeo9Q5oVBXe2K8EA/WraWDTqHN+C+52g==", + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", + "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", "dependencies": { - "@photostructure/tz-lookup": "^9.0.0", - "@types/luxon": "^3.4.1", - "batch-cluster": "^12.1.0", + "@photostructure/tz-lookup": "^9.0.1", + "@types/luxon": "^3.4.2", + "batch-cluster": "^13.0.0", "he": "^1.2.0", "luxon": "^3.4.4" }, "optionalDependencies": { - "exiftool-vendored.exe": "12.73.0", - "exiftool-vendored.pl": "12.73.0" + "exiftool-vendored.exe": "12.76.0", + "exiftool-vendored.pl": "12.76.0" } }, "node_modules/exiftool-vendored.exe": { - "version": "12.73.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.73.0.tgz", - "integrity": "sha512-7za1Iv1hBnO92A+Yua04PHicCcwrP4edCzHaYnDOuea2DhmpIhqCgUUgrShcm4tG58ueRBa3GVvhRW/gCm8n4g==", + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", + "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", "optional": true, "os": [ "win32" ] }, "node_modules/exiftool-vendored.pl": { - "version": "12.73.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.73.0.tgz", - "integrity": "sha512-qX6kiGUTuQ/HFwuP3VJHU///BSwlaHSWm+yrDTHHQG+w+yvjFdtajDTJ96CUvPA/ecehtbTUMqCUz5xgMmHfBw==", + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", + "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==", "os": [ "!win32" ] @@ -14280,9 +14280,9 @@ } }, "@photostructure/tz-lookup": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.0.tgz", - "integrity": "sha512-gM3Xrs+XhD8ojDN0TgybuzSjsQb9UvF8j9DvR75E2zHlJQNiOztzILvfhVwadgA8JJbSMNzE+kYUnwP8aQnlXw==" + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.1.tgz", + "integrity": "sha512-inMfhc1QVKheq/PHF0v2vRPnPzTljxscuOKK95o3VlZA4T4w4DeYIpu7dC4W1EyjUhfZJlCBUudpmnFGNCqTog==" }, "@pkgjs/parseargs": { "version": "0.11.0", @@ -15601,9 +15601,9 @@ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" }, "batch-cluster": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-12.1.0.tgz", - "integrity": "sha512-whGyJU4tr7kyg2USByu0/51mML5HsLAeNz5s03kMDYZNsQsGgDJgI47RdY3r7MciCjPkTaTD5O4eOVqOfEO7pg==" + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz", + "integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==" }, "bcrypt": { "version": "5.1.1", @@ -16841,15 +16841,15 @@ } }, "exiftool-vendored": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.4.0.tgz", - "integrity": "sha512-n9GjZ+t0sD4mFGyxVCuyVKkyc4wDQraGE9XE3TbWqJBResbIggMBwcYeo9Q5oVBXe2K8EA/WraWDTqHN+C+52g==", + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", + "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", "requires": { - "@photostructure/tz-lookup": "^9.0.0", - "@types/luxon": "^3.4.1", - "batch-cluster": "^12.1.0", - "exiftool-vendored.exe": "12.73.0", - "exiftool-vendored.pl": "12.73.0", + "@photostructure/tz-lookup": "^9.0.1", + "@types/luxon": "^3.4.2", + "batch-cluster": "^13.0.0", + "exiftool-vendored.exe": "12.76.0", + "exiftool-vendored.pl": "12.76.0", "he": "^1.2.0", "luxon": "^3.4.4" }, @@ -16862,15 +16862,15 @@ } }, "exiftool-vendored.exe": { - "version": "12.73.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.73.0.tgz", - "integrity": "sha512-7za1Iv1hBnO92A+Yua04PHicCcwrP4edCzHaYnDOuea2DhmpIhqCgUUgrShcm4tG58ueRBa3GVvhRW/gCm8n4g==", + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", + "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", "optional": true }, "exiftool-vendored.pl": { - "version": "12.73.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.73.0.tgz", - "integrity": "sha512-qX6kiGUTuQ/HFwuP3VJHU///BSwlaHSWm+yrDTHHQG+w+yvjFdtajDTJ96CUvPA/ecehtbTUMqCUz5xgMmHfBw==" + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", + "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==" }, "exit": { "version": "0.1.2", diff --git a/server/package.json b/server/package.json index 35eebde8a9..e7d84dc5af 100644 --- a/server/package.json +++ b/server/package.json @@ -56,8 +56,8 @@ "class-transformer": "^0.5.1", "class-validator": "^0.14.0", "cookie-parser": "^1.4.6", - "exiftool-vendored": "~24.4.0", - "exiftool-vendored.pl": "12.73", + "exiftool-vendored": "~24.5.0", + "exiftool-vendored.pl": "12.76", "fluent-ffmpeg": "^2.1.2", "geo-tz": "^8.0.0", "glob": "^10.3.3", diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/domain/metadata/metadata.service.ts index 94c3e9ae3f..562568adf6 100644 --- a/server/src/domain/metadata/metadata.service.ts +++ b/server/src/domain/metadata/metadata.service.ts @@ -493,7 +493,7 @@ export class MetadataService { model: tags.Model ?? null, modifyDate: exifDate(tags.ModifyDate) ?? asset.fileModifiedAt, orientation: validate(tags.Orientation)?.toString() ?? null, - profileDescription: tags.ProfileDescription || tags.ProfileName || null, + profileDescription: tags.ProfileDescription || null, projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, timeZone: tags.tz ?? null, }; From f798e037d500f298ae36d849769469d2e5dae901 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Feb 2024 08:28:03 -0500 Subject: [PATCH 14/59] refactor(server): e2e (#7265) * refactor: activity e2e * refactor: person e2e * refactor: shared link e2e --- .../src}/api/specs/activity.e2e-spec.ts | 291 +++++++++------- e2e/src/api/specs/person.e2e-spec.ts | 176 ++++++++++ .../src}/api/specs/shared-link.e2e-spec.ts | 320 ++++++++++-------- e2e/src/utils.ts | 54 ++- server/e2e/api/specs/person.e2e-spec.ts | 191 ----------- 5 files changed, 573 insertions(+), 459 deletions(-) rename {server/e2e => e2e/src}/api/specs/activity.e2e-spec.ts (57%) create mode 100644 e2e/src/api/specs/person.e2e-spec.ts rename {server/e2e => e2e/src}/api/specs/shared-link.e2e-spec.ts (53%) delete mode 100644 server/e2e/api/specs/person.e2e-spec.ts diff --git a/server/e2e/api/specs/activity.e2e-spec.ts b/e2e/src/api/specs/activity.e2e-spec.ts similarity index 57% rename from server/e2e/api/specs/activity.e2e-spec.ts rename to e2e/src/api/specs/activity.e2e-spec.ts index 47d2d7a199..738411338f 100644 --- a/server/e2e/api/specs/activity.e2e-spec.ts +++ b/e2e/src/api/specs/activity.e2e-spec.ts @@ -1,79 +1,94 @@ -import { AlbumResponseDto, LoginResponseDto, ReactionType } from '@app/domain'; -import { ActivityController } from '@app/immich'; -import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { ActivityEntity } from '@app/infra/entities'; -import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { + ActivityCreateDto, + AlbumResponseDto, + AssetResponseDto, + LoginResponseDto, + ReactionType, + createActivity as create, + createAlbum, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -describe(`${ActivityController.name} (e2e)`, () => { - let server: any; +describe('/activity', () => { let admin: LoginResponseDto; - let asset: AssetFileUploadResponseDto; - let album: AlbumResponseDto; let nonOwner: LoginResponseDto; + let asset: AssetResponseDto; + let album: AlbumResponseDto; + + const createActivity = (dto: ActivityCreateDto, accessToken?: string) => + create( + { activityCreateDto: dto }, + { headers: asBearerAuth(accessToken || admin.accessToken) } + ); beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - asset = await api.assetApi.upload(server, admin.accessToken, 'example'); + apiUtils.setup(); + await dbUtils.reset(); - await api.userApi.create(server, admin.accessToken, userDto.user1); - nonOwner = await api.authApi.login(server, userDto.user1); - - album = await api.albumApi.create(server, admin.accessToken, { - albumName: 'Album 1', - assetIds: [asset.id], - sharedWithUserIds: [nonOwner.userId], - }); - }); - - afterAll(async () => { - await testApp.teardown(); + admin = await apiUtils.adminSetup(); + nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1); + asset = await apiUtils.createAsset(admin.accessToken); + album = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 1', + assetIds: [asset.id], + sharedWithUserIds: [nonOwner.userId], + }, + }, + { headers: asBearerAuth(admin.accessToken) } + ); }); beforeEach(async () => { - await testApp.reset({ entities: [ActivityEntity] }); + await dbUtils.reset(['activity']); }); describe('GET /activity', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get('/activity'); + const { status, body } = await request(app).get('/activity'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require an albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should reject an invalid albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') - .query({ albumId: uuidStub.invalid }) + .query({ albumId: uuidDto.invalid }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should reject an invalid assetId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') - .query({ albumId: uuidStub.notFound, assetId: uuidStub.invalid }) + .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['assetId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])) + ); }); it('should start off empty', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -82,22 +97,22 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should filter by album id', async () => { - const album2 = await api.albumApi.create(server, admin.accessToken, { - albumName: 'Album 2', - assetIds: [asset.id], - }); + const album2 = await createAlbum( + { + createAlbumDto: { + albumName: 'Album 2', + assetIds: [asset.id], + }, + }, + { headers: asBearerAuth(admin.accessToken) } + ); + const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - type: ReactionType.LIKE, - }), - api.activityApi.create(server, admin.accessToken, { - albumId: album2.id, - type: ReactionType.LIKE, - }), + createActivity({ albumId: album.id, type: ReactionType.Like }), + createActivity({ albumId: album2.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -108,15 +123,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by type=comment', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'comment', }), - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, type: 'comment' }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -127,15 +142,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by type=like', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, type: ReactionType.Like }), + createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'comment', }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, type: 'like' }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -146,18 +161,18 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by userId', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const response1 = await request(server) + const response1 = await request(app) .get('/activity') - .query({ albumId: album.id, userId: uuidStub.notFound }) + .query({ albumId: album.id, userId: uuidDto.notFound }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(response1.status).toEqual(200); expect(response1.body.length).toBe(0); - const response2 = await request(server) + const response2 = await request(app) .get('/activity') .query({ albumId: album.id, userId: admin.userId }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -169,15 +184,15 @@ describe(`${ActivityController.name} (e2e)`, () => { it('should filter by assetId', async () => { const [reaction] = await Promise.all([ - api.activityApi.create(server, admin.accessToken, { + createActivity({ albumId: album.id, assetId: asset.id, - type: ReactionType.LIKE, + type: ReactionType.Like, }), - api.activityApi.create(server, admin.accessToken, { albumId: album.id, type: ReactionType.LIKE }), + createActivity({ albumId: album.id, type: ReactionType.Like }), ]); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/activity') .query({ albumId: album.id, assetId: asset.id }) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -189,34 +204,45 @@ describe(`${ActivityController.name} (e2e)`, () => { describe('POST /activity', () => { it('should require authentication', async () => { - const { status, body } = await request(server).post('/activity'); + const { status, body } = await request(app).post('/activity'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require an albumId', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidStub.invalid }); + .send({ albumId: uuidDto.invalid }); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(expect.arrayContaining(['albumId must be a UUID']))); + expect(body).toEqual( + errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])) + ); }); it('should require a comment when type is comment', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: uuidStub.notFound, type: 'comment', comment: null }); + .send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(['comment must be a string', 'comment should not be empty'])); + expect(body).toEqual( + errorDto.badRequest([ + 'comment must be a string', + 'comment should not be empty', + ]) + ); }); it('should add a comment to an album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: album.id, type: 'comment', comment: 'This is my first comment' }); + .send({ + albumId: album.id, + type: 'comment', + comment: 'This is my first comment', + }); expect(status).toEqual(201); expect(body).toEqual({ id: expect.any(String), @@ -229,7 +255,7 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a like to an album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -245,11 +271,11 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should return a 200 for a duplicate like on the album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ albumId: album.id, type: ReactionType.Like }), + ]); + + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -258,12 +284,14 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should not confuse an album like with an asset like', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - assetId: asset.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ + albumId: album.id, + assetId: asset.id, + type: ReactionType.Like, + }), + ]); + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, type: 'like' }); @@ -272,10 +300,15 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a comment to an asset', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) - .send({ albumId: album.id, assetId: asset.id, type: 'comment', comment: 'This is my first comment' }); + .send({ + albumId: album.id, + assetId: asset.id, + type: 'comment', + comment: 'This is my first comment', + }); expect(status).toEqual(201); expect(body).toEqual({ id: expect.any(String), @@ -288,7 +321,7 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should add a like to an asset', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, assetId: asset.id, type: 'like' }); @@ -304,12 +337,15 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should return a 200 for a duplicate like on an asset', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { - albumId: album.id, - assetId: asset.id, - type: ReactionType.LIKE, - }); - const { status, body } = await request(server) + const [reaction] = await Promise.all([ + createActivity({ + albumId: album.id, + assetId: asset.id, + type: ReactionType.Like, + }), + ]); + + const { status, body } = await request(app) .post('/activity') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ albumId: album.id, assetId: asset.id, type: 'like' }); @@ -320,50 +356,52 @@ describe(`${ActivityController.name} (e2e)`, () => { describe('DELETE /activity/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/activity/${uuidStub.notFound}`); + const { status, body } = await request(app).delete( + `/activity/${uuidDto.notFound}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require a valid uuid', async () => { - const { status, body } = await request(server) - .delete(`/activity/${uuidStub.invalid}`) + const { status, body } = await request(app) + .delete(`/activity/${uuidDto.invalid}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(['id must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); }); it('should remove a comment from an album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(204); }); it('should remove a like from an album', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.LIKE, + type: ReactionType.Like, }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(204); }); it('should let the owner remove a comment by another user', async () => { - const reaction = await api.activityApi.create(server, nonOwner.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -371,28 +409,33 @@ describe(`${ActivityController.name} (e2e)`, () => { }); it('should not let a user remove a comment by another user', async () => { - const reaction = await api.activityApi.create(server, admin.accessToken, { + const reaction = await createActivity({ albumId: album.id, - type: ReactionType.COMMENT, + type: ReactionType.Comment, comment: 'This is a test comment', }); - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Not found or no activity.delete access')); + expect(body).toEqual( + errorDto.badRequest('Not found or no activity.delete access') + ); }); it('should let a non-owner remove their own comment', async () => { - const reaction = await api.activityApi.create(server, nonOwner.accessToken, { - albumId: album.id, - type: ReactionType.COMMENT, - comment: 'This is a test comment', - }); + const reaction = await createActivity( + { + albumId: album.id, + type: ReactionType.Comment, + comment: 'This is a test comment', + }, + nonOwner.accessToken + ); - const { status } = await request(server) + const { status } = await request(app) .delete(`/activity/${reaction.id}`) .set('Authorization', `Bearer ${nonOwner.accessToken}`); diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts new file mode 100644 index 0000000000..d384fde2dc --- /dev/null +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -0,0 +1,176 @@ +import { LoginResponseDto, PersonResponseDto } from '@immich/sdk'; +import { uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, dbUtils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +describe('/activity', () => { + let admin: LoginResponseDto; + let visiblePerson: PersonResponseDto; + let hiddenPerson: PersonResponseDto; + + beforeAll(async () => { + apiUtils.setup(); + await dbUtils.reset(); + admin = await apiUtils.adminSetup(); + }); + + beforeEach(async () => { + await dbUtils.reset(['person']); + + [visiblePerson, hiddenPerson] = await Promise.all([ + apiUtils.createPerson(admin.accessToken, { + name: 'visible_person', + }), + apiUtils.createPerson(admin.accessToken, { + name: 'hidden_person', + isHidden: true, + }), + ]); + + const asset = await apiUtils.createAsset(admin.accessToken); + + await Promise.all([ + dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }), + dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }), + ]); + }); + + describe('GET /person', () => { + beforeEach(async () => {}); + + it('should require authentication', async () => { + const { status, body } = await request(app).get('/person'); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return all people (including hidden)', async () => { + const { status, body } = await request(app) + .get('/person') + .set('Authorization', `Bearer ${admin.accessToken}`) + .query({ withHidden: true }); + + expect(status).toBe(200); + expect(body).toEqual({ + total: 2, + people: [ + expect.objectContaining({ name: 'visible_person' }), + expect.objectContaining({ name: 'hidden_person' }), + ], + }); + }); + + it('should return only visible people', async () => { + const { status, body } = await request(app) + .get('/person') + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + total: 2, + people: [expect.objectContaining({ name: 'visible_person' })], + }); + }); + }); + + describe('GET /person/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/person/${uuidDto.notFound}` + ); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should throw error if person with id does not exist', async () => { + const { status, body } = await request(app) + .get(`/person/${uuidDto.notFound}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should return person information', async () => { + const { status, body } = await request(app) + .get(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id })); + }); + }); + + describe('PUT /person/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put( + `/person/${uuidDto.notFound}` + ); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + for (const { key, type } of [ + { key: 'name', type: 'string' }, + { key: 'featureFaceAssetId', type: 'string' }, + { key: 'isHidden', type: 'boolean value' }, + ]) { + it(`should not allow null ${key}`, async () => { + const { status, body } = await request(app) + .put(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ [key]: null }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`])); + }); + } + + it('should not accept invalid birth dates', async () => { + for (const { birthDate, response } of [ + { birthDate: false, response: 'Not found or no person.write access' }, + { birthDate: 'false', response: ['birthDate must be a Date instance'] }, + { + birthDate: '123567', + response: 'Not found or no person.write access', + }, + { birthDate: 123567, response: 'Not found or no person.write access' }, + ]) { + const { status, body } = await request(app) + .put(`/person/${uuidDto.notFound}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(response)); + } + }); + + it('should update a date of birth', async () => { + const { status, body } = await request(app) + .put(`/person/${visiblePerson.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate: '1990-01-01T05:00:00.000Z' }); + expect(status).toBe(200); + expect(body).toMatchObject({ birthDate: '1990-01-01' }); + }); + + it('should clear a date of birth', async () => { + // TODO ironically this uses the update endpoint to create the person + const person = await apiUtils.createPerson(admin.accessToken, { + birthDate: new Date('1990-01-01').toISOString(), + }); + + expect(person.birthDate).toBeDefined(); + + const { status, body } = await request(app) + .put(`/person/${person.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ birthDate: null }); + expect(status).toBe(200); + expect(body).toMatchObject({ birthDate: null }); + }); + }); +}); diff --git a/server/e2e/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts similarity index 53% rename from server/e2e/api/specs/shared-link.e2e-spec.ts rename to e2e/src/api/specs/shared-link.e2e-spec.ts index 034b2f2637..df57c57137 100644 --- a/server/e2e/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -1,21 +1,24 @@ import { AlbumResponseDto, AssetResponseDto, - IAssetRepository, LoginResponseDto, + SharedLinkCreateDto, SharedLinkResponseDto, -} from '@app/domain'; -import { SharedLinkController } from '@app/immich'; -import { SharedLinkType } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; -import { errorStub, userDto, uuidStub } from '@test/fixtures'; -import { DateTime } from 'luxon'; + SharedLinkType, + createSharedLink as create, + createAlbum, + deleteUser, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; +import { beforeAll, describe, expect, it } from 'vitest'; -describe(`${SharedLinkController.name} (e2e)`, () => { - let server: any; +const createSharedLink = (dto: SharedLinkCreateDto, accessToken: string) => + create({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }); + +describe('/shared-link', () => { let admin: LoginResponseDto; let asset1: AssetResponseDto; let asset2: AssetResponseDto; @@ -30,97 +33,101 @@ describe(`${SharedLinkController.name} (e2e)`, () => { let linkWithAssets: SharedLinkResponseDto; let linkWithMetadata: SharedLinkResponseDto; let linkWithoutMetadata: SharedLinkResponseDto; - let app: INestApplication; beforeAll(async () => { - app = await testApp.create(); - server = app.getHttpServer(); - const assetRepository = app.get(IAssetRepository); + apiUtils.setup(); + await dbUtils.reset(); - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - - await Promise.all([ - api.userApi.create(server, admin.accessToken, userDto.user1), - api.userApi.create(server, admin.accessToken, userDto.user2), - ]); + admin = await apiUtils.adminSetup(); [user1, user2] = await Promise.all([ - api.authApi.login(server, userDto.user1), - api.authApi.login(server, userDto.user2), + apiUtils.userSetup(admin.accessToken, createUserDto.user1), + apiUtils.userSetup(admin.accessToken, createUserDto.user2), ]); [asset1, asset2] = await Promise.all([ - api.assetApi.create(server, user1.accessToken), - api.assetApi.create(server, user1.accessToken), + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), ]); - await assetRepository.upsertExif({ - assetId: asset1.id, - longitude: -108.400968333333, - latitude: 39.115, - orientation: '1', - dateTimeOriginal: DateTime.fromISO('2022-01-10T19:15:44.310Z').toJSDate(), - timeZone: 'UTC-4', - state: 'Mesa County, Colorado', - country: 'United States of America', - }); - [album, deletedAlbum, metadataAlbum] = await Promise.all([ - api.albumApi.create(server, user1.accessToken, { albumName: 'album' }), - api.albumApi.create(server, user2.accessToken, { albumName: 'deleted album' }), - api.albumApi.create(server, user1.accessToken, { albumName: 'metadata album', assetIds: [asset1.id] }), + createAlbum( + { createAlbumDto: { albumName: 'album' } }, + { headers: asBearerAuth(user1.accessToken) } + ), + createAlbum( + { createAlbumDto: { albumName: 'deleted album' } }, + { headers: asBearerAuth(user2.accessToken) } + ), + createAlbum( + { + createAlbumDto: { + albumName: 'metadata album', + assetIds: [asset1.id], + }, + }, + { headers: asBearerAuth(user1.accessToken) } + ), ]); - [linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] = - await Promise.all([ - api.sharedLinkApi.create(server, user2.accessToken, { - type: SharedLinkType.ALBUM, - albumId: deletedAlbum.id, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.INDIVIDUAL, - assetIds: [asset1.id], - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, - albumId: album.id, - password: 'foo', - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, + [ + linkWithDeletedAlbum, + linkWithAlbum, + linkWithAssets, + linkWithPassword, + linkWithMetadata, + linkWithoutMetadata, + ] = await Promise.all([ + createSharedLink( + { type: SharedLinkType.Album, albumId: deletedAlbum.id }, + user2.accessToken + ), + createSharedLink( + { type: SharedLinkType.Album, albumId: album.id }, + user1.accessToken + ), + createSharedLink( + { type: SharedLinkType.Individual, assetIds: [asset1.id] }, + user1.accessToken + ), + createSharedLink( + { type: SharedLinkType.Album, albumId: album.id, password: 'foo' }, + user1.accessToken + ), + createSharedLink( + { + type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: true, - }), - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, + }, + user1.accessToken + ), + createSharedLink( + { + type: SharedLinkType.Album, albumId: metadataAlbum.id, showMetadata: false, - }), - ]); + }, + user1.accessToken + ), + ]); - await api.userApi.delete(server, admin.accessToken, user2.userId); - }); - - afterAll(async () => { - await testApp.teardown(); + await deleteUser( + { id: user2.userId }, + { headers: asBearerAuth(admin.accessToken) } + ); }); describe('GET /shared-link', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get('/shared-link'); + const { status, body } = await request(app).get('/shared-link'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should get all shared links created by user', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`); @@ -133,12 +140,12 @@ describe(`${SharedLinkController.name} (e2e)`, () => { expect.objectContaining({ id: linkWithPassword.id }), expect.objectContaining({ id: linkWithMetadata.id }), expect.objectContaining({ id: linkWithoutMetadata.id }), - ]), + ]) ); }); it('should not get shared links created by other users', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -149,7 +156,7 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('GET /shared-link/me', () => { it('should not require admin authentication', async () => { - const { status } = await request(server) + const { status } = await request(app) .get('/shared-link/me') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -157,52 +164,66 @@ describe(`${SharedLinkController.name} (e2e)`, () => { }); it('should get data for correct shared link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithAlbum.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithAlbum.key }); expect(status).toBe(200); expect(body).toEqual( expect.objectContaining({ album, userId: user1.userId, - type: SharedLinkType.ALBUM, - }), + type: SharedLinkType.Album, + }) ); }); it('should return unauthorized for incorrect shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link/me') .query({ key: linkWithAlbum.key + 'foo' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidShareKey); + expect(body).toEqual(errorDto.invalidShareKey); }); it('should return unauthorized if target has been soft deleted', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithDeletedAlbum.key }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidShareKey); + expect(body).toEqual(errorDto.invalidShareKey); }); it('should return unauthorized for password protected link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithPassword.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithPassword.key }); expect(status).toBe(401); - expect(body).toEqual(errorStub.invalidSharePassword); + expect(body).toEqual(errorDto.invalidSharePassword); }); it('should get data for correct password protected link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/shared-link/me') .query({ key: linkWithPassword.key, password: 'foo' }); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + expect(body).toEqual( + expect.objectContaining({ + album, + userId: user1.userId, + type: SharedLinkType.Album, + }) + ); }); it('should return metadata for album shared link', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithMetadata.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithMetadata.key }); expect(status).toBe(200); expect(body.assets).toHaveLength(1); @@ -211,22 +232,16 @@ describe(`${SharedLinkController.name} (e2e)`, () => { originalFileName: 'example', localDateTime: expect.any(String), fileCreatedAt: expect.any(String), - exifInfo: expect.objectContaining({ - longitude: -108.400968333333, - latitude: 39.115, - orientation: '1', - dateTimeOriginal: expect.any(String), - timeZone: 'UTC-4', - state: 'Mesa County, Colorado', - country: 'United States of America', - }), - }), + exifInfo: expect.any(Object), + }) ); expect(body.album).toBeDefined(); }); it('should not return metadata for album shared link without metadata', async () => { - const { status, body } = await request(server).get('/shared-link/me').query({ key: linkWithoutMetadata.key }); + const { status, body } = await request(app) + .get('/shared-link/me') + .query({ key: linkWithoutMetadata.key }); expect(status).toBe(200); expect(body.assets).toHaveLength(1); @@ -242,127 +257,150 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('GET /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get(`/shared-link/${linkWithAlbum.id}`); + const { status, body } = await request(app).get( + `/shared-link/${linkWithAlbum.id}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should get shared link by id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ album, userId: user1.userId, type: SharedLinkType.ALBUM })); + expect(body).toEqual( + expect.objectContaining({ + album, + userId: user1.userId, + type: SharedLinkType.Album, + }) + ); }); it('should not get shared link by id if user has not created the link or it does not exist', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Shared link not found' }) + ); }); }); describe('POST /shared-link', () => { it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') - .send({ type: SharedLinkType.ALBUM, albumId: uuidStub.notFound }); + .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should require a type and the correspondent asset/album id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should require an asset/album id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.ALBUM }); + .send({ type: SharedLinkType.Album }); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Invalid albumId' }) + ); }); it('should require a valid asset id', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.INDIVIDUAL, assetId: uuidStub.notFound }); + .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound }); expect(status).toBe(400); - expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' })); + expect(body).toEqual( + expect.objectContaining({ message: 'Invalid assetIds' }) + ); }); it('should create a shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .post('/shared-link') .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ type: SharedLinkType.ALBUM, albumId: album.id }); + .send({ type: SharedLinkType.Album, albumId: album.id }); expect(status).toBe(201); - expect(body).toEqual(expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId })); + expect(body).toEqual( + expect.objectContaining({ + type: SharedLinkType.Album, + userId: user1.userId, + }) + ); }); }); describe('PATCH /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .patch(`/shared-link/${linkWithAlbum.id}`) .send({ description: 'foo' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should fail if invalid link', async () => { - const { status, body } = await request(server) - .patch(`/shared-link/${uuidStub.notFound}`) + const { status, body } = await request(app) + .patch(`/shared-link/${uuidDto.notFound}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'foo' }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should update shared link', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .patch(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'foo' }); expect(status).toBe(200); expect(body).toEqual( - expect.objectContaining({ type: SharedLinkType.ALBUM, userId: user1.userId, description: 'foo' }), + expect.objectContaining({ + type: SharedLinkType.Album, + userId: user1.userId, + description: 'foo', + }) ); }); }); describe('PUT /shared-link/:id/assets', () => { it('should not add assets to shared link (album)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/shared-link/${linkWithAlbum.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + expect(body).toEqual(errorDto.badRequest('Invalid shared link type')); }); it('should add an assets to a shared link (individual)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/shared-link/${linkWithAssets.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); @@ -374,17 +412,17 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('DELETE /shared-link/:id/assets', () => { it('should not remove assets from a shared link (album)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/shared-link/${linkWithAlbum.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Invalid shared link type')); + expect(body).toEqual(errorDto.badRequest('Invalid shared link type')); }); it('should remove assets from a shared link (individual)', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/shared-link/${linkWithAssets.id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ assetIds: [asset2.id] }); @@ -396,23 +434,25 @@ describe(`${SharedLinkController.name} (e2e)`, () => { describe('DELETE /shared-link/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server).delete(`/shared-link/${linkWithAlbum.id}`); + const { status, body } = await request(app).delete( + `/shared-link/${linkWithAlbum.id}` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should fail if invalid link', async () => { - const { status, body } = await request(server) - .delete(`/shared-link/${uuidStub.notFound}`) + const { status, body } = await request(app) + .delete(`/shared-link/${uuidDto.notFound}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); + expect(body).toEqual(errorDto.badRequest()); }); it('should delete a shared link', async () => { - const { status } = await request(server) + const { status } = await request(app) .delete(`/shared-link/${linkWithAlbum.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 6c6d3b725d..1c7382879d 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -2,13 +2,15 @@ import { AssetResponseDto, CreateAssetDto, CreateUserDto, - LoginResponseDto, + PersonUpdateDto, createApiKey, + createPerson, createUser, defaults, login, setAdminOnboarding, signUpAdmin, + updatePerson, } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; import { spawn } from 'child_process'; @@ -45,7 +47,36 @@ export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); let client: pg.Client | null = null; export const dbUtils = { - reset: async () => { + createFace: async ({ + assetId, + personId, + }: { + assetId: string; + personId: string; + }) => { + if (!client) { + return; + } + + const vector = Array.from({ length: 512 }, Math.random); + const embedding = `[${vector.join(',')}]`; + + await client.query( + 'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', + [assetId, personId, embedding] + ); + }, + setPersonThumbnail: async (personId: string) => { + if (!client) { + return; + } + + await client.query( + `UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, + [personId] + ); + }, + reset: async (tables?: string[]) => { try { if (!client) { client = new pg.Client( @@ -54,14 +85,20 @@ export const dbUtils = { await client.connect(); } - for (const table of [ + tables = tables || [ + 'shared_links', + 'person', 'albums', 'assets', + 'asset_faces', + 'activity', 'api_keys', 'user_token', 'users', 'system_metadata', - ]) { + ]; + + for (const table of tables) { await client.query(`DELETE FROM ${table} CASCADE;`); } } catch (error) { @@ -165,6 +202,15 @@ export const apiUtils = { return body as AssetResponseDto; }, + createPerson: async (accessToken: string, dto: PersonUpdateDto) => { + // TODO fix createPerson to accept a body + const { id } = await createPerson({ headers: asBearerAuth(accessToken) }); + await dbUtils.setPersonThumbnail(id); + return updatePerson( + { id, personUpdateDto: dto }, + { headers: asBearerAuth(accessToken) } + ); + }, }; export const cliUtils = { diff --git a/server/e2e/api/specs/person.e2e-spec.ts b/server/e2e/api/specs/person.e2e-spec.ts deleted file mode 100644 index 73adcfab71..0000000000 --- a/server/e2e/api/specs/person.e2e-spec.ts +++ /dev/null @@ -1,191 +0,0 @@ -import { IPersonRepository, LoginResponseDto } from '@app/domain'; -import { PersonController } from '@app/immich'; -import { PersonEntity } from '@app/infra/entities'; -import { INestApplication } from '@nestjs/common'; -import { errorStub, uuidStub } from '@test/fixtures'; -import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; - -describe(`${PersonController.name}`, () => { - let app: INestApplication; - let server: any; - let loginResponse: LoginResponseDto; - let accessToken: string; - let personRepository: IPersonRepository; - let visiblePerson: PersonEntity; - let hiddenPerson: PersonEntity; - - beforeAll(async () => { - app = await testApp.create(); - server = app.getHttpServer(); - personRepository = app.get(IPersonRepository); - }); - - afterAll(async () => { - await testApp.teardown(); - }); - - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - loginResponse = await api.authApi.adminLogin(server); - accessToken = loginResponse.accessToken; - - const faceAsset = await api.assetApi.upload(server, accessToken, 'face_asset'); - visiblePerson = await personRepository.create({ - ownerId: loginResponse.userId, - name: 'visible_person', - thumbnailPath: '/thumbnail/face_asset', - }); - await personRepository.createFaces([ - { - assetId: faceAsset.id, - personId: visiblePerson.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - - hiddenPerson = await personRepository.create({ - ownerId: loginResponse.userId, - name: 'hidden_person', - isHidden: true, - thumbnailPath: '/thumbnail/face_asset', - }); - await personRepository.createFaces([ - { - assetId: faceAsset.id, - personId: hiddenPerson.id, - embedding: Array.from({ length: 512 }, Math.random), - }, - ]); - }); - - describe('GET /person', () => { - beforeEach(async () => {}); - - it('should require authentication', async () => { - const { status, body } = await request(server).get('/person'); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return all people (including hidden)', async () => { - const { status, body } = await request(server) - .get('/person') - .set('Authorization', `Bearer ${accessToken}`) - .query({ withHidden: true }); - - expect(status).toBe(200); - expect(body).toEqual({ - total: 2, - people: [ - expect.objectContaining({ name: 'visible_person' }), - expect.objectContaining({ name: 'hidden_person' }), - ], - }); - }); - - it('should return only visible people', async () => { - const { status, body } = await request(server).get('/person').set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - total: 2, - people: [expect.objectContaining({ name: 'visible_person' })], - }); - }); - }); - - describe('GET /person/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get(`/person/${uuidStub.notFound}`); - - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should throw error if person with id does not exist', async () => { - const { status, body } = await request(server) - .get(`/person/${uuidStub.notFound}`) - .set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest()); - }); - - it('should return person information', async () => { - const { status, body } = await request(server) - .get(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ id: visiblePerson.id })); - }); - }); - - describe('PUT /person/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).put(`/person/${uuidStub.notFound}`); - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - for (const { key, type } of [ - { key: 'name', type: 'string' }, - { key: 'featureFaceAssetId', type: 'string' }, - { key: 'isHidden', type: 'boolean value' }, - ]) { - it(`should not allow null ${key}`, async () => { - const { status, body } = await request(server) - .put(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ [key]: null }); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest([`${key} must be a ${type}`])); - }); - } - - it('should not accept invalid birth dates', async () => { - for (const { birthDate, response } of [ - { birthDate: false, response: 'Not found or no person.write access' }, - { birthDate: 'false', response: ['birthDate must be a Date instance'] }, - { birthDate: '123567', response: 'Not found or no person.write access' }, - { birthDate: 123567, response: 'Not found or no person.write access' }, - ]) { - const { status, body } = await request(server) - .put(`/person/${uuidStub.notFound}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate }); - expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest(response)); - } - }); - - it('should update a date of birth', async () => { - const { status, body } = await request(server) - .put(`/person/${visiblePerson.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate: '1990-01-01T05:00:00.000Z' }); - expect(status).toBe(200); - expect(body).toMatchObject({ birthDate: '1990-01-01' }); - }); - - it('should clear a date of birth', async () => { - const person = await personRepository.create({ - birthDate: new Date('1990-01-01'), - ownerId: loginResponse.userId, - }); - - expect(person.birthDate).toBeDefined(); - - const { status, body } = await request(server) - .put(`/person/${person.id}`) - .set('Authorization', `Bearer ${accessToken}`) - .send({ birthDate: null }); - expect(status).toBe(200); - expect(body).toMatchObject({ birthDate: null }); - }); - }); -}); From 855aa8e30a6ce811784634f58088957d7de4df33 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:28:16 +0100 Subject: [PATCH 15/59] fix(web): back button for gallery viewer (#7250) --- .../gallery-viewer/gallery-viewer.svelte | 23 ++++++++++++------- 1 file changed, 15 insertions(+), 8 deletions(-) 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 2fb4feb67a..04345c4c55 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 @@ -10,6 +10,7 @@ import justifiedLayout from 'justified-layout'; import { getAssetRatio } from '$lib/utils/asset-utils'; import { calculateWidth } from '$lib/utils/timeline-util'; + import { pushState, replaceState } from '$app/navigation'; const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>(); @@ -31,7 +32,7 @@ currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id); selectedAsset = assets[currentViewAssetIndex]; $showAssetViewer = true; - pushState(selectedAsset.id); + updateAssetState(selectedAsset.id, false); }; const selectAssetHandler = (event: CustomEvent) => { @@ -52,7 +53,7 @@ if (currentViewAssetIndex < assets.length - 1) { currentViewAssetIndex++; selectedAsset = assets[currentViewAssetIndex]; - pushState(selectedAsset.id); + updateAssetState(selectedAsset.id); } } catch (error) { handleError(error, 'Cannot navigate to the next asset'); @@ -64,22 +65,26 @@ if (currentViewAssetIndex > 0) { currentViewAssetIndex--; selectedAsset = assets[currentViewAssetIndex]; - pushState(selectedAsset.id); + updateAssetState(selectedAsset.id); } } catch (error) { handleError(error, 'Cannot navigate to previous asset'); } }; - const pushState = (assetId: string) => { - // add a URL to the browser's history - // changes the current URL in the address bar but doesn't perform any SvelteKit navigation - history.pushState(null, '', `${$page.url.pathname}/photos/${assetId}`); + const updateAssetState = (assetId: string, replace = true) => { + const route = `${$page.url.pathname}/photos/${assetId}`; + + if (replace) { + replaceState(route, {}); + } else { + pushState(route, {}); + } }; const closeViewer = () => { $showAssetViewer = false; - history.pushState(null, '', `${$page.url.pathname}`); + pushState(`${$page.url.pathname}${$page.url.search}`, {}); }; onDestroy(() => { @@ -105,6 +110,8 @@ })(); + + {#if assets.length > 0}
{#each assets as asset, i (i)} From 8f57bfb4966729945c694304518a7ea24868179d Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:29:22 +0100 Subject: [PATCH 16/59] fix(web): small issues everywhere (#7207) * multiple fix * fix: album re-render * fix: revert re-render album * fix: linter --------- Co-authored-by: Alex Tran --- docker/docker-compose.prod.yml | 4 +-- .../asset-viewer/activity-viewer.svelte | 3 +- .../asset-viewer/asset-viewer.svelte | 31 +++++-------------- .../asset-viewer/detail-panel.svelte | 21 ++++--------- web/src/lib/utils/autogrow.ts | 1 - .../(user)/albums/[albumId]/+page.svelte | 11 ++++--- 6 files changed, 25 insertions(+), 46 deletions(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 48a526c4c1..352309671b 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -17,7 +17,7 @@ x-server-build: &server-common services: immich-server: container_name: immich_server - command: [ "./start-server.sh" ] + command: [ "start.sh", "immich" ] <<: *server-common ports: - 2283:3001 @@ -27,7 +27,7 @@ services: immich-microservices: container_name: immich_microservices - command: [ "./start-microservices.sh" ] + command: [ "start.sh", "microservices" ] <<: *server-common # extends: # file: hwaccel.transcoding.yml diff --git a/web/src/lib/components/asset-viewer/activity-viewer.svelte b/web/src/lib/components/asset-viewer/activity-viewer.svelte index f102a980ba..186625b912 100644 --- a/web/src/lib/components/asset-viewer/activity-viewer.svelte +++ b/web/src/lib/components/asset-viewer/activity-viewer.svelte @@ -291,8 +291,9 @@ {disabled} bind:this={textArea} bind:value={message} + use:autoGrowHeight={'5px'} placeholder={disabled ? 'Comments are disabled' : 'Say something'} - on:input={() => autoGrowHeight(textArea)} + on:input={() => autoGrowHeight(textArea, '5px')} on:keypress={handleEnter} class="h-[18px] {disabled ? 'cursor-not-allowed' diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 1baabc9280..2c52cd4a3a 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,5 +1,4 @@ + +
($isShowDetail = false)} on:closeViewer={handleCloseViewer} - on:descriptionFocusIn={disableKeyDownEvent} - on:descriptionFocusOut={enableKeyDownEvent} />
{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 15d46da80b..17f3fa9fd4 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -40,6 +40,7 @@ import ChangeLocation from '../shared-components/change-location.svelte'; import UserAvatar from '../shared-components/user-avatar.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; + import { NotificationType, notificationController } from '../shared-components/notification/notification'; export let asset: AssetResponseDto; export let albums: AlbumResponseDto[] = []; @@ -101,9 +102,6 @@ const dispatch = createEventDispatcher<{ close: void; - descriptionFocusIn: void; - descriptionFocusOut: void; - click: AlbumResponseDto; closeViewer: void; }>(); @@ -139,19 +137,18 @@ showEditFaces = false; }; - const handleFocusIn = () => { - dispatch('descriptionFocusIn'); - }; - const handleFocusOut = async () => { textArea.blur(); if (description === originalDescription) { return; } originalDescription = description; - dispatch('descriptionFocusOut'); try { await updateAsset({ id: asset.id, updateAssetDto: { description } }); + notificationController.show({ + type: NotificationType.Info, + message: 'Asset description has been updated', + }); } catch (error) { handleError(error, 'Cannot update the description'); } @@ -220,7 +217,6 @@ class="max-h-[500px] w-full resize-none overflow-hidden border-b border-gray-500 bg-transparent text-base text-black outline-none transition-all focus:border-b-2 focus:border-immich-primary disabled:border-none dark:text-white dark:focus:border-immich-dark-primary" placeholder={isOwner ? 'Add a description' : ''} - on:focusin={handleFocusIn} on:focusout={handleFocusOut} on:input={() => autoGrowHeight(textArea)} bind:value={description} @@ -665,12 +661,7 @@

APPEARS IN

{#each albums as album} - -
dispatch('click', album)} - on:keydown={() => dispatch('click', album)} - > +
{album.albumName} { - textarea.scrollHeight; textarea.style.height = height; textarea.style.height = `${textarea.scrollHeight}px`; }; diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 9d6106be6d..52b780acc2 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -83,8 +83,6 @@ let album = data.album; let description = album.description; - $: album = data.album; - $: { if (!album.isActivityEnabled && $numberOfComments === 0) { isShowActivity = false; @@ -452,7 +450,10 @@ description, }, }); - + notificationController.show({ + type: NotificationType.Info, + message: 'Album description has been updated', + }); album.description = description; } catch (error) { handleError(error, 'Error updating album description'); @@ -672,7 +673,9 @@ placeholder="Add description" /> {:else if description} -

{description}

+

+ {description} +

{/if} {/if} From 06c134950a6d76d178b6c6885c451332af402335 Mon Sep 17 00:00:00 2001 From: Marcel Eeken <940593+MarcelEeken@users.noreply.github.com> Date: Wed, 21 Feb 2024 14:35:24 +0100 Subject: [PATCH 17/59] Localize the output of the library count to make it more readable (#7305) --- web/src/lib/components/user-settings-page/library-list.svelte | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/user-settings-page/library-list.svelte b/web/src/lib/components/user-settings-page/library-list.svelte index 6205a7ab14..c11b41332f 100644 --- a/web/src/lib/components/user-settings-page/library-list.svelte +++ b/web/src/lib/components/user-settings-page/library-list.svelte @@ -1,6 +1,7 @@ -
+
(value = '')} on:submit|preventDefault={onSubmit} @@ -148,9 +144,9 @@ on:selectSearchTerm={({ detail: searchTerm }) => onHistoryTermClick(searchTerm)} /> {/if} - - {#if showFilter} - onSearch(detail)} /> - {/if} + + {#if showFilter} + onSearch(detail)} /> + {/if}
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index c433be947f..71b2c9d3a5 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -17,7 +17,6 @@ import { fly } from 'svelte/transition'; import Combobox, { type ComboBoxOption } from '../combobox.svelte'; import { DateTime } from 'luxon'; - import { searchQuery } from '$lib/stores/search.store'; enum MediaType { All = 'all', @@ -44,7 +43,7 @@ type SearchFilter = { context?: string; - people: PersonResponseDto[]; + people: (PersonResponseDto | Pick)[]; location: { country?: ComboBoxOption; @@ -69,6 +68,8 @@ mediaType: MediaType; }; + export let searchQuery: MetadataSearchDto | SmartSearchDto; + let suggestions: SearchSuggestion = { people: [], country: [], @@ -112,19 +113,19 @@ populateExistingFilters(); }); - const showSelectedPeopleFirst = () => { - suggestions.people.sort((a, _) => { + function orderBySelectedPeopleFirst>(people: T[]) { + return people.sort((a, _) => { if (filter.people.some((p) => p.id === a.id)) { return -1; } return 1; }); - }; + } const getPeople = async () => { try { const { people } = await getAllPeople({ withHidden: false }); - suggestions.people = people; + suggestions.people = orderBySelectedPeopleFirst(people); } catch (error) { handleError(error, 'Failed to get people'); } @@ -133,14 +134,12 @@ const handlePeopleSelection = (id: string) => { if (filter.people.some((p) => p.id === id)) { filter.people = filter.people.filter((p) => p.id !== id); - showSelectedPeopleFirst(); return; } const person = suggestions.people.find((p) => p.id === id); if (person) { filter.people = [...filter.people, person]; - showSelectedPeopleFirst(); } }; @@ -280,35 +279,36 @@ }; function populateExistingFilters() { - if ($searchQuery) { + if (searchQuery) { + const personIds = 'personIds' in searchQuery && searchQuery.personIds ? searchQuery.personIds : []; + filter = { - context: 'query' in $searchQuery ? $searchQuery.query : '', - people: - 'personIds' in $searchQuery ? ($searchQuery.personIds?.map((id) => ({ id })) as PersonResponseDto[]) : [], + context: 'query' in searchQuery ? searchQuery.query : '', + people: orderBySelectedPeopleFirst(personIds.map((id) => ({ id }))), location: { - country: $searchQuery.country ? { label: $searchQuery.country, value: $searchQuery.country } : undefined, - state: $searchQuery.state ? { label: $searchQuery.state, value: $searchQuery.state } : undefined, - city: $searchQuery.city ? { label: $searchQuery.city, value: $searchQuery.city } : undefined, + country: searchQuery.country ? { label: searchQuery.country, value: searchQuery.country } : undefined, + state: searchQuery.state ? { label: searchQuery.state, value: searchQuery.state } : undefined, + city: searchQuery.city ? { label: searchQuery.city, value: searchQuery.city } : undefined, }, camera: { - make: $searchQuery.make ? { label: $searchQuery.make, value: $searchQuery.make } : undefined, - model: $searchQuery.model ? { label: $searchQuery.model, value: $searchQuery.model } : undefined, + make: searchQuery.make ? { label: searchQuery.make, value: searchQuery.make } : undefined, + model: searchQuery.model ? { label: searchQuery.model, value: searchQuery.model } : undefined, }, date: { - takenAfter: $searchQuery.takenAfter - ? DateTime.fromISO($searchQuery.takenAfter).toUTC().toFormat('yyyy-MM-dd') + takenAfter: searchQuery.takenAfter + ? DateTime.fromISO(searchQuery.takenAfter).toUTC().toFormat('yyyy-MM-dd') : undefined, - takenBefore: $searchQuery.takenBefore - ? DateTime.fromISO($searchQuery.takenBefore).toUTC().toFormat('yyyy-MM-dd') + takenBefore: searchQuery.takenBefore + ? DateTime.fromISO(searchQuery.takenBefore).toUTC().toFormat('yyyy-MM-dd') : undefined, }, - isArchive: $searchQuery.isArchived, - isFavorite: $searchQuery.isFavorite, - isNotInAlbum: 'isNotInAlbum' in $searchQuery ? $searchQuery.isNotInAlbum : undefined, + isArchive: searchQuery.isArchived, + isFavorite: searchQuery.isFavorite, + isNotInAlbum: 'isNotInAlbum' in searchQuery ? searchQuery.isNotInAlbum : undefined, mediaType: - $searchQuery.type === AssetTypeEnum.Image + searchQuery.type === AssetTypeEnum.Image ? MediaType.Image - : $searchQuery.type === AssetTypeEnum.Video + : searchQuery.type === AssetTypeEnum.Video ? MediaType.Video : MediaType.All, }; @@ -344,7 +344,7 @@ {#each peopleList as person (person.id)} {/each}
@@ -498,7 +498,7 @@

-
+

MEDIA TYPE

diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 239df2f844..6608550200 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -69,7 +69,6 @@ export enum QueryParameter { PREVIOUS_ROUTE = 'previousRoute', QUERY = 'query', SEARCHED_PEOPLE = 'searchedPeople', - SEARCH_TERM = 'q', SMART_SEARCH = 'smartSearch', PAGE = 'page', } diff --git a/web/src/lib/stores/search.store.ts b/web/src/lib/stores/search.store.ts index ded7dc17ae..41fd287f4c 100644 --- a/web/src/lib/stores/search.store.ts +++ b/web/src/lib/stores/search.store.ts @@ -1,8 +1,6 @@ -import type { MetadataSearchDto, SmartSearchDto } from '@immich/sdk'; import { persisted } from 'svelte-local-storage-store'; import { writable } from 'svelte/store'; export const savedSearchTerms = persisted('search-terms', [], {}); export const isSearchEnabled = writable(false); export const preventRaceConditionSearchBar = writable(false); -export const searchQuery = writable(undefined); diff --git a/web/src/routes/(user)/search/+page.svelte b/web/src/routes/(user)/search/+page.svelte index 354a84bd89..fd84071d32 100644 --- a/web/src/routes/(user)/search/+page.svelte +++ b/web/src/routes/(user)/search/+page.svelte @@ -1,5 +1,4 @@ + +
{#if isMultiSelectionMode}
@@ -252,44 +221,43 @@
goto(previousRoute)} backIcon={mdiArrowLeft}>
- +
{/if}
-{#if terms} -
- {#each Object.keys(terms) as key, index (index)} -
-
- {getHumanReadableSearchKey(key)} -
- - {#if terms[key] !== true} -
- {#if key === 'takenAfter' || key === 'takenBefore'} - {getHumanReadableDate(terms[key])} - {:else if key === 'personIds'} - {#await getPersonName(terms[key]) then personName} - {personName} - {/await} - {:else} - {terms[key]} - {/if} -
- {/if} +
+ {#each getObjectKeys(terms) as key (key)} + {@const value = terms[key]} +
+
+ {getHumanReadableSearchKey(key)}
- {/each} -
-{/if} + + {#if value !== true} +
+ {#if (key === 'takenAfter' || key === 'takenBefore') && typeof value === 'string'} + {getHumanReadableDate(value)} + {:else if key === 'personIds' && Array.isArray(value)} + {#await getPersonName(value) then personName} + {personName} + {/await} + {:else} + {value} + {/if} +
+ {/if} +
+ {/each} +
- {#if albums && albums.length > 0} + {#if searchResultAlbums.length > 0}
ALBUMS
- {#each albums as album, index (album.id)} + {#each searchResultAlbums as album, index (album.id)} {/if}
- {#if searchResultAssets && searchResultAssets.length > 0} + {#if isLoading} +
+ +
+ {:else if searchResultAssets.length > 0} { +export const load = (async () => { await authenticate(); - const url = new URL(data.url.href); - const term = - url.searchParams.get(QueryParameter.SEARCH_TERM) || url.searchParams.get(QueryParameter.QUERY) || undefined; - let results: SearchResponseDto | null = null; - if (term) { - const payload = JSON.parse(term) as SmartSearchDto | MetadataSearchDto; - searchQuery.set(payload); - - results = - payload && 'query' in payload - ? await searchSmart({ smartSearchDto: { ...payload, withExif: true, isVisible: true } }) - : await searchMetadata({ metadataSearchDto: { ...payload, withExif: true, isVisible: true } }); - } - return { - term, - results, meta: { title: 'Search', }, From 173b47033a8d5910c2d46e8ed4252eaf206d27ec Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 21 Feb 2024 16:52:38 +0100 Subject: [PATCH 19/59] fix(server): search with same face multiple times (#7306) --- server/src/infra/infra.utils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/infra/infra.utils.ts b/server/src/infra/infra.utils.ts index 1538958e0f..745f5a38ff 100644 --- a/server/src/infra/infra.utils.ts +++ b/server/src/infra/infra.utils.ts @@ -213,9 +213,9 @@ export function searchAssetBuilder( if (personIds && personIds.length > 0) { builder .leftJoin(`${builder.alias}.faces`, 'faces') - .andWhere('faces.personId IN (:...personIds)', { personIds: personIds }) + .andWhere('faces.personId IN (:...personIds)', { personIds }) .addGroupBy(`${builder.alias}.id`) - .having('COUNT(faces.id) = :personCount', { personCount: personIds.length }); + .having('COUNT(DISTINCT faces.personId) = :personCount', { personCount: personIds.length }); if (withExif) { builder.addGroupBy('exifInfo.assetId'); From 546edc2e9104f28f0846f228ffb86e197e1bdd98 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 21 Feb 2024 16:52:13 -0500 Subject: [PATCH 20/59] refactor: album e2e (#7320) * refactor: album e2e * refactor: user e2e --- .../src}/api/specs/album.e2e-spec.ts | 493 +++++++++++------- e2e/src/api/specs/shared-link.e2e-spec.ts | 62 +-- e2e/src/api/specs/user.e2e-spec.ts | 117 ++--- e2e/src/utils.ts | 14 + 4 files changed, 390 insertions(+), 296 deletions(-) rename {server/e2e => e2e/src}/api/specs/album.e2e-spec.ts (52%) diff --git a/server/e2e/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts similarity index 52% rename from server/e2e/api/specs/album.e2e-spec.ts rename to e2e/src/api/specs/album.e2e-spec.ts index 312816035c..c131edc49c 100644 --- a/server/e2e/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -1,11 +1,15 @@ -import { AlbumResponseDto, LoginResponseDto } from '@app/domain'; -import { AlbumController } from '@app/immich'; -import { AssetFileUploadResponseDto } from '@app/immich/api-v1/asset/response-dto/asset-file-upload-response.dto'; -import { SharedLinkType } from '@app/infra/entities'; -import { errorStub, userDto, uuidStub } from '@test/fixtures'; +import { + AlbumResponseDto, + AssetResponseDto, + LoginResponseDto, + SharedLinkType, + deleteUser, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { api } from '../../client'; -import { testApp } from '../utils'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; const user1SharedUser = 'user1SharedUser'; const user1SharedLink = 'user1SharedLink'; @@ -14,193 +18,327 @@ const user2SharedUser = 'user2SharedUser'; const user2SharedLink = 'user2SharedLink'; const user2NotShared = 'user2NotShared'; -describe(`${AlbumController.name} (e2e)`, () => { - let server: any; +describe('/album', () => { let admin: LoginResponseDto; let user1: LoginResponseDto; - let user1Asset: AssetFileUploadResponseDto; + let user1Asset1: AssetResponseDto; + let user1Asset2: AssetResponseDto; let user1Albums: AlbumResponseDto[]; let user2: LoginResponseDto; let user2Albums: AlbumResponseDto[]; + let user3: LoginResponseDto; // deleted beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); - }); + apiUtils.setup(); + await dbUtils.reset(); - afterAll(async () => { - await testApp.teardown(); - }); + admin = await apiUtils.adminSetup(); - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - - await Promise.all([ - api.userApi.create(server, admin.accessToken, userDto.user1), - api.userApi.create(server, admin.accessToken, userDto.user2), + [user1, user2, user3] = await Promise.all([ + apiUtils.userSetup(admin.accessToken, createUserDto.user1), + apiUtils.userSetup(admin.accessToken, createUserDto.user2), + apiUtils.userSetup(admin.accessToken, createUserDto.user3), ]); - [user1, user2] = await Promise.all([ - api.authApi.login(server, userDto.user1), - api.authApi.login(server, userDto.user2), + [user1Asset1, user1Asset2] = await Promise.all([ + apiUtils.createAsset(user1.accessToken), + apiUtils.createAsset(user1.accessToken), ]); - user1Asset = await api.assetApi.upload(server, user1.accessToken, 'example'); - const albums = await Promise.all([ // user 1 - api.albumApi.create(server, user1.accessToken, { + apiUtils.createAlbum(user1.accessToken, { albumName: user1SharedUser, sharedWithUserIds: [user2.userId], - assetIds: [user1Asset.id], + assetIds: [user1Asset1.id], + }), + apiUtils.createAlbum(user1.accessToken, { + albumName: user1SharedLink, + assetIds: [user1Asset1.id], + }), + apiUtils.createAlbum(user1.accessToken, { + albumName: user1NotShared, + assetIds: [user1Asset1.id, user1Asset2.id], }), - api.albumApi.create(server, user1.accessToken, { albumName: user1SharedLink, assetIds: [user1Asset.id] }), - api.albumApi.create(server, user1.accessToken, { albumName: user1NotShared, assetIds: [user1Asset.id] }), // user 2 - api.albumApi.create(server, user2.accessToken, { + apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedUser, sharedWithUserIds: [user1.userId], - assetIds: [user1Asset.id], + assetIds: [user1Asset1.id], + }), + apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }), + apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }), + + // user 3 + apiUtils.createAlbum(user3.accessToken, { + albumName: 'Deleted', + sharedWithUserIds: [user1.userId], }), - api.albumApi.create(server, user2.accessToken, { albumName: user2SharedLink }), - api.albumApi.create(server, user2.accessToken, { albumName: user2NotShared }), ]); user1Albums = albums.slice(0, 3); - user2Albums = albums.slice(3); + user2Albums = albums.slice(3, 6); await Promise.all([ // add shared link to user1SharedLink album - api.sharedLinkApi.create(server, user1.accessToken, { - type: SharedLinkType.ALBUM, + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Album, albumId: user1Albums[1].id, }), - // add shared link to user2SharedLink album - api.sharedLinkApi.create(server, user2.accessToken, { - type: SharedLinkType.ALBUM, + apiUtils.createSharedLink(user2.accessToken, { + type: SharedLinkType.Album, albumId: user2Albums[1].id, }), ]); + + await deleteUser( + { id: user3.userId }, + { headers: asBearerAuth(admin.accessToken) } + ); }); describe('GET /album', () => { it('should require authentication', async () => { - const { status, body } = await request(server).get('/album'); + const { status, body } = await request(app).get('/album'); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should reject an invalid shared param', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/album?shared=invalid') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(['shared must be a boolean value'])); + expect(body).toEqual( + errorDto.badRequest(['shared must be a boolean value']) + ); }); it('should reject an invalid assetId param', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/album?assetId=invalid') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toEqual(400); - expect(body).toEqual(errorStub.badRequest(['assetId must be a UUID'])); + expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID'])); }); it('should not return shared albums with a deleted owner', async () => { - await api.userApi.delete(server, admin.accessToken, user1.userId); - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/album?shared=true') - .set('Authorization', `Bearer ${user2.accessToken}`); + .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(1); + expect(body).toHaveLength(3); expect(body).toEqual( expect.arrayContaining([ - expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedLink, shared: true }), - ]), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedLink, + shared: true, + }), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedUser, + shared: true, + }), + expect.objectContaining({ + ownerId: user2.userId, + albumName: user2SharedUser, + shared: true, + }), + ]) ); }); it('should return the album collection including owned and shared', async () => { - const { status, body } = await request(server).get('/album').set('Authorization', `Bearer ${user1.accessToken}`); + const { status, body } = await request(app) + .get('/album') + .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(3); expect(body).toEqual( expect.arrayContaining([ - expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), - expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), - expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), - ]), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedUser, + shared: true, + }), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedLink, + shared: true, + }), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1NotShared, + shared: false, + }), + ]) ); }); it('should return the album collection filtered by shared', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/album?shared=true') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(3); expect(body).toEqual( expect.arrayContaining([ - expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedUser, shared: true }), - expect.objectContaining({ ownerId: user1.userId, albumName: user1SharedLink, shared: true }), - expect.objectContaining({ ownerId: user2.userId, albumName: user2SharedUser, shared: true }), - ]), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedUser, + shared: true, + }), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1SharedLink, + shared: true, + }), + expect.objectContaining({ + ownerId: user2.userId, + albumName: user2SharedUser, + shared: true, + }), + ]) ); }); it('should return the album collection filtered by NOT shared', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .get('/album?shared=false') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(1); expect(body).toEqual( expect.arrayContaining([ - expect.objectContaining({ ownerId: user1.userId, albumName: user1NotShared, shared: false }), - ]), + expect.objectContaining({ + ownerId: user1.userId, + albumName: user1NotShared, + shared: false, + }), + ]) ); }); it('should return the album collection filtered by assetId', async () => { - const asset = await api.assetApi.upload(server, user1.accessToken, 'example2'); - await api.albumApi.addAssets(server, user1.accessToken, user1Albums[0].id, { ids: [asset.id] }); - const { status, body } = await request(server) - .get(`/album?assetId=${asset.id}`) + const { status, body } = await request(app) + .get(`/album?assetId=${user1Asset2.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(1); }); it('should return the album collection filtered by assetId and ignores shared=true', async () => { - const { status, body } = await request(server) - .get(`/album?shared=true&assetId=${user1Asset.id}`) + const { status, body } = await request(app) + .get(`/album?shared=true&assetId=${user1Asset1.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(4); }); it('should return the album collection filtered by assetId and ignores shared=false', async () => { - const { status, body } = await request(server) - .get(`/album?shared=false&assetId=${user1Asset.id}`) + const { status, body } = await request(app) + .get(`/album?shared=false&assetId=${user1Asset1.id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); expect(body).toHaveLength(4); }); }); + describe('GET /album/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/album/${user1Albums[0].id}` + ); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return album info for own album', async () => { + const { status, body } = await request(app) + .get(`/album/${user1Albums[0].id}?withoutAssets=false`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user1Albums[0], + assets: [expect.objectContaining(user1Albums[0].assets[0])], + }); + }); + + it('should return album info for shared album', async () => { + const { status, body } = await request(app) + .get(`/album/${user2Albums[0].id}?withoutAssets=false`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user2Albums[0], + assets: [expect.objectContaining(user2Albums[0].assets[0])], + }); + }); + + it('should return album info with assets when withoutAssets is undefined', async () => { + const { status, body } = await request(app) + .get(`/album/${user1Albums[0].id}`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user1Albums[0], + assets: [expect.objectContaining(user1Albums[0].assets[0])], + }); + }); + + it('should return album info without assets when withoutAssets is true', async () => { + const { status, body } = await request(app) + .get(`/album/${user1Albums[0].id}?withoutAssets=true`) + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ + ...user1Albums[0], + assets: [], + assetCount: 1, + }); + }); + }); + + describe('GET /album/count', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/album/count'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should return total count of albums the user has access to', async () => { + const { status, body } = await request(app) + .get('/album/count') + .set('Authorization', `Bearer ${user1.accessToken}`); + + expect(status).toBe(200); + expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 }); + }); + }); + describe('POST /album', () => { it('should require authentication', async () => { - const { status, body } = await request(server).post('/album').send({ albumName: 'New album' }); + const { status, body } = await request(app) + .post('/album') + .send({ albumName: 'New album' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should create an album', async () => { - const body = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' }); + const { status, body } = await request(app) + .post('/album') + .send({ albumName: 'New album' }) + .set('Authorization', `Bearer ${user1.accessToken}`); + expect(status).toBe(201); expect(body).toEqual({ id: expect.any(String), createdAt: expect.any(String), @@ -220,113 +358,56 @@ describe(`${AlbumController.name} (e2e)`, () => { }); }); - describe('GET /album/count', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get('/album/count'); - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return total count of albums the user has access to', async () => { - const { status, body } = await request(server) - .get('/album/count') - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ owned: 3, shared: 3, notShared: 1 }); - }); - }); - - describe('GET /album/:id', () => { - it('should require authentication', async () => { - const { status, body } = await request(server).get(`/album/${user1Albums[0].id}`); - expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should return album info for own album', async () => { - const { status, body } = await request(server) - .get(`/album/${user1Albums[0].id}?withoutAssets=false`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] }); - }); - - it('should return album info for shared album', async () => { - const { status, body } = await request(server) - .get(`/album/${user2Albums[0].id}?withoutAssets=false`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ ...user2Albums[0], assets: [expect.objectContaining(user2Albums[0].assets[0])] }); - }); - - it('should return album info with assets when withoutAssets is undefined', async () => { - const { status, body } = await request(server) - .get(`/album/${user1Albums[0].id}`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ ...user1Albums[0], assets: [expect.objectContaining(user1Albums[0].assets[0])] }); - }); - - it('should return album info without assets when withoutAssets is true', async () => { - const { status, body } = await request(server) - .get(`/album/${user1Albums[0].id}?withoutAssets=true`) - .set('Authorization', `Bearer ${user1.accessToken}`); - - expect(status).toBe(200); - expect(body).toEqual({ - ...user1Albums[0], - assets: [], - assetCount: 1, - }); - }); - }); - describe('PUT /album/:id/assets', () => { it('should require authentication', async () => { - const { status, body } = await request(server).put(`/album/${user1Albums[0].id}/assets`); + const { status, body } = await request(app).put( + `/album/${user1Albums[0].id}/assets` + ); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should be able to add own asset to own album', async () => { - const asset = await api.assetApi.upload(server, user1.accessToken, 'example1'); - const { status, body } = await request(server) + const asset = await apiUtils.createAsset(user1.accessToken); + const { status, body } = await request(app) .put(`/album/${user1Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ ids: [asset.id] }); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]); + expect(body).toEqual([ + expect.objectContaining({ id: asset.id, success: true }), + ]); }); it('should be able to add own asset to shared album', async () => { - const asset = await api.assetApi.upload(server, user1.accessToken, 'example1'); - const { status, body } = await request(server) + const asset = await apiUtils.createAsset(user1.accessToken); + const { status, body } = await request(app) .put(`/album/${user2Albums[0].id}/assets`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ ids: [asset.id] }); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]); + expect(body).toEqual([ + expect.objectContaining({ id: asset.id, success: true }), + ]); }); }); describe('PATCH /album/:id', () => { it('should require authentication', async () => { - const { status, body } = await request(server) - .patch(`/album/${uuidStub.notFound}`) + const { status, body } = await request(app) + .patch(`/album/${uuidDto.notFound}`) .send({ albumName: 'New album name' }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should update an album', async () => { - const album = await api.albumApi.create(server, user1.accessToken, { albumName: 'New album' }); - const { status, body } = await request(server) + const album = await apiUtils.createAlbum(user1.accessToken, { + albumName: 'New album', + }); + const { status, body } = await request(app) .patch(`/album/${album.id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ @@ -345,52 +426,68 @@ describe(`${AlbumController.name} (e2e)`, () => { describe('DELETE /album/:id/assets', () => { it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/album/${user1Albums[0].id}/assets`) - .send({ ids: [user1Asset.id] }); + .send({ ids: [user1Asset1.id] }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); - }); - - it('should be able to remove own asset from own album', async () => { - const { status, body } = await request(server) - .delete(`/album/${user1Albums[0].id}/assets`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ ids: [user1Asset.id] }); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]); - }); - - it('should be able to remove own asset from shared album', async () => { - const { status, body } = await request(server) - .delete(`/album/${user2Albums[0].id}/assets`) - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ ids: [user1Asset.id] }); - - expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: true })]); + expect(body).toEqual(errorDto.unauthorized); }); it('should not be able to remove foreign asset from own album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/album/${user2Albums[0].id}/assets`) .set('Authorization', `Bearer ${user2.accessToken}`) - .send({ ids: [user1Asset.id] }); + .send({ ids: [user1Asset1.id] }); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]); + expect(body).toEqual([ + expect.objectContaining({ + id: user1Asset1.id, + success: false, + error: 'no_permission', + }), + ]); }); it('should not be able to remove foreign asset from foreign album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .delete(`/album/${user1Albums[0].id}/assets`) .set('Authorization', `Bearer ${user2.accessToken}`) - .send({ ids: [user1Asset.id] }); + .send({ ids: [user1Asset1.id] }); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: user1Asset.id, success: false, error: 'no_permission' })]); + expect(body).toEqual([ + expect.objectContaining({ + id: user1Asset1.id, + success: false, + error: 'no_permission', + }), + ]); + }); + + it('should be able to remove own asset from own album', async () => { + const { status, body } = await request(app) + .delete(`/album/${user1Albums[0].id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [user1Asset1.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: user1Asset1.id, success: true }), + ]); + }); + + it('should be able to remove own asset from shared album', async () => { + const { status, body } = await request(app) + .delete(`/album/${user2Albums[0].id}/assets`) + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ ids: [user1Asset1.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: user1Asset1.id, success: true }), + ]); }); }); @@ -398,51 +495,57 @@ describe(`${AlbumController.name} (e2e)`, () => { let album: AlbumResponseDto; beforeEach(async () => { - album = await api.albumApi.create(server, user1.accessToken, { albumName: 'testAlbum' }); + album = await apiUtils.createAlbum(user1.accessToken, { + albumName: 'testAlbum', + }); }); it('should require authentication', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/album/${user1Albums[0].id}/users`) .send({ sharedUserIds: [] }); expect(status).toBe(401); - expect(body).toEqual(errorStub.unauthorized); + expect(body).toEqual(errorDto.unauthorized); }); it('should be able to add user to own album', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/album/${album.id}/users`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ sharedUserIds: [user2.userId] }); expect(status).toBe(200); - expect(body).toEqual(expect.objectContaining({ sharedUsers: [expect.objectContaining({ id: user2.userId })] })); + expect(body).toEqual( + expect.objectContaining({ + sharedUsers: [expect.objectContaining({ id: user2.userId })], + }) + ); }); it('should not be able to share album with owner', async () => { - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/album/${album.id}/users`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ sharedUserIds: [user1.userId] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('Cannot be shared with owner')); + expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner')); }); it('should not be able to add existing user to shared album', async () => { - await request(server) + await request(app) .put(`/album/${album.id}/users`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ sharedUserIds: [user2.userId] }); - const { status, body } = await request(server) + const { status, body } = await request(app) .put(`/album/${album.id}/users`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ sharedUserIds: [user2.userId] }); expect(status).toBe(400); - expect(body).toEqual(errorStub.badRequest('User already added')); + expect(body).toEqual(errorDto.badRequest('User already added')); }); }); }); diff --git a/e2e/src/api/specs/shared-link.e2e-spec.ts b/e2e/src/api/specs/shared-link.e2e-spec.ts index df57c57137..e791c447ac 100644 --- a/e2e/src/api/specs/shared-link.e2e-spec.ts +++ b/e2e/src/api/specs/shared-link.e2e-spec.ts @@ -15,9 +15,6 @@ import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; import { beforeAll, describe, expect, it } from 'vitest'; -const createSharedLink = (dto: SharedLinkCreateDto, accessToken: string) => - create({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }); - describe('/shared-link', () => { let admin: LoginResponseDto; let asset1: AssetResponseDto; @@ -78,38 +75,33 @@ describe('/shared-link', () => { linkWithMetadata, linkWithoutMetadata, ] = await Promise.all([ - createSharedLink( - { type: SharedLinkType.Album, albumId: deletedAlbum.id }, - user2.accessToken - ), - createSharedLink( - { type: SharedLinkType.Album, albumId: album.id }, - user1.accessToken - ), - createSharedLink( - { type: SharedLinkType.Individual, assetIds: [asset1.id] }, - user1.accessToken - ), - createSharedLink( - { type: SharedLinkType.Album, albumId: album.id, password: 'foo' }, - user1.accessToken - ), - createSharedLink( - { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, - showMetadata: true, - }, - user1.accessToken - ), - createSharedLink( - { - type: SharedLinkType.Album, - albumId: metadataAlbum.id, - showMetadata: false, - }, - user1.accessToken - ), + apiUtils.createSharedLink(user2.accessToken, { + type: SharedLinkType.Album, + albumId: deletedAlbum.id, + }), + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Album, + albumId: album.id, + }), + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Individual, + assetIds: [asset1.id], + }), + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Album, + albumId: album.id, + password: 'foo', + }), + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Album, + albumId: metadataAlbum.id, + showMetadata: true, + }), + apiUtils.createSharedLink(user1.accessToken, { + type: SharedLinkType.Album, + albumId: metadataAlbum.id, + showMetadata: false, + }), ]); await deleteUser( diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 74e1646802..9bfb47284a 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -1,26 +1,31 @@ -import { - LoginResponseDto, - UserResponseDto, - createUser, - deleteUser, - getUserById, -} from '@immich/sdk'; +import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk'; import { createUserDto, userDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils'; import request from 'supertest'; -import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { beforeAll, describe, expect, it } from 'vitest'; describe('/server-info', () => { let admin: LoginResponseDto; + let deletedUser: LoginResponseDto; + let userToDelete: LoginResponseDto; + let nonAdmin: LoginResponseDto; beforeAll(async () => { apiUtils.setup(); - }); - - beforeEach(async () => { await dbUtils.reset(); admin = await apiUtils.adminSetup({ onboarding: false }); + + [deletedUser, nonAdmin, userToDelete] = await Promise.all([ + apiUtils.userSetup(admin.accessToken, createUserDto.user1), + apiUtils.userSetup(admin.accessToken, createUserDto.user2), + apiUtils.userSetup(admin.accessToken, createUserDto.user3), + ]); + + await deleteUser( + { id: deletedUser.userId }, + { headers: asBearerAuth(admin.accessToken) } + ); }); describe('GET /user', () => { @@ -30,60 +35,54 @@ describe('/server-info', () => { expect(body).toEqual(errorDto.unauthorized); }); - it('should start with the admin', async () => { + it('should get users', async () => { const { status, body } = await request(app) .get('/user') .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toEqual(200); - expect(body).toHaveLength(1); - expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' }); + expect(body).toHaveLength(4); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'admin@immich.cloud' }), + expect.objectContaining({ email: 'user1@immich.cloud' }), + expect.objectContaining({ email: 'user2@immich.cloud' }), + expect.objectContaining({ email: 'user3@immich.cloud' }), + ]) + ); }); it('should hide deleted users', async () => { - const user1 = await apiUtils.userSetup( - admin.accessToken, - createUserDto.user1 - ); - await deleteUser( - { id: user1.userId }, - { headers: asBearerAuth(admin.accessToken) } - ); - const { status, body } = await request(app) .get(`/user`) .query({ isAll: true }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(1); - expect(body[0]).toMatchObject({ email: 'admin@immich.cloud' }); + expect(body).toHaveLength(3); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'admin@immich.cloud' }), + expect.objectContaining({ email: 'user2@immich.cloud' }), + expect.objectContaining({ email: 'user3@immich.cloud' }), + ]) + ); }); it('should include deleted users', async () => { - const user1 = await apiUtils.userSetup( - admin.accessToken, - createUserDto.user1 - ); - await deleteUser( - { id: user1.userId }, - { headers: asBearerAuth(admin.accessToken) } - ); - const { status, body } = await request(app) .get(`/user`) .query({ isAll: false }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toHaveLength(2); - expect(body[0]).toMatchObject({ - id: user1.userId, - email: 'user1@immich.cloud', - deletedAt: expect.any(String), - }); - expect(body[1]).toMatchObject({ - id: admin.userId, - email: 'admin@immich.cloud', - }); + expect(body).toHaveLength(4); + expect(body).toEqual( + expect.arrayContaining([ + expect.objectContaining({ email: 'admin@immich.cloud' }), + expect.objectContaining({ email: 'user1@immich.cloud' }), + expect.objectContaining({ email: 'user2@immich.cloud' }), + expect.objectContaining({ email: 'user3@immich.cloud' }), + ]) + ); }); }); @@ -149,13 +148,13 @@ describe('/server-info', () => { .post(`/user`) .send({ isAdmin: true, - email: 'user1@immich.cloud', - password: 'Password123', + email: 'user4@immich.cloud', + password: 'password123', name: 'Immich', }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toMatchObject({ - email: 'user1@immich.cloud', + email: 'user4@immich.cloud', isAdmin: false, shouldChangePassword: true, }); @@ -181,18 +180,9 @@ describe('/server-info', () => { }); describe('DELETE /user/:id', () => { - let userToDelete: UserResponseDto; - - beforeEach(async () => { - userToDelete = await createUser( - { createUserDto: createUserDto.user1 }, - { headers: asBearerAuth(admin.accessToken) } - ); - }); - it('should require authentication', async () => { const { status, body } = await request(app).delete( - `/user/${userToDelete.id}` + `/user/${userToDelete.userId}` ); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); @@ -200,12 +190,12 @@ describe('/server-info', () => { it('should delete user', async () => { const { status, body } = await request(app) - .delete(`/user/${userToDelete.id}`) + .delete(`/user/${userToDelete.userId}`) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(200); - expect(body).toEqual({ - ...userToDelete, + expect(body).toMatchObject({ + id: userToDelete.userId, updatedAt: expect.any(String), deletedAt: expect.any(String), }); @@ -231,14 +221,9 @@ describe('/server-info', () => { } it('should not allow a non-admin to become an admin', async () => { - const user = await apiUtils.userSetup( - admin.accessToken, - createUserDto.user1 - ); - const { status, body } = await request(app) .put(`/user`) - .send({ isAdmin: true, id: user.userId }) + .send({ isAdmin: true, id: nonAdmin.userId }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(400); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 1c7382879d..a6374aff52 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -1,10 +1,14 @@ import { AssetResponseDto, + CreateAlbumDto, CreateAssetDto, CreateUserDto, PersonUpdateDto, + SharedLinkCreateDto, + createAlbum, createApiKey, createPerson, + createSharedLink, createUser, defaults, login, @@ -181,6 +185,11 @@ export const apiUtils = { { headers: asBearerAuth(accessToken) } ); }, + createAlbum: (accessToken: string, dto: CreateAlbumDto) => + createAlbum( + { createAlbumDto: dto }, + { headers: asBearerAuth(accessToken) } + ), createAsset: async ( accessToken: string, dto?: Omit @@ -211,6 +220,11 @@ export const apiUtils = { { headers: asBearerAuth(accessToken) } ); }, + createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) => + createSharedLink( + { sharedLinkCreateDto: dto }, + { headers: asBearerAuth(accessToken) } + ), }; export const cliUtils = { From 5c0c98473dc53e2e343a5ecbf475757a852aa106 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Wed, 21 Feb 2024 23:03:45 +0100 Subject: [PATCH 21/59] fix(server, web): people page (#7319) * fix: people page * fix: use locale * fix: e2e * fix: remove useless w-full * fix: don't count people without thumbnail * fix: es6 template string Co-authored-by: Jason Rasmussen --------- Co-authored-by: Jason Rasmussen --- e2e/src/api/specs/person.e2e-spec.ts | 2 ++ mobile/openapi/doc/PeopleResponseDto.md | 1 + .../lib/model/people_response_dto.dart | 10 ++++++- .../test/people_response_dto_test.dart | 5 ++++ open-api/immich-openapi-specs.json | 4 +++ open-api/typescript-sdk/axios-client/api.ts | 6 +++++ open-api/typescript-sdk/fetch-client.ts | 1 + server/src/domain/person/person.dto.ts | 3 ++- .../src/domain/person/person.service.spec.ts | 27 ++----------------- server/src/domain/person/person.service.ts | 9 +++---- .../domain/repositories/person.repository.ts | 7 ++++- .../infra/repositories/person.repository.ts | 22 +++++++++++---- server/src/infra/sql/person.repository.sql | 11 +++++++- .../components/faces-page/show-hide.svelte | 9 +++++-- web/src/routes/(user)/people/+page.svelte | 23 +++++++++++----- 15 files changed, 92 insertions(+), 48 deletions(-) diff --git a/e2e/src/api/specs/person.e2e-spec.ts b/e2e/src/api/specs/person.e2e-spec.ts index d384fde2dc..3f17eac220 100644 --- a/e2e/src/api/specs/person.e2e-spec.ts +++ b/e2e/src/api/specs/person.e2e-spec.ts @@ -56,6 +56,7 @@ describe('/activity', () => { expect(status).toBe(200); expect(body).toEqual({ total: 2, + hidden: 1, people: [ expect.objectContaining({ name: 'visible_person' }), expect.objectContaining({ name: 'hidden_person' }), @@ -71,6 +72,7 @@ describe('/activity', () => { expect(status).toBe(200); expect(body).toEqual({ total: 2, + hidden: 1, people: [expect.objectContaining({ name: 'visible_person' })], }); }); diff --git a/mobile/openapi/doc/PeopleResponseDto.md b/mobile/openapi/doc/PeopleResponseDto.md index 2f87f19993..78f9b2207c 100644 --- a/mobile/openapi/doc/PeopleResponseDto.md +++ b/mobile/openapi/doc/PeopleResponseDto.md @@ -8,6 +8,7 @@ import 'package:openapi/api.dart'; ## Properties Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- +**hidden** | **int** | | **people** | [**List**](PersonResponseDto.md) | | [default to const []] **total** | **int** | | diff --git a/mobile/openapi/lib/model/people_response_dto.dart b/mobile/openapi/lib/model/people_response_dto.dart index 80abedfc72..02a82cadf1 100644 --- a/mobile/openapi/lib/model/people_response_dto.dart +++ b/mobile/openapi/lib/model/people_response_dto.dart @@ -13,30 +13,36 @@ part of openapi.api; class PeopleResponseDto { /// Returns a new [PeopleResponseDto] instance. PeopleResponseDto({ + required this.hidden, this.people = const [], required this.total, }); + int hidden; + List people; int total; @override bool operator ==(Object other) => identical(this, other) || other is PeopleResponseDto && + other.hidden == hidden && _deepEquality.equals(other.people, people) && other.total == total; @override int get hashCode => // ignore: unnecessary_parenthesis + (hidden.hashCode) + (people.hashCode) + (total.hashCode); @override - String toString() => 'PeopleResponseDto[people=$people, total=$total]'; + String toString() => 'PeopleResponseDto[hidden=$hidden, people=$people, total=$total]'; Map toJson() { final json = {}; + json[r'hidden'] = this.hidden; json[r'people'] = this.people; json[r'total'] = this.total; return json; @@ -50,6 +56,7 @@ class PeopleResponseDto { final json = value.cast(); return PeopleResponseDto( + hidden: mapValueOfType(json, r'hidden')!, people: PersonResponseDto.listFromJson(json[r'people']), total: mapValueOfType(json, r'total')!, ); @@ -99,6 +106,7 @@ class PeopleResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'hidden', 'people', 'total', }; diff --git a/mobile/openapi/test/people_response_dto_test.dart b/mobile/openapi/test/people_response_dto_test.dart index ad669eeced..94db6eb86b 100644 --- a/mobile/openapi/test/people_response_dto_test.dart +++ b/mobile/openapi/test/people_response_dto_test.dart @@ -16,6 +16,11 @@ void main() { // final instance = PeopleResponseDto(); group('test PeopleResponseDto', () { + // int hidden + test('to test the property `hidden`', () async { + // TODO + }); + // List people (default value: const []) test('to test the property `people`', () async { // TODO diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 87f0fb4158..cac1d663bd 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8593,6 +8593,9 @@ }, "PeopleResponseDto": { "properties": { + "hidden": { + "type": "integer" + }, "people": { "items": { "$ref": "#/components/schemas/PersonResponseDto" @@ -8604,6 +8607,7 @@ } }, "required": [ + "hidden", "people", "total" ], diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index bef5ceab1b..c01b200d03 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -2801,6 +2801,12 @@ export type PathType = typeof PathType[keyof typeof PathType]; * @interface PeopleResponseDto */ export interface PeopleResponseDto { + /** + * + * @type {number} + * @memberof PeopleResponseDto + */ + 'hidden': number; /** * * @type {Array} diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index d7ecb906e3..0ee871ca60 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -524,6 +524,7 @@ export type UpdatePartnerDto = { inTimeline: boolean; }; export type PeopleResponseDto = { + hidden: number; people: PersonResponseDto[]; total: number; }; diff --git a/server/src/domain/person/person.dto.ts b/server/src/domain/person/person.dto.ts index 360a9b2348..b8ad8f0451 100644 --- a/server/src/domain/person/person.dto.ts +++ b/server/src/domain/person/person.dto.ts @@ -127,7 +127,8 @@ export class PersonStatisticsResponseDto { export class PeopleResponseDto { @ApiProperty({ type: 'integer' }) total!: number; - + @ApiProperty({ type: 'integer' }) + hidden!: number; people!: PersonResponseDto[]; } diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/domain/person/person.service.spec.ts index 5da8666016..ffda9034bd 100644 --- a/server/src/domain/person/person.service.spec.ts +++ b/server/src/domain/person/person.service.spec.ts @@ -114,35 +114,12 @@ describe(PersonService.name, () => { }); describe('getAll', () => { - it('should get all people with thumbnails', async () => { - personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.noThumbnail]); - personMock.getNumberOfPeople.mockResolvedValue(1); - await expect(sut.getAll(authStub.admin, { withHidden: undefined })).resolves.toEqual({ - total: 1, - people: [responseDto], - }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { - minimumFaceCount: 3, - withHidden: false, - }); - }); - it('should get all visible people with thumbnails', async () => { - personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); - personMock.getNumberOfPeople.mockResolvedValue(2); - await expect(sut.getAll(authStub.admin, { withHidden: false })).resolves.toEqual({ - total: 2, - people: [responseDto], - }); - expect(personMock.getAllForUser).toHaveBeenCalledWith(authStub.admin.user.id, { - minimumFaceCount: 3, - withHidden: false, - }); - }); it('should get all hidden and visible people with thumbnails', async () => { personMock.getAllForUser.mockResolvedValue([personStub.withName, personStub.hidden]); - personMock.getNumberOfPeople.mockResolvedValue(2); + personMock.getNumberOfPeople.mockResolvedValue({ total: 2, hidden: 1 }); await expect(sut.getAll(authStub.admin, { withHidden: true })).resolves.toEqual({ total: 2, + hidden: 1, people: [ responseDto, { diff --git a/server/src/domain/person/person.service.ts b/server/src/domain/person/person.service.ts index 6fbc409bf8..6300cc743c 100644 --- a/server/src/domain/person/person.service.ts +++ b/server/src/domain/person/person.service.ts @@ -82,15 +82,12 @@ export class PersonService { minimumFaceCount: machineLearning.facialRecognition.minFaces, withHidden: dto.withHidden || false, }); - const total = await this.repository.getNumberOfPeople(auth.user.id); - const persons: PersonResponseDto[] = people - // with thumbnails - .filter((person) => !!person.thumbnailPath) - .map((person) => mapPerson(person)); + const { total, hidden } = await this.repository.getNumberOfPeople(auth.user.id); return { - people: persons.filter((person) => dto.withHidden || !person.isHidden), + people: people.map((person) => mapPerson(person)), total, + hidden, }; } diff --git a/server/src/domain/repositories/person.repository.ts b/server/src/domain/repositories/person.repository.ts index 80240091a9..85c11fe921 100644 --- a/server/src/domain/repositories/person.repository.ts +++ b/server/src/domain/repositories/person.repository.ts @@ -28,6 +28,11 @@ export interface PersonStatistics { assets: number; } +export interface PeopleStatistics { + total: number; + hidden: number; +} + export interface IPersonRepository { getAll(pagination: PaginationOptions, options?: FindManyOptions): Paginated; getAllForUser(userId: string, options: PersonSearchOptions): Promise; @@ -54,7 +59,7 @@ export interface IPersonRepository { getRandomFace(personId: string): Promise; getStatistics(personId: string): Promise; reassignFace(assetFaceId: string, newPersonId: string): Promise; - getNumberOfPeople(userId: string): Promise; + getNumberOfPeople(userId: string): Promise; reassignFaces(data: UpdateFacesData): Promise; update(entity: Partial): Promise; } diff --git a/server/src/infra/repositories/person.repository.ts b/server/src/infra/repositories/person.repository.ts index 85423b74dd..63b3d570ef 100644 --- a/server/src/infra/repositories/person.repository.ts +++ b/server/src/infra/repositories/person.repository.ts @@ -3,6 +3,7 @@ import { IPersonRepository, Paginated, PaginationOptions, + PeopleStatistics, PersonNameSearchOptions, PersonSearchOptions, PersonStatistics, @@ -69,6 +70,7 @@ export class PersonRepository implements IPersonRepository { .addOrderBy("NULLIF(person.name, '') IS NULL", 'ASC') .addOrderBy('COUNT(face.assetId)', 'DESC') .addOrderBy("NULLIF(person.name, '')", 'ASC', 'NULLS LAST') + .andWhere("person.thumbnailPath != ''") .having("person.name != '' OR COUNT(face.assetId) >= :faces", { faces: options?.minimumFaceCount || 1 }) .groupBy('person.id') .limit(500); @@ -207,15 +209,25 @@ export class PersonRepository implements IPersonRepository { } @GenerateSql({ params: [DummyValue.UUID] }) - async getNumberOfPeople(userId: string): Promise { - return this.personRepository + async getNumberOfPeople(userId: string): Promise { + const items = await this.personRepository .createQueryBuilder('person') .leftJoin('person.faces', 'face') .where('person.ownerId = :userId', { userId }) + .innerJoin('face.asset', 'asset') + .andWhere('asset.isArchived = false') + .andWhere("person.thumbnailPath != ''") + .select('COUNT(DISTINCT(person.id))', 'total') + .addSelect('COUNT(DISTINCT(person.id)) FILTER (WHERE person.isHidden = true)', 'hidden') .having('COUNT(face.assetId) != 0') - .groupBy('person.id') - .withDeleted() - .getCount(); + .getRawOne(); + + const result: PeopleStatistics = { + total: items ? Number.parseInt(items.total) : 0, + hidden: items ? Number.parseInt(items.hidden) : 0, + }; + + return result; } create(entity: Partial): Promise { diff --git a/server/src/infra/sql/person.repository.sql b/server/src/infra/sql/person.repository.sql index bd4a523e86..c2cc45ee88 100644 --- a/server/src/infra/sql/person.repository.sql +++ b/server/src/infra/sql/person.repository.sql @@ -26,6 +26,7 @@ FROM WHERE "person"."ownerId" = $1 AND "asset"."isArchived" = false + AND "person"."thumbnailPath" != '' AND "person"."isHidden" = false GROUP BY "person"."id" @@ -344,12 +345,20 @@ LIMIT -- PersonRepository.getNumberOfPeople SELECT - COUNT(DISTINCT ("person"."id")) AS "cnt" + COUNT(DISTINCT ("person"."id")) AS "total", + COUNT(DISTINCT ("person"."id")) FILTER ( + WHERE + "person"."isHidden" = true + ) AS "hidden" FROM "person" "person" LEFT JOIN "asset_faces" "face" ON "face"."personId" = "person"."id" + INNER JOIN "assets" "asset" ON "asset"."id" = "face"."assetId" + AND ("asset"."deletedAt" IS NULL) WHERE "person"."ownerId" = $1 + AND "asset"."isArchived" = false + AND "person"."thumbnailPath" != '' HAVING COUNT("face"."assetId") != 0 diff --git a/web/src/lib/components/faces-page/show-hide.svelte b/web/src/lib/components/faces-page/show-hide.svelte index e766262280..bee1e98fb7 100644 --- a/web/src/lib/components/faces-page/show-hide.svelte +++ b/web/src/lib/components/faces-page/show-hide.svelte @@ -6,6 +6,7 @@ import { createEventDispatcher } from 'svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { mdiClose, mdiEye, mdiEyeOff, mdiRestart } from '@mdi/js'; + import { locale } from '$lib/stores/preferences.store'; const dispatch = createEventDispatcher<{ close: void; @@ -17,6 +18,7 @@ export let showLoadingSpinner: boolean; export let toggleVisibility: boolean; export let screenHeight: number; + export let countTotalPeople: number;
dispatch('close')} /> - +
+

Show & hide people

+

({countTotalPeople.toLocaleString($locale)})

+
@@ -47,7 +52,7 @@
-
+
diff --git a/web/src/routes/(user)/people/+page.svelte b/web/src/routes/(user)/people/+page.svelte index c28f4d1f6c..eba6ed2764 100644 --- a/web/src/routes/(user)/people/+page.svelte +++ b/web/src/routes/(user)/people/+page.svelte @@ -40,11 +40,13 @@ import { mdiAccountOff, mdiEyeOutline } from '@mdi/js'; import { onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; + import { locale } from '$lib/stores/preferences.store'; export let data: PageData; let people = data.people.people; let countTotalPeople = data.people.total; + let countHiddenPeople = data.people.hidden; let selectHidden = false; let initialHiddenValues: Record = {}; @@ -75,7 +77,7 @@ $: searchedPeopleLocal = searchName ? searchNameLocal(searchName, searchedPeople, maximumLengthSearchPeople) : []; - $: countVisiblePeople = people.filter((person) => !person.isHidden).length; + $: countVisiblePeople = countTotalPeople - countHiddenPeople; const onKeyboardPress = (event: KeyboardEvent) => handleKeyboardPress(event); @@ -152,6 +154,11 @@ for (const person of people) { if (person.isHidden !== initialHiddenValues[person.id]) { changed.push({ id: person.id, isHidden: person.isHidden }); + if (person.isHidden) { + countHiddenPeople++; + } else { + countHiddenPeople--; + } // Update the initial hidden values initialHiddenValues[person.id] = person.isHidden; @@ -203,10 +210,10 @@ const mergedPerson = await getPerson({ id: personToBeMergedIn.id }); - countVisiblePeople--; people = people.filter((person: PersonResponseDto) => person.id !== personToMerge.id); people = people.map((person: PersonResponseDto) => (person.id === personToBeMergedIn.id ? mergedPerson : person)); - + countHiddenPeople--; + countTotalPeople--; notificationController.show({ message: 'Merge people successfully', type: NotificationType.Info, @@ -274,7 +281,7 @@ } showChangeNameModal = false; - + countHiddenPeople++; notificationController.show({ message: 'Changed visibility successfully', type: NotificationType.Info, @@ -423,7 +430,10 @@ {/if} - + {#if countTotalPeople > 0}
@@ -522,9 +532,10 @@ on:change={handleToggleVisibility} bind:showLoadingSpinner bind:toggleVisibility + {countTotalPeople} screenHeight={innerHeight} > -
+
{#each people as person, index (person.id)}
- +
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index d0ce0e25a9..44994c2955 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -1,12 +1,7 @@ @@ -15,34 +10,32 @@ import Icon from '$lib/components/elements/icon.svelte'; import { clickOutside } from '$lib/utils/click-outside'; - import { mdiMagnify, mdiUnfoldMoreHorizontal } from '@mdi/js'; + import { mdiMagnify, mdiUnfoldMoreHorizontal, mdiClose } from '@mdi/js'; import { createEventDispatcher } from 'svelte'; + import IconButton from '../elements/buttons/icon-button.svelte'; - export let type: Type = 'button'; + export let id: string | undefined = undefined; export let options: ComboBoxOption[] = []; - export let selectedOption: ComboBoxOption | undefined = undefined; + export let selectedOption: ComboBoxOption | undefined; export let placeholder = ''; - export const label = ''; - export let noLabel = false; let isOpen = false; - let searchQuery = ''; + let searchQuery = selectedOption?.label || ''; $: filteredOptions = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())); const dispatch = createEventDispatcher<{ - select: ComboBoxOption; + select: ComboBoxOption | undefined; click: void; }>(); - let handleClick = () => { + const handleClick = () => { searchQuery = ''; - isOpen = !isOpen; + isOpen = true; dispatch('click'); }; let handleOutClick = () => { - searchQuery = ''; isOpen = false; }; @@ -51,49 +44,77 @@ dispatch('select', option); isOpen = false; }; + + const onClear = () => { + selectedOption = undefined; + searchQuery = ''; + dispatch('select', selectedOption); + };
- +
{#if isOpen}
-
-
-
- -
-
- - - -
-
- {#each filteredOptions as option (option.label)} - - {/each} -
+ {#if filteredOptions.length === 0} +
No results
+ {/if} + {#each filteredOptions as option (option.label)} + {@const selected = option.label === selectedOption?.label} + + {/each}
{/if}
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 71b2c9d3a5..00ceb6a872 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -404,8 +404,9 @@
-

Country

+
-

State

+
-

City

+
-

Make

+
-

Model

+ Date: Thu, 22 Feb 2024 08:16:56 -0500 Subject: [PATCH 25/59] chore(deps): update base-image to v20240222 (major) (#7338) chore(deps): update base-image to v20240222 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 9a7fc31fa2..7ea2795ea7 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240213@sha256:16646a37bae065b51e68cb2ba7a63027b29504d43a30644625382afbe326114a as dev +FROM ghcr.io/immich-app/base-server-dev:20240222@sha256:2ff467d6ae5c00a2317eb7b13cb40ba5be0fd33c160175dba621b1bf72bc1cd1 as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -40,7 +40,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240213@sha256:61d159d069c5b522f16de9733fb79feb0e82c0b099d16f026196f344d12a1e5e +FROM ghcr.io/immich-app/base-server-prod:20240222@sha256:9ae5eebf95cf7759eec9dcfbd9e48a722701075ac855209f2e0b01c631b76f5c WORKDIR /usr/src/app ENV NODE_ENV=production \ From ec55acc98c293ccbf3eb73c4fa1d788eba079639 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:48:27 +0100 Subject: [PATCH 26/59] perf(server): optimize mapAsset (#7331) --- .../asset/response-dto/asset-response.dto.ts | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/domain/asset/response-dto/asset-response.dto.ts index 94a9f8a42d..4cc0bd6672 100644 --- a/server/src/domain/asset/response-dto/asset-response.dto.ts +++ b/server/src/domain/asset/response-dto/asset-response.dto.ts @@ -73,23 +73,21 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; - const sanitizedAssetResponse: SanitizedAssetResponseDto = { - id: entity.id, - type: entity.type, - thumbhash: entity.thumbhash?.toString('base64') ?? null, - localDateTime: entity.localDateTime, - resized: !!entity.resizePath, - duration: entity.duration ?? '0:00:00.00000', - livePhotoVideoId: entity.livePhotoVideoId, - hasMetadata: false, - }; - if (stripMetadata) { + const sanitizedAssetResponse: SanitizedAssetResponseDto = { + id: entity.id, + type: entity.type, + thumbhash: entity.thumbhash?.toString('base64') ?? null, + localDateTime: entity.localDateTime, + resized: !!entity.resizePath, + duration: entity.duration ?? '0:00:00.00000', + livePhotoVideoId: entity.livePhotoVideoId, + hasMetadata: false, + }; return sanitizedAssetResponse as AssetResponseDto; } return { - ...sanitizedAssetResponse, id: entity.id, deviceAssetId: entity.deviceAssetId, ownerId: entity.ownerId, From e3cccba78c836f969af9f42654637b4f139ec45c Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 22 Feb 2024 14:50:46 +0100 Subject: [PATCH 27/59] fix(server): out of memory when unstacking assets (#7332) --- server/src/domain/asset/asset.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/domain/asset/asset.service.ts b/server/src/domain/asset/asset.service.ts index 325bb8ea4c..f328b5dcf6 100644 --- a/server/src/domain/asset/asset.service.ts +++ b/server/src/domain/asset/asset.service.ts @@ -326,7 +326,7 @@ export class AssetService { const stackIdsToCheckForDelete: string[] = []; if (removeParent) { (options as Partial).stack = null; - const assets = await this.assetRepository.getByIds(ids); + const assets = await this.assetRepository.getByIds(ids, { stack: true }); stackIdsToCheckForDelete.push(...new Set(assets.filter((a) => !!a.stackId).map((a) => a.stackId!))); // This updates the updatedAt column of the parents to indicate that one of its children is removed // All the unique parent's -> parent is set to null From 75947ab6c28740e46ef9108f93c107d8693b5684 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Thu, 22 Feb 2024 15:04:43 +0100 Subject: [PATCH 28/59] feat(web): search albums (#7322) * feat: search albums * pr feedback * fix: comparison * pr feedback * simplify * chore: more compact album padding --------- Co-authored-by: Jason Rasmussen --- .../{faces-page => elements}/search-bar.svelte | 9 +++++---- .../lib/components/faces-page/people-list.svelte | 5 +++-- .../lib/components/layouts/user-page-layout.svelte | 2 +- web/src/routes/(user)/albums/+page.svelte | 13 ++++++++++--- web/src/routes/(user)/albums/[albumId]/+page.svelte | 8 ++++---- web/src/routes/(user)/people/+page.svelte | 5 +++-- 6 files changed, 26 insertions(+), 16 deletions(-) rename web/src/lib/components/{faces-page => elements}/search-bar.svelte (88%) diff --git a/web/src/lib/components/faces-page/search-bar.svelte b/web/src/lib/components/elements/search-bar.svelte similarity index 88% rename from web/src/lib/components/faces-page/search-bar.svelte rename to web/src/lib/components/elements/search-bar.svelte index e1f999dbca..9c6eded224 100644 --- a/web/src/lib/components/faces-page/search-bar.svelte +++ b/web/src/lib/components/elements/search-bar.svelte @@ -1,12 +1,13 @@ diff --git a/web/src/routes/(user)/albums/+page.svelte b/web/src/routes/(user)/albums/+page.svelte index fa0115d828..07000d9136 100644 --- a/web/src/routes/(user)/albums/+page.svelte +++ b/web/src/routes/(user)/albums/+page.svelte @@ -43,11 +43,13 @@ import { flip } from 'svelte/animate'; import type { PageData } from './$types'; import { useAlbums } from './albums.bloc'; + import SearchBar from '$lib/components/elements/search-bar.svelte'; export let data: PageData; let shouldShowEditUserForm = false; let selectedAlbum: AlbumResponseDto; + let searchAlbum = ''; let sortByOptions: Record = { albumTitle: { @@ -180,6 +182,8 @@ } } + $: albumsFiltered = $albums.filter((album) => album.albumName.toLowerCase().includes(searchAlbum.toLowerCase())); + const searchSort = (searched: string): Sort => { for (const key in sortByOptions) { if (sortByOptions[key].title === searched) { @@ -243,6 +247,9 @@
+
@@ -285,7 +292,7 @@ {#if $albumViewSettings.view === AlbumViewMode.Cover}
- {#each $albums as album, index (album.id)} + {#each albumsFiltered as album, index (album.id)} {:else if $albumViewSettings.view === AlbumViewMode.List} - +
@@ -310,7 +317,7 @@ - {#each $albums as album (album.id)} + {#each albumsFiltered as album (album.id)} goto(`${AppRoute.ALBUMS}/${album.id}`)} diff --git a/web/src/routes/(user)/albums/[albumId]/+page.svelte b/web/src/routes/(user)/albums/[albumId]/+page.svelte index 52b780acc2..7c9ca39acd 100644 --- a/web/src/routes/(user)/albums/[albumId]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId]/+page.svelte @@ -603,7 +603,7 @@ e.key === 'Enter' && titleInput.blur()} on:blur={handleUpdateName} - class="w-[99%] border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned + class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" type="text" @@ -616,7 +616,7 @@ {#if album.assetCount > 0} - +

{getDateRange()}

·

{album.assetCount} items

@@ -625,7 +625,7 @@ {#if album.sharedUsers.length > 0 || (album.hasSharedLink && isOwned)} -
+
{#if album.hasSharedLink && isOwned} {#if isOwned}