From 7ca53ba5077136c853c2b28fce1e0ad9d331e591 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 3 Sep 2024 18:25:09 -0400 Subject: [PATCH 01/31] feat(server): support lightroom tags (#12288) --- server/src/services/metadata.service.spec.ts | 45 ++++++++++++++++++++ server/src/services/metadata.service.ts | 10 +++++ 2 files changed, 55 insertions(+) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 84b67be5cdfed..2fc95df00e629 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -449,6 +449,51 @@ describe(MetadataService.name, () => { }); }); + it('should extract hierarchy from HierarchicalSubject', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent|Child'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + + it('should extract ignore / characters in a HierarchicalSubject tag', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] }); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ + userId: 'user-id', + value: 'Mom|Dad', + parent: undefined, + }); + }); + + it('should ignore HierarchicalSubject when TagsList is present', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + it('should not apply motion photos if asset is video', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index de3babb138580..29aebc4a36abb 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -355,6 +355,16 @@ export class MetadataService { const tags: unknown[] = []; if (exifTags.TagsList) { tags.push(...exifTags.TagsList); + } else if (exifTags.HierarchicalSubject) { + tags.push( + exifTags.HierarchicalSubject.map((tag) => + tag + // convert | to / + .replaceAll('/', '') + .replaceAll('|', '/') + .replaceAll('', '|'), + ), + ); } else if (exifTags.Keywords) { let keywords = exifTags.Keywords; if (!Array.isArray(keywords)) { From 0b6cd74e4d1919ac6b34234eb84d9c6708412da4 Mon Sep 17 00:00:00 2001 From: Gavin Mogan Date: Tue, 3 Sep 2024 16:51:09 -0700 Subject: [PATCH 02/31] docs: ioredis link (#12291) Fix link to ioredis docs it was docker, now its ioredis! --- docs/docs/install/environment-variables.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 9a4b0b9360b78..a0cf71e044724 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -125,7 +125,7 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`. `REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration. -More info can be found in the upstream [ioredis][redis-api] documentation. +More info can be found in the upstream [ioredis] documentation. When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored. ::: @@ -226,4 +226,4 @@ to use use a Docker secret for the password in the Redis container. [docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234 [docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets [docker-secrets]: https://docs.docker.com/engine/swarm/secrets/ -[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository +[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis From e1ed7fa6ed95ff19ab216632caf354081d256948 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 19:58:03 -0400 Subject: [PATCH 03/31] fix(deps): update typescript-projects (#12274) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 7 +++--- server/package-lock.json | 46 ++++++++++++++++++++-------------------- server/package.json | 2 +- 3 files changed, 28 insertions(+), 27 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index c67c2b64fcb4e..05417ce1275a7 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -13698,9 +13698,10 @@ } }, "node_modules/prism-react-renderer": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz", - "integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz", + "integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==", + "license": "MIT", "dependencies": { "@types/prismjs": "^1.26.0", "clsx": "^2.0.0" diff --git a/server/package-lock.json b/server/package-lock.json index 45375b6964442..ca6a54c82c20e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,7 +24,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.53.0", "@opentelemetry/sdk-node": "^0.53.0", - "@react-email/components": "^0.0.23", + "@react-email/components": "^0.0.24", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -5070,9 +5070,9 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", - "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", + "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", "dependencies": { "prismjs": "1.29.0" }, @@ -5106,13 +5106,13 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", - "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", + "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", "dependencies": { "@react-email/body": "0.0.10", "@react-email/button": "0.0.17", - "@react-email/code-block": "0.0.7", + "@react-email/code-block": "0.0.8", "@react-email/code-inline": "0.0.4", "@react-email/column": "0.0.12", "@react-email/container": "0.0.14", @@ -5125,7 +5125,7 @@ "@react-email/link": "0.0.10", "@react-email/markdown": "0.0.12", "@react-email/preview": "0.0.11", - "@react-email/render": "1.0.0", + "@react-email/render": "1.0.1", "@react-email/row": "0.0.10", "@react-email/section": "0.0.14", "@react-email/tailwind": "0.1.0", @@ -5249,9 +5249,9 @@ } }, "node_modules/@react-email/render": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", - "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz", + "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==", "dependencies": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -19280,9 +19280,9 @@ "requires": {} }, "@react-email/code-block": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", - "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz", + "integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==", "requires": { "prismjs": "1.29.0" } @@ -19300,13 +19300,13 @@ "requires": {} }, "@react-email/components": { - "version": "0.0.23", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", - "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", + "version": "0.0.24", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz", + "integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==", "requires": { "@react-email/body": "0.0.10", "@react-email/button": "0.0.17", - "@react-email/code-block": "0.0.7", + "@react-email/code-block": "0.0.8", "@react-email/code-inline": "0.0.4", "@react-email/column": "0.0.12", "@react-email/container": "0.0.14", @@ -19319,7 +19319,7 @@ "@react-email/link": "0.0.10", "@react-email/markdown": "0.0.12", "@react-email/preview": "0.0.11", - "@react-email/render": "1.0.0", + "@react-email/render": "1.0.1", "@react-email/row": "0.0.10", "@react-email/section": "0.0.14", "@react-email/tailwind": "0.1.0", @@ -19389,9 +19389,9 @@ "requires": {} }, "@react-email/render": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", - "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz", + "integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==", "requires": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", diff --git a/server/package.json b/server/package.json index bfa6dd9e11dcb..58d7208adf91f 100644 --- a/server/package.json +++ b/server/package.json @@ -50,7 +50,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.53.0", "@opentelemetry/sdk-node": "^0.53.0", - "@react-email/components": "^0.0.23", + "@react-email/components": "^0.0.24", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", From 526cf23a9ebc9caf4dd8501a6a4737ba9b3ad806 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 3 Sep 2024 21:20:21 -0400 Subject: [PATCH 04/31] fix(server): public references in migrations (#12298) --- server/src/migrations/1661881837496-AddAssetChecksum.ts | 2 +- server/src/migrations/1670257571385-CreateTagsTable.ts | 4 ++-- server/src/migrations/1673150490490-AddSharedLinkTable.ts | 6 +++--- .../src/migrations/1675812532822-FixAlbumEntityTypeORM.ts | 8 ++++---- .../migrations/1676437878377-AppleContentIdentifier.ts | 2 +- .../migrations/1676848629119-ExifEntityDefinitionFixes.ts | 2 +- .../1676852143506-SmartInfoEntityDefinitionFixes.ts | 2 +- .../1677535643119-AddIndexForAlbumInSharedLinkTable.ts | 2 +- .../migrations/1684328185099-RequireChecksumNotNull.ts | 4 ++-- server/src/migrations/1692804658140-AddAuditTable.ts | 2 +- .../src/migrations/1696888644031-AddOriginalPathIndex.ts | 2 +- server/src/migrations/1698693294632-AddActivity.ts | 2 +- .../src/migrations/1700752078178-AddAssetFaceIndicies.ts | 4 ++-- server/src/migrations/1701665867595-AddExifCityIndex.ts | 2 +- server/src/migrations/1703035138085-AddAutoStackId.ts | 2 +- .../migrations/1705306747072-AddOriginalFileNameIndex.ts | 2 +- .../src/migrations/1705363967169-CreateAssetStackTable.ts | 2 +- server/src/migrations/1711637874206-AddMemoryTable.ts | 4 ++-- server/src/migrations/1715804005643-RemoveLibraryType.ts | 4 ++-- server/src/migrations/1724101822106-AddAssetFilesTable.ts | 2 +- server/src/migrations/1724790460210-NestedTagTable.ts | 4 ++-- 21 files changed, 32 insertions(+), 32 deletions(-) diff --git a/server/src/migrations/1661881837496-AddAssetChecksum.ts b/server/src/migrations/1661881837496-AddAssetChecksum.ts index 231aeecca79cf..2901b4f554038 100644 --- a/server/src/migrations/1661881837496-AddAssetChecksum.ts +++ b/server/src/migrations/1661881837496-AddAssetChecksum.ts @@ -11,7 +11,7 @@ export class AddAssetChecksum1661881837496 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`); + await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`); await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`); } } diff --git a/server/src/migrations/1670257571385-CreateTagsTable.ts b/server/src/migrations/1670257571385-CreateTagsTable.ts index 0585aecc8ca63..75fba9249c258 100644 --- a/server/src/migrations/1670257571385-CreateTagsTable.ts +++ b/server/src/migrations/1670257571385-CreateTagsTable.ts @@ -17,8 +17,8 @@ export class CreateTagsTable1670257571385 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`); + await queryRunner.query(`DROP INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4"`); + await queryRunner.query(`DROP INDEX "IDX_f8e8a9e893cb5c54907f1b798e"`); await queryRunner.query(`DROP TABLE "tag_asset"`); await queryRunner.query(`DROP TABLE "tags"`); } diff --git a/server/src/migrations/1673150490490-AddSharedLinkTable.ts b/server/src/migrations/1673150490490-AddSharedLinkTable.ts index a7508722d2c33..8d5bd2f5a5716 100644 --- a/server/src/migrations/1673150490490-AddSharedLinkTable.ts +++ b/server/src/migrations/1673150490490-AddSharedLinkTable.ts @@ -18,10 +18,10 @@ export class AddSharedLinkTable1673150490490 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`); await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`); await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`); - await queryRunner.query(`DROP INDEX "public"."IDX_c9fab4aa97ffd1b034f3d6581a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_5b7decce6c8d3db9593d6111a6"`); + await queryRunner.query(`DROP INDEX "IDX_c9fab4aa97ffd1b034f3d6581a"`); + await queryRunner.query(`DROP INDEX "IDX_5b7decce6c8d3db9593d6111a6"`); await queryRunner.query(`DROP TABLE "shared_link__asset"`); - await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_key"`); + await queryRunner.query(`DROP INDEX "IDX_sharedlink_key"`); await queryRunner.query(`DROP TABLE "shared_links"`); } diff --git a/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts b/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts index 3be6a2aa1d147..6f48ac736d804 100644 --- a/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts +++ b/server/src/migrations/1675812532822-FixAlbumEntityTypeORM.ts @@ -44,10 +44,10 @@ export class FixAlbumEntityTypeORM1675812532822 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621"`); await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_427c350ad49bd3935a50baab737"`); await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06"`); - await queryRunner.query(`DROP INDEX "public"."IDX_427c350ad49bd3935a50baab73"`); - await queryRunner.query(`DROP INDEX "public"."IDX_f48513bf9bccefd6ff3ad30bd0"`); - await queryRunner.query(`DROP INDEX "public"."IDX_e590fa396c6898fcd4a50e4092"`); - await queryRunner.query(`DROP INDEX "public"."IDX_4bd1303d199f4e72ccdf998c62"`); + await queryRunner.query(`DROP INDEX "IDX_427c350ad49bd3935a50baab73"`); + await queryRunner.query(`DROP INDEX "IDX_f48513bf9bccefd6ff3ad30bd0"`); + await queryRunner.query(`DROP INDEX "IDX_e590fa396c6898fcd4a50e4092"`); + await queryRunner.query(`DROP INDEX "IDX_4bd1303d199f4e72ccdf998c62"`); await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`); await queryRunner.query( diff --git a/server/src/migrations/1676437878377-AppleContentIdentifier.ts b/server/src/migrations/1676437878377-AppleContentIdentifier.ts index 40a4dce579c5b..8d11139878e19 100644 --- a/server/src/migrations/1676437878377-AppleContentIdentifier.ts +++ b/server/src/migrations/1676437878377-AppleContentIdentifier.ts @@ -9,7 +9,7 @@ export class AppleContentIdentifier1676437878377 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_live_photo_cid"`); + await queryRunner.query(`DROP INDEX "IDX_live_photo_cid"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "livePhotoCID"`); } } diff --git a/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts b/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts index 35d4c77eba363..947559ed2d5dd 100644 --- a/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts +++ b/server/src/migrations/1676848629119-ExifEntityDefinitionFixes.ts @@ -6,7 +6,7 @@ export class ExifEntityDefinitionFixes1676848629119 implements MigrationInterfac public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" SET NOT NULL`); - await queryRunner.query(`DROP INDEX "public"."IDX_c0117fdbc50b917ef9067740c4"`); + await queryRunner.query(`DROP INDEX "IDX_c0117fdbc50b917ef9067740c4"`); await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_28663352d85078ad0046dafafaa"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "id"`); await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`); diff --git a/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts b/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts index f89c7acdd2c78..e089619c6d3db 100644 --- a/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts +++ b/server/src/migrations/1676852143506-SmartInfoEntityDefinitionFixes.ts @@ -4,7 +4,7 @@ export class SmartInfoEntityDefinitionFixes1676852143506 implements MigrationInt name = 'SmartInfoEntityDefinitionFixes1676852143506' public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_5e3753aadd956110bf3ec0244a"`); + await queryRunner.query(`DROP INDEX "IDX_5e3753aadd956110bf3ec0244a"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_0beace66440e9713f5c40470e46"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN "id"`); await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`); diff --git a/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts b/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts index f3fb4a6c63232..986b5ebd20b58 100644 --- a/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts +++ b/server/src/migrations/1677535643119-AddIndexForAlbumInSharedLinkTable.ts @@ -8,7 +8,7 @@ export class AddIndexForAlbumInSharedLinkTable1677535643119 implements Migration } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_albumId"`); + await queryRunner.query(`DROP INDEX "IDX_sharedlink_albumId"`); } } diff --git a/server/src/migrations/1684328185099-RequireChecksumNotNull.ts b/server/src/migrations/1684328185099-RequireChecksumNotNull.ts index 6da8f326220f0..e691fff2b1f7f 100644 --- a/server/src/migrations/1684328185099-RequireChecksumNotNull.ts +++ b/server/src/migrations/1684328185099-RequireChecksumNotNull.ts @@ -4,13 +4,13 @@ export class RequireChecksumNotNull1684328185099 implements MigrationInterface { name = 'removeNotNullFromChecksumIndex1684328185099'; public async up(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`); + await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`); await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `); } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_8d3efe36c0755849395e6ea866"`); + await queryRunner.query(`DROP INDEX "IDX_8d3efe36c0755849395e6ea866"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`); await queryRunner.query( `CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`, diff --git a/server/src/migrations/1692804658140-AddAuditTable.ts b/server/src/migrations/1692804658140-AddAuditTable.ts index 71b8c7b2c63f0..d398051a79e27 100644 --- a/server/src/migrations/1692804658140-AddAuditTable.ts +++ b/server/src/migrations/1692804658140-AddAuditTable.ts @@ -9,7 +9,7 @@ export class AddAuditTable1692804658140 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_ownerId_createdAt"`); + await queryRunner.query(`DROP INDEX "IDX_ownerId_createdAt"`); await queryRunner.query(`DROP TABLE "audit"`); } diff --git a/server/src/migrations/1696888644031-AddOriginalPathIndex.ts b/server/src/migrations/1696888644031-AddOriginalPathIndex.ts index 826700ffe87d4..78e1c92ecb3ea 100644 --- a/server/src/migrations/1696888644031-AddOriginalPathIndex.ts +++ b/server/src/migrations/1696888644031-AddOriginalPathIndex.ts @@ -8,6 +8,6 @@ export class AddOriginalPathIndex1696888644031 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`); + await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`); } } diff --git a/server/src/migrations/1698693294632-AddActivity.ts b/server/src/migrations/1698693294632-AddActivity.ts index 46041570ead83..5556ef2b20850 100644 --- a/server/src/migrations/1698693294632-AddActivity.ts +++ b/server/src/migrations/1698693294632-AddActivity.ts @@ -15,7 +15,7 @@ export class AddActivity1698693294632 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_1af8519996fbfb3684b58df280b"`); await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`); await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_8091ea76b12338cb4428d33d782"`); - await queryRunner.query(`DROP INDEX "public"."IDX_activity_like"`); + await queryRunner.query(`DROP INDEX "IDX_activity_like"`); await queryRunner.query(`DROP TABLE "activity"`); } diff --git a/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts b/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts index 723b22b3d14d0..38dd915139991 100644 --- a/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts +++ b/server/src/migrations/1700752078178-AddAssetFaceIndicies.ts @@ -9,8 +9,8 @@ export class AddAssetFaceIndicies1700752078178 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); - await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`); + await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "IDX_bf339a24070dac7e71304ec530"`); } } diff --git a/server/src/migrations/1701665867595-AddExifCityIndex.ts b/server/src/migrations/1701665867595-AddExifCityIndex.ts index 9979762dc4f34..0899ea1e6b9db 100644 --- a/server/src/migrations/1701665867595-AddExifCityIndex.ts +++ b/server/src/migrations/1701665867595-AddExifCityIndex.ts @@ -8,7 +8,7 @@ export class AddExifCityIndex1701665867595 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."exif_city"`); + await queryRunner.query(`DROP INDEX "exif_city"`); } } diff --git a/server/src/migrations/1703035138085-AddAutoStackId.ts b/server/src/migrations/1703035138085-AddAutoStackId.ts index 666914261123b..d8c83ac56573b 100644 --- a/server/src/migrations/1703035138085-AddAutoStackId.ts +++ b/server/src/migrations/1703035138085-AddAutoStackId.ts @@ -9,7 +9,7 @@ export class AddAutoStackId1703035138085 implements MigrationInterface { } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_auto_stack_id"`); + await queryRunner.query(`DROP INDEX "IDX_auto_stack_id"`); await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "autoStackId"`); } diff --git a/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts b/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts index b465d429438c8..c62c01f50c1c7 100644 --- a/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts +++ b/server/src/migrations/1705306747072-AddOriginalFileNameIndex.ts @@ -8,6 +8,6 @@ export class AddOriginalFileNameIndex1705306747072 implements MigrationInterface } public async down(queryRunner: QueryRunner): Promise { - await queryRunner.query(`DROP INDEX "public"."IDX_4d66e76dada1ca180f67a205dc"`); + await queryRunner.query(`DROP INDEX "IDX_4d66e76dada1ca180f67a205dc"`); } } diff --git a/server/src/migrations/1705363967169-CreateAssetStackTable.ts b/server/src/migrations/1705363967169-CreateAssetStackTable.ts index 74c75d555ceee..d1591797ffa49 100644 --- a/server/src/migrations/1705363967169-CreateAssetStackTable.ts +++ b/server/src/migrations/1705363967169-CreateAssetStackTable.ts @@ -41,7 +41,7 @@ export class CreateAssetStackTable1705197515600 implements MigrationInterface { ); // update constraints - await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`); + await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`); await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`); await queryRunner.query( `ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack"("id") ON DELETE SET NULL ON UPDATE CASCADE`, diff --git a/server/src/migrations/1711637874206-AddMemoryTable.ts b/server/src/migrations/1711637874206-AddMemoryTable.ts index 6309cb5082a64..b1c5b437d736e 100644 --- a/server/src/migrations/1711637874206-AddMemoryTable.ts +++ b/server/src/migrations/1711637874206-AddMemoryTable.ts @@ -17,8 +17,8 @@ export class AddMemoryTable1711637874206 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`); await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`); await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`); - await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`); - await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`); + await queryRunner.query(`DROP INDEX "IDX_6942ecf52d75d4273de19d2c16"`); + await queryRunner.query(`DROP INDEX "IDX_984e5c9ab1f04d34538cd32334"`); await queryRunner.query(`DROP TABLE "memories_assets_assets"`); await queryRunner.query(`DROP TABLE "memories"`); } diff --git a/server/src/migrations/1715804005643-RemoveLibraryType.ts b/server/src/migrations/1715804005643-RemoveLibraryType.ts index d42ba4ec7351d..cd4dc574f2197 100644 --- a/server/src/migrations/1715804005643-RemoveLibraryType.ts +++ b/server/src/migrations/1715804005643-RemoveLibraryType.ts @@ -5,8 +5,8 @@ export class RemoveLibraryType1715804005643 implements MigrationInterface { public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`); - await queryRunner.query(`DROP INDEX "public"."UQ_assets_owner_library_checksum"`); - await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`); + await queryRunner.query(`DROP INDEX "UQ_assets_owner_library_checksum"`); + await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`); await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`); await queryRunner.query(` UPDATE "assets" diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts index 1ed4945749dd8..bb086b084e9f5 100644 --- a/server/src/migrations/1724101822106-AddAssetFilesTable.ts +++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts @@ -27,7 +27,7 @@ export class AddAssetFilesTable1724101822106 implements MigrationInterface { await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`); await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`); - await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`); + await queryRunner.query(`DROP INDEX "IDX_asset_files_assetId"`); await queryRunner.query(`DROP TABLE "asset_files"`); } diff --git a/server/src/migrations/1724790460210-NestedTagTable.ts b/server/src/migrations/1724790460210-NestedTagTable.ts index dfda9a6d7a38e..d468ff6ba4e97 100644 --- a/server/src/migrations/1724790460210-NestedTagTable.ts +++ b/server/src/migrations/1724790460210-NestedTagTable.ts @@ -47,8 +47,8 @@ export class NestedTagTable1724790460210 implements MigrationInterface { await queryRunner.query(`ALTER TABLE "tags" ADD "name" character varying NOT NULL`); await queryRunner.query(`ALTER TABLE "tags" ADD "type" character varying NOT NULL`); await queryRunner.query(`ALTER TABLE "tags" ADD "renameTagId" uuid`); - await queryRunner.query(`DROP INDEX "public"."IDX_b1a2a7ed45c29179b5ad51548a"`); - await queryRunner.query(`DROP INDEX "public"."IDX_15fbcbc67663c6bfc07b354c22"`); + await queryRunner.query(`DROP INDEX "IDX_b1a2a7ed45c29179b5ad51548a"`); + await queryRunner.query(`DROP INDEX "IDX_15fbcbc67663c6bfc07b354c22"`); await queryRunner.query(`DROP TABLE "tags_closure"`); await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId")`); await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); From c3a8ddaaf2dfe1c968c10e202bebecb346b8ff86 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 3 Sep 2024 21:23:34 -0400 Subject: [PATCH 05/31] fix(server): missing asset files relation (#12295) --- server/src/services/media.service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 3f2513474154d..e74335bdc391c 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -176,7 +176,7 @@ export class MediaService { async handleGeneratePreview({ id }: IEntityJob): Promise { const [{ image }, [asset]] = await Promise.all([ this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true }), + this.assetRepository.getByIds([id], { exifInfo: true, files: true }), ]); if (!asset) { return JobStatus.FAILED; From c7ddd0b44a2ec92c54f1af992bb894f72907cab6 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 3 Sep 2024 23:53:34 -0400 Subject: [PATCH 06/31] fix(web): paste event in input fields (#12297) --- web/src/lib/actions/shortcut.ts | 4 ++-- .../drag-and-drop-upload-overlay.svelte | 13 ++++++++++--- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index d28c294a8996b..df155ea821ad0 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -15,7 +15,7 @@ export type ShortcutOptions = { preventDefault?: boolean; }; -export const shouldIgnoreShortcut = (event: KeyboardEvent): boolean => { +export const shouldIgnoreEvent = (event: KeyboardEvent | ClipboardEvent): boolean => { if (event.target === event.currentTarget) { return false; } @@ -52,7 +52,7 @@ export const shortcuts = ( options: ShortcutOptions[], ): ActionReturn[]> => { function onKeydown(event: KeyboardEvent) { - const ignoreShortcut = shouldIgnoreShortcut(event); + const ignoreShortcut = shouldIgnoreEvent(event); for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) { if (ignoreInputFields && ignoreShortcut) { continue; diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte index e84d2d66f08f2..6f92d81886485 100644 --- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte +++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte @@ -1,11 +1,12 @@ @@ -14,7 +14,6 @@ - From ee6550c02cc9154485154772bd26a14859b3eb21 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 4 Sep 2024 09:20:45 -0400 Subject: [PATCH 09/31] feat(web): add Malay language (#12311) feat(web): add ms.json --- web/src/lib/constants.ts | 1 + web/src/lib/i18n/ms.json | 1 + 2 files changed, 2 insertions(+) create mode 100644 web/src/lib/i18n/ms.json diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index ce5cefd8153d2..05011680dcc41 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -284,6 +284,7 @@ export const langs = [ { name: 'Lithuanian', code: 'lt', loader: () => import('$lib/i18n/lt.json') }, { name: 'Latvian', code: 'lv', loader: () => import('$lib/i18n/lv.json') }, { name: 'Mongolian', code: 'mn', loader: () => import('$lib/i18n/mn.json') }, + { name: 'Malay', code: 'ms', loader: () => import('$lib/i18n/ms.json') }, { name: 'Norwegian Bokmål', code: 'nb-NO', weblateCode: 'nb_NO', loader: () => import('$lib/i18n/nb_NO.json') }, { name: 'Dutch', code: 'nl', loader: () => import('$lib/i18n/nl.json') }, { name: 'Polish', code: 'pl', loader: () => import('$lib/i18n/pl.json') }, diff --git a/web/src/lib/i18n/ms.json b/web/src/lib/i18n/ms.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/ms.json @@ -0,0 +1 @@ +{} From cbb0a7f8d40f87535c877257e598853c29f66d32 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Wed, 4 Sep 2024 16:27:04 +0200 Subject: [PATCH 10/31] fix(server): parse time zone with explicit zero offset (#12307) * fix(server): fix test: use data as returned by exiftool-vendored * fix(server): retain +00:00 timezone if set explicitly --- server/src/services/metadata.service.spec.ts | 42 ++++++++++++++++---- server/src/services/metadata.service.ts | 27 +++++++++++-- 2 files changed, 58 insertions(+), 11 deletions(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 2fc95df00e629..834fd16afc01c 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { BinaryField } from 'exiftool-vendored'; +import { BinaryField, ExifDateTime } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; @@ -746,6 +746,8 @@ describe(MetadataService.name, () => { }); it('should save all metadata', async () => { + const dateForTest = new Date('1970-01-01T00:00:00.000-11:30'); + const tags: ImmichTags = { BitsPerSample: 1, ComponentBitDepth: 1, @@ -753,7 +755,7 @@ describe(MetadataService.name, () => { BitDepth: 1, ColorBitDepth: 1, ColorSpace: '1', - DateTimeOriginal: new Date('1970-01-01').toISOString(), + DateTimeOriginal: ExifDateTime.fromISO(dateForTest.toISOString()), ExposureTime: '100ms', FocalLength: 20, ImageDescription: 'test description', @@ -762,11 +764,11 @@ describe(MetadataService.name, () => { MediaGroupUUID: 'livePhoto', Make: 'test-factory', Model: "'mockel'", - ModifyDate: new Date('1970-01-01').toISOString(), + ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()), Orientation: 0, ProfileDescription: 'extensive description', ProjectionType: 'equirectangular', - tz: '+02:00', + tz: 'UTC-11:30', Rating: 3, }; assetMock.getByIds.mockResolvedValue([assetStub.image]); @@ -779,7 +781,7 @@ describe(MetadataService.name, () => { bitsPerSample: expect.any(Number), autoStackId: null, colorspace: tags.ColorSpace, - dateTimeOriginal: new Date('1970-01-01'), + dateTimeOriginal: dateForTest, description: tags.ImageDescription, exifImageHeight: null, exifImageWidth: null, @@ -805,11 +807,37 @@ describe(MetadataService.name, () => { expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.image.id, duration: null, - fileCreatedAt: new Date('1970-01-01'), - localDateTime: new Date('1970-01-01'), + fileCreatedAt: dateForTest, + localDateTime: dateForTest, }); }); + it('should extract +00:00 timezone from raw value', async () => { + // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly + // https://github.com/photostructure/exiftool-vendored.js/issues/203 + + // this only tests our assumptions of exiftool-vendored, demonstrating the issue + const someDate = '2024-09-01T00:00:00.000'; + expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC'); + expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0 + expect(ExifDateTime.fromISO(someDate + '+04:00')?.zone).toBe('UTC+4'); + + const tags: ImmichTags = { + DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'), + tz: undefined, + }; + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue(tags); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + timeZone: 'UTC+0', + }), + ); + }); + it('should extract duration', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); mediaMock.probe.mockResolvedValue({ diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 29aebc4a36abb..7eab4702ad630 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -531,12 +531,16 @@ export class MetadataService { this.logger.verbose('Exif Tags', exifTags); + const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags); + const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt; + const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue); + const exifData = { // altitude: tags.GPSAltitude ?? null, assetId: asset.id, bitsPerSample: this.getBitsPerSample(exifTags), colorspace: exifTags.ColorSpace ?? null, - dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt, + dateTimeOriginal, description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), exifImageHeight: validate(exifTags.ImageHeight), exifImageWidth: validate(exifTags.ImageWidth), @@ -557,7 +561,7 @@ export class MetadataService { orientation: validate(exifTags.Orientation)?.toString() ?? null, profileDescription: exifTags.ProfileDescription || null, projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, - timeZone: exifTags.tz ?? null, + timeZone, rating: exifTags.Rating ?? null, }; @@ -578,10 +582,25 @@ export class MetadataService { } private getDateTimeOriginal(tags: ImmichTags | Tags | null) { + return this.getDateTimeOriginalWithRawValue(tags).exifDate; + } + + private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } { if (!tags) { - return null; + return { exifDate: null, rawValue: '' }; } - return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS)); + const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS); + return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' }; + } + + private getTimeZone(exifTags: ImmichTags, rawValue: string) { + const timeZone = exifTags.tz ?? null; + if (timeZone == null && rawValue.endsWith('+00:00')) { + // exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly + // https://github.com/photostructure/exiftool-vendored.js/issues/203 + return 'UTC+0'; + } + return timeZone; } private getBitsPerSample(tags: ImmichTags): number | null { From 4bf82fb4c4880d4cf43b2935cdb713bb02867697 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Wed, 4 Sep 2024 16:47:40 +0200 Subject: [PATCH 11/31] fix(web): retain selected time zone offset also for +00:00 (#12310) Co-authored-by: Alex --- .../shared-components/change-date.svelte | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/web/src/lib/components/shared-components/change-date.svelte b/web/src/lib/components/shared-components/change-date.svelte index 962a97ecf7e0c..916b9349f9f7c 100644 --- a/web/src/lib/components/shared-components/change-date.svelte +++ b/web/src/lib/components/shared-components/change-date.svelte @@ -10,18 +10,25 @@ type ZoneOption = { /** - * Timezone name + * Timezone name with offset * * e.g. Asia/Jerusalem (+03:00) */ label: string; /** - * Timezone offset + * Timezone name * - * e.g. UTC+01:00 + * e.g. Asia/Jerusalem */ value: string; + + /** + * Timezone offset in minutes + * + * e.g. 300 + */ + offsetMinutes: number; }; const timezones: ZoneOption[] = Intl.supportedValuesOf('timeZone') @@ -37,21 +44,23 @@ const offset = zone.toFormat('ZZ'); return { label: `${zone.zoneName} (${offset})`, - value: 'UTC' + offset, + value: zone.zoneName, + offsetMinutes: zone.offset, }; }); - const initialOption = timezones.find((item) => item.value === 'UTC' + initialDate.toFormat('ZZ')); + const initialOption = timezones.find((item) => item.offsetMinutes === initialDate.offset); let selectedOption = initialOption && { label: initialOption?.label || '', + offsetMinutes: initialOption?.offsetMinutes || 0, value: initialOption?.value || '', }; let selectedDate = initialDate.toFormat("yyyy-MM-dd'T'HH:mm"); - // Keep local time if not it's really confusing - $: date = DateTime.fromISO(selectedDate).setZone(selectedOption?.value, { keepLocalTime: true }); + // when changing the time zone, assume the configured date/time is meant for that time zone (instead of updating it) + $: date = DateTime.fromISO(selectedDate, { zone: selectedOption?.value, setZone: true }); const dispatch = createEventDispatcher<{ cancel: void; From d685bc1f340762156619f8f403ac3f721b467c8f Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Sep 2024 10:39:31 -0500 Subject: [PATCH 12/31] chore(mobile): handle sync album on duplicated (#12173) * chore(mobile): handle sync album on duplicated * remove check for duplicate in manual sync * linting --- mobile/lib/services/asset.service.dart | 13 ------------- mobile/lib/services/backup.service.dart | 2 +- mobile/pubspec.lock | 4 ++-- 3 files changed, 3 insertions(+), 16 deletions(-) diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 17508cba5153e..c4f258e259129 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; @@ -309,18 +308,6 @@ class AssetService { useTimeFilter: false, ); - final duplicates = await _apiService.assetsApi.checkExistingAssets( - CheckExistingAssetsDto( - deviceAssetIds: candidates.map((c) => c.asset.id).toList(), - deviceId: Store.get(StoreKey.deviceId), - ), - ); - - if (duplicates != null) { - candidates - .removeWhere((c) => !duplicates.existingIds.contains(c.asset.id)); - } - await refreshRemoteAssets(); final remoteAssets = await _db.assets .where() diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 12edd14d609ca..858499443ee1d 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -484,7 +484,7 @@ class BackupService { ), ); - if (shouldSyncAlbums && !isDuplicate) { + if (shouldSyncAlbums) { await _albumService.syncUploadAlbums( candidate.albumNames, [responseBody['id'] as String], diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 14b487ce4dd48..c9493f6490b72 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1737,10 +1737,10 @@ packages: dependency: transitive description: name: vm_service - sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.4" + version: "14.2.5" wakelock_plus: dependency: "direct main" description: From 1783dfd393168dd98ece34e0a6fbaec6d37a62e5 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 4 Sep 2024 17:02:37 +0100 Subject: [PATCH 13/31] fix(web): handle RTL languages in the map component (#12308) --- web/package-lock.json | 115 +++++++++++++++++- web/package.json | 3 +- .../shared-components/map/map.svelte | 3 + 3 files changed, 119 insertions(+), 2 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 97b1a303a573e..4ddc6d9baa966 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11,6 +11,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", @@ -27,7 +28,7 @@ "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", - "svelte-maplibre": "^0.9.0", + "svelte-maplibre": "^0.9.13", "thumbhash": "^0.1.1" }, "devDependencies": { @@ -1446,6 +1447,13 @@ "geojson-rewind": "geojson-rewind" } }, + "node_modules/@mapbox/geojson-types": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz", + "integrity": "sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==", + "license": "ISC", + "peer": true + }, "node_modules/@mapbox/jsonlint-lines-primitives": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz", @@ -1454,6 +1462,25 @@ "node": ">= 0.6" } }, + "node_modules/@mapbox/mapbox-gl-rtl-text": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-rtl-text/-/mapbox-gl-rtl-text-0.2.3.tgz", + "integrity": "sha512-RaCYfnxULUUUxNwcUimV9C/o2295ktTyLEUzD/+VWkqXqvaVfFcZ5slytGzb2Sd/Jj4MlbxD0DCZbfa6CzcmMw==", + "license": "BSD-2-Clause", + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, + "node_modules/@mapbox/mapbox-gl-supported": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@mapbox/mapbox-gl-supported/-/mapbox-gl-supported-1.5.0.tgz", + "integrity": "sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==", + "license": "BSD-3-Clause", + "peer": true, + "peerDependencies": { + "mapbox-gl": ">=0.32.1 <2.0.0" + } + }, "node_modules/@mapbox/point-geometry": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/@mapbox/point-geometry/-/point-geometry-0.1.0.tgz", @@ -3307,6 +3334,13 @@ "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", "dev": true }, + "node_modules/csscolorparser": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/csscolorparser/-/csscolorparser-1.0.3.tgz", + "integrity": "sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==", + "license": "MIT", + "peer": true + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4563,6 +4597,13 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/grid-index": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grid-index/-/grid-index-1.1.0.tgz", + "integrity": "sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==", + "license": "ISC", + "peer": true + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -5388,6 +5429,78 @@ "node": ">=10" } }, + "node_modules/mapbox-gl": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.3.tgz", + "integrity": "sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==", + "license": "SEE LICENSE IN LICENSE.txt", + "peer": true, + "dependencies": { + "@mapbox/geojson-rewind": "^0.5.2", + "@mapbox/geojson-types": "^1.0.2", + "@mapbox/jsonlint-lines-primitives": "^2.0.2", + "@mapbox/mapbox-gl-supported": "^1.5.0", + "@mapbox/point-geometry": "^0.1.0", + "@mapbox/tiny-sdf": "^1.1.1", + "@mapbox/unitbezier": "^0.0.0", + "@mapbox/vector-tile": "^1.3.1", + "@mapbox/whoots-js": "^3.1.0", + "csscolorparser": "~1.0.3", + "earcut": "^2.2.2", + "geojson-vt": "^3.2.1", + "gl-matrix": "^3.2.1", + "grid-index": "^1.1.0", + "murmurhash-js": "^1.0.0", + "pbf": "^3.2.1", + "potpack": "^1.0.1", + "quickselect": "^2.0.0", + "rw": "^1.3.3", + "supercluster": "^7.1.0", + "tinyqueue": "^2.0.3", + "vt-pbf": "^3.1.1" + }, + "engines": { + "node": ">=6.4.0" + } + }, + "node_modules/mapbox-gl/node_modules/@mapbox/tiny-sdf": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@mapbox/tiny-sdf/-/tiny-sdf-1.2.5.tgz", + "integrity": "sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/@mapbox/unitbezier": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@mapbox/unitbezier/-/unitbezier-0.0.0.tgz", + "integrity": "sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==", + "license": "BSD-2-Clause", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/kdbush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-3.0.0.tgz", + "integrity": "sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==", + "license": "ISC", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/potpack": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz", + "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==", + "license": "ISC", + "peer": true + }, + "node_modules/mapbox-gl/node_modules/supercluster": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-7.1.5.tgz", + "integrity": "sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==", + "license": "ISC", + "peer": true, + "dependencies": { + "kdbush": "^3.0.0" + } + }, "node_modules/maplibre-gl": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", diff --git a/web/package.json b/web/package.json index 1996f4eaefe1c..1ba350022d98c 100644 --- a/web/package.json +++ b/web/package.json @@ -67,6 +67,7 @@ "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", "@immich/sdk": "file:../open-api/typescript-sdk", + "@mapbox/mapbox-gl-rtl-text": "^0.2.3", "@mdi/js": "^7.4.47", "@photo-sphere-viewer/core": "^5.7.1", "@photo-sphere-viewer/equirectangular-video-adapter": "^5.7.2", @@ -83,7 +84,7 @@ "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", - "svelte-maplibre": "^0.9.0", + "svelte-maplibre": "^0.9.13", "thumbhash": "^0.1.1" }, "volta": { diff --git a/web/src/lib/components/shared-components/map/map.svelte b/web/src/lib/components/shared-components/map/map.svelte index 45d2879b37fff..7d0dbbee6fa40 100644 --- a/web/src/lib/components/shared-components/map/map.svelte +++ b/web/src/lib/components/shared-components/map/map.svelte @@ -7,6 +7,7 @@ import { mdiCog, mdiMap, mdiMapMarker } from '@mdi/js'; import type { Feature, GeoJsonProperties, Geometry, Point } from 'geojson'; import type { GeoJSONSource, LngLatLike, StyleSpecification } from 'maplibre-gl'; + import mapboxRtlUrl from '@mapbox/mapbox-gl-rtl-text/mapbox-gl-rtl-text.min.js?url'; import maplibregl from 'maplibre-gl'; import { createEventDispatcher } from 'svelte'; import { @@ -51,6 +52,8 @@ let map: maplibregl.Map; let marker: maplibregl.Marker | null = null; + void maplibregl.setRTLTextPlugin(mapboxRtlUrl, true); + $: style = (() => getMapStyle({ theme: ($mapSettings.allowDarkMode ? $colorTheme.value : Theme.LIGHT) as unknown as MapTheme, From 12b65e3c24bc78d7cef6d33a713723f150c07528 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 4 Sep 2024 13:32:43 -0400 Subject: [PATCH 14/31] fix(server): auto-reconnect to database (#12320) --- server/src/app.module.ts | 17 +++++-- server/src/interfaces/database.interface.ts | 1 + server/src/middleware/error.interceptor.ts | 8 ++-- .../src/middleware/global-exception.filter.ts | 47 +++++++++++++++++++ .../src/middleware/http-exception.filter.ts | 39 --------------- .../src/repositories/database.repository.ts | 13 +++++ server/src/repositories/logger.repository.ts | 2 +- server/src/services/database.service.ts | 25 ++++++++++ .../src/utils/{logger-colors.ts => logger.ts} | 23 +++++++++ .../repositories/database.repository.mock.ts | 1 + 10 files changed, 130 insertions(+), 46 deletions(-) create mode 100644 server/src/middleware/global-exception.filter.ts delete mode 100644 server/src/middleware/http-exception.filter.ts rename server/src/utils/{logger-colors.ts => logger.ts} (55%) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index c6cd68a96ff77..9446010127450 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -18,10 +18,11 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; -import { HttpExceptionFilter } from 'src/middleware/http-exception.filter'; +import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter'; import { LoggingInterceptor } from 'src/middleware/logging.interceptor'; import { repositories } from 'src/repositories'; import { services } from 'src/services'; +import { DatabaseService } from 'src/services/database.service'; import { setupEventHandlers } from 'src/utils/events'; import { otelConfig } from 'src/utils/instrumentation'; @@ -29,7 +30,7 @@ const common = [...services, ...repositories]; const middleware = [ FileUploadInterceptor, - { provide: APP_FILTER, useClass: HttpExceptionFilter }, + { provide: APP_FILTER, useClass: GlobalExceptionFilter }, { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) }, { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor }, { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor }, @@ -43,7 +44,17 @@ const imports = [ ConfigModule.forRoot(immichAppConfig), EventEmitterModule.forRoot(), OpenTelemetryModule.forRoot(otelConfig), - TypeOrmModule.forRoot(databaseConfig), + TypeOrmModule.forRootAsync({ + inject: [ModuleRef], + useFactory: (moduleRef: ModuleRef) => { + return { + ...databaseConfig, + poolErrorHandler: (error) => { + moduleRef.get(DatabaseService, { strict: false }).handleConnectionError(error); + }, + }; + }, + }), TypeOrmModule.forFeature(entities), ]; diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index 98bb0c02889c2..373f1091429d7 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -40,6 +40,7 @@ export interface VectorUpdateResult { export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { + reconnect(): Promise; getExtensionVersion(extension: DatabaseExtension): Promise; getExtensionVersionRange(extension: VectorExtension): string; getPostgresVersion(): Promise; diff --git a/server/src/middleware/error.interceptor.ts b/server/src/middleware/error.interceptor.ts index a0c333e4b24cb..5d93b40dc22d0 100644 --- a/server/src/middleware/error.interceptor.ts +++ b/server/src/middleware/error.interceptor.ts @@ -9,6 +9,7 @@ import { } from '@nestjs/common'; import { Observable, catchError, throwError } from 'rxjs'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { logGlobalError } from 'src/utils/logger'; import { routeToErrorMessage } from 'src/utils/misc'; @Injectable() @@ -25,9 +26,10 @@ export class ErrorInterceptor implements NestInterceptor { return error; } - const errorMessage = routeToErrorMessage(context.getHandler().name); - this.logger.error(errorMessage, error, error?.errors, error?.stack); - return new InternalServerErrorException(errorMessage); + logGlobalError(this.logger, error); + + const message = routeToErrorMessage(context.getHandler().name); + return new InternalServerErrorException(message); }), ), ); diff --git a/server/src/middleware/global-exception.filter.ts b/server/src/middleware/global-exception.filter.ts new file mode 100644 index 0000000000000..6200363e86e9e --- /dev/null +++ b/server/src/middleware/global-exception.filter.ts @@ -0,0 +1,47 @@ +import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common'; +import { Response } from 'express'; +import { ClsService } from 'nestjs-cls'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { logGlobalError } from 'src/utils/logger'; + +@Catch() +export class GlobalExceptionFilter implements ExceptionFilter { + constructor( + @Inject(ILoggerRepository) private logger: ILoggerRepository, + private cls: ClsService, + ) { + this.logger.setContext(GlobalExceptionFilter.name); + } + + catch(error: Error, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + const { status, body } = this.fromError(error); + if (!response.headersSent) { + response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() }); + } + } + + private fromError(error: Error) { + logGlobalError(this.logger, error); + + if (error instanceof HttpException) { + const status = error.getStatus(); + let body = error.getResponse(); + + // unclear what circumstances would return a string + if (typeof body === 'string') { + body = { message: body }; + } + + return { status, body }; + } + + return { + status: 500, + body: { + message: 'Internal server error', + }, + }; + } +} diff --git a/server/src/middleware/http-exception.filter.ts b/server/src/middleware/http-exception.filter.ts deleted file mode 100644 index 3306b50ca67e0..0000000000000 --- a/server/src/middleware/http-exception.filter.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common'; -import { Response } from 'express'; -import { ClsService } from 'nestjs-cls'; -import { ILoggerRepository } from 'src/interfaces/logger.interface'; - -@Catch(HttpException) -export class HttpExceptionFilter implements ExceptionFilter { - constructor( - @Inject(ILoggerRepository) private logger: ILoggerRepository, - private cls: ClsService, - ) { - this.logger.setContext(HttpExceptionFilter.name); - } - - catch(exception: HttpException, host: ArgumentsHost) { - const ctx = host.switchToHttp(); - const response = ctx.getResponse(); - const status = exception.getStatus(); - - this.logger.debug(`HttpException(${status}) ${JSON.stringify(exception.getResponse())}`); - - let responseBody = exception.getResponse(); - // unclear what circumstances would return a string - if (typeof responseBody === 'string') { - responseBody = { - error: 'Unknown', - message: responseBody, - statusCode: status, - }; - } - - if (!response.headersSent) { - response.status(status).json({ - ...responseBody, - correlationId: this.cls.getId(), - }); - } - } -} diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index 9ee7f8e6fccea..0453421a39d1b 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -31,6 +31,19 @@ export class DatabaseRepository implements IDatabaseRepository { this.logger.setContext(DatabaseRepository.name); } + async reconnect() { + try { + if (this.dataSource.isInitialized) { + await this.dataSource.destroy(); + } + const { isInitialized } = await this.dataSource.initialize(); + return isInitialized; + } catch (error) { + this.logger.error(`Database connection failed: ${error}`); + return false; + } + } + async getExtensionVersion(extension: DatabaseExtension): Promise { const [res]: ExtensionVersion[] = await this.dataSource.query( `SELECT default_version as "availableVersion", installed_version as "installedVersion" diff --git a/server/src/repositories/logger.repository.ts b/server/src/repositories/logger.repository.ts index 1527965b496cd..1e0c7b74d973e 100644 --- a/server/src/repositories/logger.repository.ts +++ b/server/src/repositories/logger.repository.ts @@ -3,7 +3,7 @@ import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-en import { ClsService } from 'nestjs-cls'; import { LogLevel } from 'src/config'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; -import { LogColor } from 'src/utils/logger-colors'; +import { LogColor } from 'src/utils/logger'; const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL]; diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index d2a2813a0550c..a5280ff28be23 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,4 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; +import { Duration } from 'luxon'; import semver from 'semver'; import { getVectorExtension } from 'src/database.config'; import { OnEmit } from 'src/decorators'; @@ -59,8 +60,12 @@ const messages = { If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`, }; +const RETRY_DURATION = Duration.fromObject({ seconds: 5 }); + @Injectable() export class DatabaseService { + private reconnection?: NodeJS.Timeout; + constructor( @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -117,6 +122,26 @@ export class DatabaseService { }); } + handleConnectionError(error: Error) { + if (this.reconnection) { + return; + } + + this.logger.error(`Database disconnected: ${error}`); + this.reconnection = setInterval(() => void this.reconnect(), RETRY_DURATION.toMillis()); + } + + private async reconnect() { + const isConnected = await this.databaseRepository.reconnect(); + if (isConnected) { + this.logger.log('Database reconnected'); + clearInterval(this.reconnection); + delete this.reconnection; + } else { + this.logger.warn(`Database connection failed, retrying in ${RETRY_DURATION.toHuman()}`); + } + } + private async createExtension(extension: DatabaseExtension) { try { await this.databaseRepository.createExtension(extension); diff --git a/server/src/utils/logger-colors.ts b/server/src/utils/logger.ts similarity index 55% rename from server/src/utils/logger-colors.ts rename to server/src/utils/logger.ts index 36104ee520c82..d4eb02ead21ab 100644 --- a/server/src/utils/logger-colors.ts +++ b/server/src/utils/logger.ts @@ -1,3 +1,7 @@ +import { HttpException } from '@nestjs/common'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { TypeORMError } from 'typeorm'; + type ColorTextFn = (text: string) => string; const isColorAllowed = () => !process.env.NO_COLOR; @@ -15,3 +19,22 @@ export const LogColor = { export const LogStyle = { bold: colorIfAllowed((text: string) => `\u001B[1m${text}\u001B[0m`), }; + +export const logGlobalError = (logger: ILoggerRepository, error: Error) => { + if (error instanceof HttpException) { + const status = error.getStatus(); + const response = error.getResponse(); + logger.debug(`HttpException(${status}): ${JSON.stringify(response)}`); + return; + } + + if (error instanceof TypeORMError) { + logger.error(`Database error: ${error}`); + return; + } + + if (error instanceof Error) { + logger.error(`Unknown error: ${error}`); + return; + } +}; diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index e8b0817dfe0b7..0e1d4ab3e71dd 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -3,6 +3,7 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked => { return { + reconnect: vitest.fn(), getExtensionVersion: vitest.fn(), getExtensionVersionRange: vitest.fn(), getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), From f8211a128e4f92cd79bdf425f73494b9841e9dd9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 4 Sep 2024 14:36:12 -0400 Subject: [PATCH 15/31] fix(deps): update machine-learning (#12257) --- machine-learning/Dockerfile | 4 +- machine-learning/export/Dockerfile | 2 +- machine-learning/poetry.lock | 123 +++++++++++++++-------------- 3 files changed, 65 insertions(+), 64 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 8fc72b308f08a..f680aac826af3 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:f7543d9969bdc112dd9819ca642e14433fdacfe857f170f6b803392fc7e451ad AS builder-cpu +FROM python:3.11-bookworm@sha256:20c1819af5af3acba0b2b66074a2615e398ceee6842adf03cd7ad5f8d0ee3daf AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:ad5dadd957a398226996bc4846e522c39f2a77340b531b28aaab85b2d361210b AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:ed4e985674f478c90ce879e9aa224fbb772c84e39b4aed5155b9e2280f131039 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index d458d92d15014..eaa35d14be0dd 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:475730daef12ff9c0733e70092aeeefdf4c373a584c952dac3f7bdb739601990 AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:29174348bd09352e5f1b1f6756cf1d00021487b8340fae040e91e4f98e954ce5 AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 7385d1269d3f8..bd09bd8469e67 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -680,13 +680,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.112.1" +version = "0.112.2" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.112.1-py3-none-any.whl", hash = "sha256:cc227cf9402d0ba54a24f80eb205c33bcb25d3ea18d53fdac3fd76ea5af8e76d"}, - {file = "fastapi_slim-0.112.1.tar.gz", hash = "sha256:876ebd24e72273986709db2d469b75dc18f04c3ab9140ffd78b29d7785d26687"}, + {file = "fastapi_slim-0.112.2-py3-none-any.whl", hash = "sha256:c023f74768f187af142c2fe5ff9e4ca3c4c1940bbde7df008cb283532422a23f"}, + {file = "fastapi_slim-0.112.2.tar.gz", hash = "sha256:75b8eb0c6ee05a20270da7a527ac7ad53b83414602f42b68f7027484dab3aedb"}, ] [package.dependencies] @@ -695,8 +695,8 @@ starlette = ">=0.37.2,<0.39.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" @@ -1212,13 +1212,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"] [[package]] name = "httpx" -version = "0.27.0" +version = "0.27.2" description = "The next generation HTTP client." optional = false python-versions = ">=3.8" files = [ - {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, - {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, + {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"}, + {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"}, ] [package.dependencies] @@ -1233,6 +1233,7 @@ brotli = ["brotli", "brotlicffi"] cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "huggingface-hub" @@ -1530,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.31.3" +version = "2.31.5" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.31.3-py3-none-any.whl", hash = "sha256:03122e007519b371a5a553d578af502826755de83551d79ea8a412ea1c660115"}, - {file = "locust-2.31.3.tar.gz", hash = "sha256:25f4603f24afa11ef1ee1f26b1c86a232eb9a1140be30b2a4642c12d7a7af8ae"}, + {file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"}, + {file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"}, ] [package.dependencies] @@ -1794,38 +1795,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.1" +version = "1.11.2" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, - {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, - {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, - {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, - {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, - {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, - {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, - {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, - {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, - {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, - {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, - {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, - {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, - {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, - {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, - {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, - {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, - {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, - {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, - {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, - {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, - {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, - {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, ] [package.dependencies] @@ -2815,13 +2816,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rich" -version = "13.7.1" +version = "13.8.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.7.0" files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, + {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"}, + {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"}, ] [package.dependencies] @@ -2833,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.6.2" +version = "0.6.3" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, - {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, - {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, - {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, - {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, - {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, - {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, - {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, - {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, + {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"}, + {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"}, + {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"}, + {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"}, + {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"}, + {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"}, + {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"}, + {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"}, + {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"}, ] [[package]] From 0a8bd7dc661e4a7033aaedf13f923161c6bfc7e2 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 4 Sep 2024 14:07:32 -0500 Subject: [PATCH 16/31] fix(web): correct color for active tree item (#12318) * fix(web): correct color for active tree item * remove white space --- web/src/lib/components/shared-components/tree/tree.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index 5bc7a715ac793..5c4b367a5482f 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -13,7 +13,7 @@ export let getColor: (path: string) => string | undefined; $: path = normalizeTreePath(`${parent}/${value}`); - $: isActive = active.startsWith(path); + $: isActive = active === path || active.startsWith(`${path}/`); $: isOpen = isActive; $: isTarget = active === path; $: color = getColor(path); From 720412645f3c5344d9c8e2ef4ef538e4563e0b0c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 4 Sep 2024 18:21:21 -0400 Subject: [PATCH 17/31] feat(web): sort albums in modal (#12331) --- .../components/album-page/albums-list.svelte | 71 +++---------------- .../album-selection-modal.svelte | 18 ++--- web/src/lib/utils/album-utils.ts | 62 ++++++++++++++++ 3 files changed, 81 insertions(+), 70 deletions(-) diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index 4355aca94d58b..5e3499bd10b3f 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -1,6 +1,6 @@ + +
+
+
+
+ +
+ + onReset({ ...options, configKeys: ['metadata'] })} + onSave={() => onSave({ metadata: config.metadata })} + showResetToDefault={!isEqual(savedConfig.metadata.faces.import, defaultConfig.metadata.faces.import)} + {disabled} + /> + +
+
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 25f3b6ea2faab..113998dc890d1 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -137,7 +137,11 @@ "map_settings_description": "Manage map settings", "map_style_description": "URL to a style.json map theme", "metadata_extraction_job": "Extract metadata", - "metadata_extraction_job_description": "Extract metadata information from each asset, such as GPS and resolution", + "metadata_extraction_job_description": "Extract metadata information from each asset, such as GPS, faces and resolution", + "metadata_faces_import_setting": "Enable face import", + "metadata_faces_import_setting_description": "Import faces from image EXIF data and sidecar files", + "metadata_settings": "Metadata Settings", + "metadata_settings_description": "Manage metadata settings", "migration_job": "Migration", "migration_job_description": "Migrate thumbnails for assets and faces to the latest folder structure", "no_paths_added": "No paths added", diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 1d3c4bc00eb26..14d1e4e66e895 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -8,6 +8,7 @@ export const featureFlags = writable({ smartSearch: true, duplicateDetection: false, facialRecognition: true, + importFaces: false, sidecar: true, map: true, reverseGeocoding: true, diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index 0555bab256f68..d03865cb39075 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -4,6 +4,7 @@ import FFmpegSettings from '$lib/components/admin-page/settings/ffmpeg/ffmpeg-settings.svelte'; import ImageSettings from '$lib/components/admin-page/settings/image/image-settings.svelte'; import JobSettings from '$lib/components/admin-page/settings/job-settings/job-settings.svelte'; + import MetadataSettings from '$lib/components/admin-page/settings/metadata-settings/metadata-settings.svelte'; import LibrarySettings from '$lib/components/admin-page/settings/library-settings/library-settings.svelte'; import LoggingSettings from '$lib/components/admin-page/settings/logging-settings/logging-settings.svelte'; import MachineLearningSettings from '$lib/components/admin-page/settings/machine-learning-settings/machine-learning-settings.svelte'; @@ -86,6 +87,12 @@ subtitle: $t('admin.job_settings_description'), key: 'job', }, + { + component: MetadataSettings, + title: $t('admin.metadata_settings'), + subtitle: $t('admin.metadata_settings_description'), + key: 'metadata', + }, { component: LibrarySettings, title: $t('admin.library_settings'), From 0d6bef2c05a10b16ebbd4b2b4d1b238f34fec988 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 4 Sep 2024 23:28:30 +0100 Subject: [PATCH 19/31] ci: job naming improvements and success job for matrix (#12316) Co-authored-by: bo0tzz --- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 13 +++++++++++++ .github/workflows/docs-build.yml | 1 + .github/workflows/docs-deploy.yml | 2 ++ .github/workflows/docs-destroy.yml | 1 + .github/workflows/test.yml | 12 ++++++------ 6 files changed, 24 insertions(+), 7 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 1ec17b381dbfd..5292075cce902 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -22,7 +22,7 @@ permissions: jobs: publish: - name: Publish + name: CLI Publish runs-on: ubuntu-latest defaults: run: diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 7784b32f362f1..6be26c9bbe62f 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -234,3 +234,16 @@ jobs: BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }} BUILD_SOURCE_REF=${{ github.ref_name }} BUILD_SOURCE_COMMIT=${{ github.sha }} + + success-check: + name: Docker Build & Push Success + needs: [build_and_push_ml, build_and_push_server] + runs-on: ubuntu-latest + if: always() + steps: + - name: Any jobs failed? + if: ${{ contains(needs.*.result, 'failure') }} + run: exit 1 + - name: All jobs passed or skipped + if: ${{ !(contains(needs.*.result, 'failure')) }} + run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}" diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml index 682e3c45f008a..387d8e042496b 100644 --- a/.github/workflows/docs-build.yml +++ b/.github/workflows/docs-build.yml @@ -30,6 +30,7 @@ jobs: run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT" build: + name: Docs Build needs: pre-job if: ${{ needs.pre-job.outputs.should_run == 'true' }} runs-on: ubuntu-latest diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml index a863cf8ed2f9c..ab197fa459d94 100644 --- a/.github/workflows/docs-deploy.yml +++ b/.github/workflows/docs-deploy.yml @@ -7,6 +7,7 @@ on: jobs: checks: + name: Docs Deploy Checks runs-on: ubuntu-latest outputs: parameters: ${{ steps.parameters.outputs.result }} @@ -91,6 +92,7 @@ jobs: return parameters; deploy: + name: Docs Deploy runs-on: ubuntu-latest needs: checks if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }} diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml index 861a6319fe95d..80700569245d1 100644 --- a/.github/workflows/docs-destroy.yml +++ b/.github/workflows/docs-destroy.yml @@ -5,6 +5,7 @@ on: jobs: deploy: + name: Docs Destroy runs-on: ubuntu-latest steps: - name: Checkout code diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ac6236d2eb6c2..24e3e086235f0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -48,7 +48,7 @@ jobs: run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT" server-unit-tests: - name: Server + name: Test & Lint Server needs: pre-job if: ${{ needs.pre-job.outputs.should_run_server == 'true' }} runs-on: ubuntu-latest @@ -85,7 +85,7 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests: - name: CLI + name: Unit Test CLI needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: ubuntu-latest @@ -126,7 +126,7 @@ jobs: if: ${{ !cancelled() }} cli-unit-tests-win: - name: CLI (Windows) + name: Unit Test CLI (Windows) needs: pre-job if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }} runs-on: windows-latest @@ -160,7 +160,7 @@ jobs: if: ${{ !cancelled() }} web-unit-tests: - name: Web + name: Test & Lint Web needs: pre-job if: ${{ needs.pre-job.outputs.should_run_web == 'true' }} runs-on: ubuntu-latest @@ -327,7 +327,7 @@ jobs: if: ${{ !cancelled() }} mobile-unit-tests: - name: Mobile + name: Unit Test Mobile needs: pre-job if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }} runs-on: ubuntu-latest @@ -343,7 +343,7 @@ jobs: run: flutter test -j 1 ml-unit-tests: - name: Machine Learning + name: Unit Test ML needs: pre-job if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }} runs-on: ubuntu-latest From f4ec8425775c37c25e11c89b3dfd5b11de46b42d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 4 Sep 2024 23:38:55 -0400 Subject: [PATCH 20/31] refactor(web): upload panel (#12326) Co-authored-by: Alex --- web/src/lib/components/elements/icon.svelte | 3 +- .../upload-asset-preview.svelte | 146 +++++++++--------- .../shared-components/upload-panel.svelte | 37 +++-- web/src/lib/models/upload-asset.ts | 2 +- web/src/lib/stores/upload.ts | 89 +++++++---- web/src/lib/utils/file-uploader.ts | 47 ++++-- 6 files changed, 184 insertions(+), 140 deletions(-) diff --git a/web/src/lib/components/elements/icon.svelte b/web/src/lib/components/elements/icon.svelte index bb22276286f11..5965928718276 100644 --- a/web/src/lib/components/elements/icon.svelte +++ b/web/src/lib/components/elements/icon.svelte @@ -16,13 +16,14 @@ export let ariaLabelledby: string | undefined = undefined; export let strokeWidth: number = 0; export let strokeColor: string = 'currentColor'; + export let spin = false; + import Icon from '$lib/components/elements/icon.svelte'; + import { AppRoute } from '$lib/constants'; import type { UploadAsset } from '$lib/models/upload-asset'; import { UploadState } from '$lib/models/upload-asset'; import { locale } from '$lib/stores/preferences.store'; - import { getByteUnitString } from '$lib/utils/byte-units'; - import { fade } from 'svelte/transition'; - import ImmichLogo from './immich-logo.svelte'; - import { getFilenameExtension } from '$lib/utils/asset-utils'; import { uploadAssetsStore } from '$lib/stores/upload'; - import Icon from '$lib/components/elements/icon.svelte'; + import { getByteUnitString } from '$lib/utils/byte-units'; import { fileUploadHandler } from '$lib/utils/file-uploader'; - import { mdiRefresh, mdiCancel } from '@mdi/js'; + import { + mdiAlertCircle, + mdiCheckCircle, + mdiCircleOutline, + mdiClose, + mdiLoading, + mdiOpenInNew, + mdiRestart, + } from '@mdi/js'; import { t } from 'svelte-i18n'; + import { fade } from 'svelte/transition'; export let uploadAsset: UploadAsset; + const handleDismiss = (uploadAsset: UploadAsset) => { + uploadAssetsStore.removeItem(uploadAsset.id); + }; + const handleRetry = async (uploadAsset: UploadAsset) => { - uploadAssetsStore.removeUploadAsset(uploadAsset.id); + uploadAssetsStore.removeItem(uploadAsset.id); await fileUploadHandler([uploadAsset.file], uploadAsset.albumId); }; @@ -23,86 +34,69 @@
-
-
-
- -
-
-

- .{getFilenameExtension(uploadAsset.file.name)} -

-
+
+
+ {#if uploadAsset.state === UploadState.PENDING} + + {:else if uploadAsset.state === UploadState.STARTED} + + {:else if uploadAsset.state === UploadState.ERROR} + + {:else if uploadAsset.state === UploadState.DUPLICATED} + + {:else if uploadAsset.state === UploadState.DONE} + + {/if}
-
- + + {uploadAsset.file.name} -
- {#if uploadAsset.state === UploadState.STARTED} -
-

- {#if uploadAsset.message} - {uploadAsset.message} - {:else} - {uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s - {/if} -

- {:else if uploadAsset.state === UploadState.PENDING} -
-

{$t('pending')}

- {:else if uploadAsset.state === UploadState.ERROR} -
-

{$t('error')}

- {:else if uploadAsset.state === UploadState.DUPLICATED} -
-

- {$t('asset_skipped')} - {#if uploadAsset.message} - ({uploadAsset.message}) - {/if} -

- {:else if uploadAsset.state === UploadState.DONE} -
-

- {$t('asset_uploaded')} - {#if uploadAsset.message} - ({uploadAsset.message}) - {/if} -

- {/if} -
-
- {#if uploadAsset.state === UploadState.ERROR} -
- - +
+ {:else if uploadAsset.state === UploadState.ERROR} +
+ +
{/if}
+ {#if uploadAsset.state === UploadState.STARTED} +
+
+

+ {#if uploadAsset.message} + {uploadAsset.message} + {:else} + {uploadAsset.progress}% - {getByteUnitString(uploadAsset.speed || 0, $locale)}/s - {uploadAsset.eta}s + {/if} +

+
+ {/if} + {#if uploadAsset.state === UploadState.ERROR}
-

+

{uploadAsset.error}

diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index ee213d796925f..d5360532862b1 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -15,8 +15,7 @@ let showOptions = false; let concurrency = uploadExecutionQueue.concurrency; - let { isUploading, hasError, remainingUploads, errorCounter, duplicateCounter, successCounter, totalUploadCounter } = - uploadAssetsStore; + let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore; const autoHide = () => { if (!$isUploading && showDetail) { @@ -33,29 +32,29 @@ } -{#if $hasError || $isUploading} +{#if $isUploading}
{ - if ($errorCounter > 0) { + if ($stats.errors > 0) { notificationController.show({ - message: $t('upload_errors', { values: { count: $errorCounter } }), + message: $t('upload_errors', { values: { count: $stats.errors } }), type: NotificationType.Warning, }); - } else if ($successCounter > 0) { + } else if ($stats.success > 0) { notificationController.show({ message: $t('upload_success'), type: NotificationType.Info, }); } - if ($duplicateCounter > 0) { + if ($stats.duplicates > 0) { notificationController.show({ - message: $t('upload_skipped_duplicates', { values: { count: $duplicateCounter } }), + message: $t('upload_skipped_duplicates', { values: { count: $stats.duplicates } }), type: NotificationType.Warning, }); } - uploadAssetsStore.resetStore(); + uploadAssetsStore.reset(); }} class="fixed bottom-6 right-6 z-[10000]" > @@ -70,20 +69,20 @@ {$t('upload_progress', { values: { remaining: $remainingUploads, - processed: $successCounter + $errorCounter, - total: $totalUploadCounter, + processed: $stats.total - $remainingUploads, + total: $stats.total, }, })}

{$t('upload_status_uploaded')} - {$successCounter.toLocaleString($locale)} + {$stats.success.toLocaleString($locale)} - {$t('upload_status_errors')} - {$errorCounter.toLocaleString($locale)} + {$stats.errors.toLocaleString($locale)} - {$t('upload_status_duplicates')} - {$duplicateCounter.toLocaleString($locale)} + {$stats.duplicates.toLocaleString($locale)}

@@ -103,7 +102,7 @@ on:click={() => (showDetail = false)} />
- {#if $hasError} + {#if $isDismissible}
{#if showOptions} -
+
@@ -133,7 +132,7 @@ />
{/if} -
+
{#each $uploadAssetsStore as uploadAsset (uploadAsset.id)} {/each} @@ -149,14 +148,14 @@ > {$remainingUploads.toLocaleString($locale)} - {#if $hasError} + {#if $stats.errors > 0} {/if}