From 04b311bd93e9d60e965ab4d3c5bb587bb62ef3bd Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 8 Dec 2024 17:22:39 -0600 Subject: [PATCH 01/25] chore(mobile): disable Impeller (#14589) --- mobile/android/app/src/main/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index bbc562c103..e49cf5b8da 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -37,7 +37,7 @@ + android:value="false" /> Date: Sun, 8 Dec 2024 23:41:22 +0000 Subject: [PATCH 02/25] chore: version v1.122.2 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | Bin 32447 -> 32447 bytes mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index b0611f1af0..03b8061efb 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.34", + "version": "2.2.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.34", + "version": "2.2.35", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index b69a36cf19..b58825b2b9 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.34", + "version": "2.2.35", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 0d664e1272..7ba9125c03 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.122.2", + "url": "https://v1.122.2.archive.immich.app" + }, { "label": "v1.122.1", "url": "https://v1.122.1.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d962cb3368..011e6b2fdd 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.122.1", + "version": "1.122.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.34", + "version": "2.2.35", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 42ea62d64b..a47b4bbae9 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.122.1", + "version": "1.122.2", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e4d1d7fbf5..cff2a432b3 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.122.1" +version = "1.122.2" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 9e384e8591..4fc00ce6c7 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 169, - "android.injected.version.name" => "1.122.1", + "android.injected.version.code" => 170, + "android.injected.version.name" => "1.122.2", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 1c28c050aa..d7604b4283 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.122.1" + version_number: "1.122.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 7cfd48d83dc30b70e198adb50c455d06b172a23c..d9e10bc316bf4dadd00e5cb55b9f8458e7eb5f68 100644 GIT binary patch delta 14 Wcmdo0mvR4J#tCy7jW*6xtOEczt_H~f delta 14 Wcmdo0mvR4J#tCy74L8nHtOEczsRqaZ diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6974c560a2..39621a953e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.122.1+169 +version: 1.122.2+170 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a13fb6e696..706d6a28ee 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7436,7 +7436,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.122.1", + "version": "1.122.2", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 2e69c4cfc6..c0d5d329d1 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index bc3a3023e4..22f480e68d 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ef82b82954..68f44d7bed 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.122.1 + * 1.122.2 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 3a01c83fa1..4ad00c90f7 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.122.1", + "version": "1.122.2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 84cd0e4aae..a7005deafa 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.122.1", + "version": "1.122.2", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 4669730ae1..cab21cd4dc 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.122.1", + "version": "1.122.2", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 6bac30054a..2bd429cc1a 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.122.1", + "version": "1.122.2", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From e4b76e8efea5cddd20f2527316b939ed61fa0087 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Mon, 9 Dec 2024 00:52:10 +0100 Subject: [PATCH 03/25] chore: add language requests from weblate (#14578) --- i18n/bn.json | 1 + i18n/ur.json | 1 + web/src/lib/constants.ts | 2 ++ 3 files changed, 4 insertions(+) create mode 100644 i18n/bn.json create mode 100644 i18n/ur.json diff --git a/i18n/bn.json b/i18n/bn.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/bn.json @@ -0,0 +1 @@ +{} diff --git a/i18n/ur.json b/i18n/ur.json new file mode 100644 index 0000000000..0967ef424b --- /dev/null +++ b/i18n/ur.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 8d4fb809a5..b7ea2cfb52 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -265,6 +265,7 @@ export const langs = [ { name: 'Belarusian', code: 'be', loader: () => import('$i18n/be.json') }, { name: 'Bulgarian', code: 'bg', loader: () => import('$i18n/bg.json') }, { name: 'Bislama', code: 'bi', loader: () => import('$i18n/bi.json') }, + { name: 'Bengali', code: 'bn', loader: () => import('$i18n/bn.json') }, { name: 'Catalan', code: 'ca', loader: () => import('$i18n/ca.json') }, { name: 'Czech', code: 'cs', loader: () => import('$i18n/cs.json') }, { name: 'Chuvash', code: 'cv', loader: () => import('$i18n/cv.json') }, @@ -319,6 +320,7 @@ export const langs = [ { name: 'Thai', code: 'th', loader: () => import('$i18n/th.json') }, { name: 'Turkish', code: 'tr', loader: () => import('$i18n/tr.json') }, { name: 'Ukrainian', code: 'uk', loader: () => import('$i18n/uk.json') }, + { name: 'Urdu', code: 'ur', loader: () => import('$i18n/ur.json') }, { name: 'Vietnamese', code: 'vi', loader: () => import('$i18n/vi.json') }, { name: 'Chinese (Traditional)', From 1ba622adc95d7e0845e7bf7fc8a77103d73e8c5b Mon Sep 17 00:00:00 2001 From: Lukas Date: Sun, 8 Dec 2024 21:35:23 -0500 Subject: [PATCH 04/25] feat: Add support for vob (#14590) Add support for vob --- server/src/services/asset-media.service.spec.ts | 2 +- server/src/utils/mime-types.spec.ts | 1 + server/src/utils/mime-types.ts | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index da7e23be54..1daeb99d0b 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -98,7 +98,7 @@ const validImages = [ '.x3f', ]; -const validVideos = ['.3gp', '.avi', '.flv', '.m2ts', '.mkv', '.mov', '.mp4', '.mpg', '.mts', '.webm', '.wmv']; +const validVideos = ['.3gp', '.avi', '.flv', '.m2ts', '.mkv', '.mov', '.mp4', '.mpg', '.mts', '.vob', '.webm', '.wmv']; const uploadTests = [ { diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index 50fe760a04..05cd8566c8 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -92,6 +92,7 @@ describe('mimeTypes', () => { { mimetype: 'video/x-matroska', extension: '.mkv' }, { mimetype: 'video/x-ms-wmv', extension: '.wmv' }, { mimetype: 'video/x-msvideo', extension: '.avi' }, + { mimetype: 'video/mpeg', extension: '.vob' }, ]) { it(`should map ${extension} to ${mimetype}`, () => { expect({ ...mimeTypes.image, ...mimeTypes.video }[extension]).toContain(mimetype); diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index cbf6e5b489..165eb44a4f 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -74,6 +74,7 @@ const video: Record = { '.mpeg': ['video/mpeg'], '.mpg': ['video/mpeg'], '.mts': ['video/mp2t'], + '.vob': ['video/mpeg'], '.webm': ['video/webm'], '.wmv': ['video/x-ms-wmv'], }; From 60c783bbe9b9fdf91e7ba2a28974018fd291f574 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 10 Dec 2024 12:11:19 -0500 Subject: [PATCH 05/25] fix(server): partial fallback for hardware transcoding (#14611) --- server/src/interfaces/media.interface.ts | 5 + server/src/services/media.service.spec.ts | 106 ++++++++++++---------- server/src/services/media.service.ts | 77 +++++++--------- server/src/utils/media.ts | 54 +++++------ 4 files changed, 121 insertions(+), 121 deletions(-) diff --git a/server/src/interfaces/media.interface.ts b/server/src/interfaces/media.interface.ts index 468a6ad88d..b90dfb483c 100644 --- a/server/src/interfaces/media.interface.ts +++ b/server/src/interfaces/media.interface.ts @@ -130,6 +130,11 @@ export interface ProbeOptions { countFrames: boolean; } +export interface VideoInterfaces { + dri: string[]; + mali: boolean; +} + export interface IMediaRepository { // image extract(input: string, output: string): Promise; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 909b9d02e3..36a9045677 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -1,4 +1,3 @@ -import type { Stats } from 'node:fs'; import { SystemConfig } from 'src/config'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; @@ -303,7 +302,7 @@ describe(MediaService.name, () => { it('should skip video thumbnail generation if no video stream', async () => { mediaMock.probe.mockResolvedValue(probeStub.noVideoStreams); assetMock.getById.mockResolvedValue(assetStub.video); - await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toBeInstanceOf(Error); + await expect(sut.handleGenerateThumbnails({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.generateThumbnail).not.toHaveBeenCalled(); expect(assetMock.update).not.toHaveBeenCalledWith(); }); @@ -770,6 +769,7 @@ describe(MediaService.name, () => { describe('handleVideoConversion', () => { beforeEach(() => { assetMock.getByIds.mockResolvedValue([assetStub.video]); + sut.videoInterfaces = { dri: ['renderD128'], mali: true }; }); it('should skip transcoding if asset not found', async () => { @@ -826,7 +826,7 @@ describe(MediaService.name, () => { systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'foo' } } as never as SystemConfig); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toBeDefined(); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1079,7 +1079,7 @@ describe(MediaService.name, () => { mediaMock.probe.mockResolvedValue(probeStub.videoStream2160p); systemMock.get.mockResolvedValue({ ffmpeg: { transcode: 'invalid' as any } }); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrow(); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1434,7 +1434,7 @@ describe(MediaService.name, () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.NVENC, targetVideoCodec: VideoCodec.VP9 } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1442,7 +1442,7 @@ describe(MediaService.name, () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: 'invalid' as any } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); @@ -1628,7 +1628,6 @@ describe(MediaService.name, () => { }); it('should set options for qsv', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, maxBitrate: '10000k' } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1664,7 +1663,6 @@ describe(MediaService.name, () => { }); it('should set options for qsv with custom dri node', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { @@ -1690,7 +1688,6 @@ describe(MediaService.name, () => { }); it('should omit preset for qsv if invalid', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, preset: 'invalid' } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1710,7 +1707,6 @@ describe(MediaService.name, () => { }); it('should set low power mode for qsv if target video codec is vp9', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, targetVideoCodec: VideoCodec.VP9 } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1730,17 +1726,18 @@ describe(MediaService.name, () => { }); it('should fail for qsv if no hw devices', async () => { - storageMock.readdir.mockRejectedValue(new Error('Could not read directory')); + sut.videoInterfaces = { dri: [], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); + + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); + expect(mediaMock.transcode).not.toHaveBeenCalled(); - expect(loggerMock.debug).toHaveBeenCalledWith('No devices found in /dev/dri.'); }); it('should prefer higher index renderD* device for qsv', async () => { - storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); + sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1760,7 +1757,6 @@ describe(MediaService.name, () => { }); it('should use hardware decoding for qsv if enabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, @@ -1790,7 +1786,6 @@ describe(MediaService.name, () => { }); it('should use hardware tone-mapping for qsv if hardware decoding is enabled and should tone map', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, @@ -1820,7 +1815,7 @@ describe(MediaService.name, () => { }); it('should use preferred device for qsv when hardware decoding', async () => { - storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']); + sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true, preferredHwDevice: 'renderD129' }, @@ -1840,7 +1835,6 @@ describe(MediaService.name, () => { }); it('should set format to nv12 for qsv if input is not yuv420p', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true }, @@ -1866,7 +1860,6 @@ describe(MediaService.name, () => { }); it('should set options for vaapi', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1898,7 +1891,6 @@ describe(MediaService.name, () => { }); it('should set vbr options for vaapi when max bitrate is enabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, maxBitrate: '10000k' } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1924,7 +1916,6 @@ describe(MediaService.name, () => { }); it('should set cq options for vaapi when max bitrate is disabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1950,7 +1941,6 @@ describe(MediaService.name, () => { }); it('should omit preset for vaapi if invalid', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preset: 'invalid' } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1970,7 +1960,7 @@ describe(MediaService.name, () => { }); it('should prefer higher index renderD* device for vaapi', async () => { - storageMock.readdir.mockResolvedValue(['card1', 'renderD129', 'card0', 'renderD128']); + sut.videoInterfaces = { dri: ['card1', 'renderD129', 'card0', 'renderD128'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -1990,7 +1980,7 @@ describe(MediaService.name, () => { }); it('should select specific gpu node if selected', async () => { - storageMock.readdir.mockResolvedValue(['renderD129', 'card1', 'card0', 'renderD128']); + sut.videoInterfaces = { dri: ['renderD129', 'card1', 'card0', 'renderD128'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, preferredHwDevice: '/dev/dri/renderD128' }, @@ -2012,7 +2002,6 @@ describe(MediaService.name, () => { }); it('should use hardware decoding for vaapi if enabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, @@ -2041,7 +2030,6 @@ describe(MediaService.name, () => { }); it('should use hardware tone-mapping for vaapi if hardware decoding is enabled and should tone map', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, @@ -2066,7 +2054,6 @@ describe(MediaService.name, () => { }); it('should set format to nv12 for vaapi if input is not yuv420p', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true }, @@ -2087,7 +2074,7 @@ describe(MediaService.name, () => { }); it('should use preferred device for vaapi when hardware decoding', async () => { - storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']); + sut.videoInterfaces = { dri: ['renderD128', 'renderD129', 'renderD130'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true, preferredHwDevice: 'renderD129' }, @@ -2106,8 +2093,47 @@ describe(MediaService.name, () => { ); }); - it('should fallback to sw transcoding if hw transcoding fails', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); + it('should fallback to hw encoding and sw decoding if hw transcoding fails and hw decoding is enabled', async () => { + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + mediaMock.transcode.mockRejectedValueOnce(new Error('error')); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledTimes(2); + expect(mediaMock.transcode).toHaveBeenLastCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.arrayContaining([ + '-init_hw_device vaapi=accel:/dev/dri/renderD128', + '-filter_hw_device accel', + ]), + outputOptions: expect.arrayContaining([`-c:v h264_vaapi`]), + twoPass: false, + }), + ); + }); + + it('should fallback to sw decoding if fallback to sw decoding + hw encoding fails', async () => { + mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); + systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true } }); + assetMock.getByIds.mockResolvedValue([assetStub.video]); + mediaMock.transcode.mockRejectedValueOnce(new Error('error')); + mediaMock.transcode.mockRejectedValueOnce(new Error('error')); + await sut.handleVideoConversion({ id: assetStub.video.id }); + expect(mediaMock.transcode).toHaveBeenCalledTimes(3); + expect(mediaMock.transcode).toHaveBeenLastCalledWith( + '/original/path.ext', + 'upload/encoded-video/user-id/as/se/asset-id.mp4', + expect.objectContaining({ + inputOptions: expect.any(Array), + outputOptions: expect.arrayContaining(['-c:v h264']), + twoPass: false, + }), + ); + }); + + it('should fallback to sw transcoding if hw transcoding fails and hw decoding is disabled', async () => { mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -2126,17 +2152,15 @@ describe(MediaService.name, () => { }); it('should fail for vaapi if no hw devices', async () => { - storageMock.readdir.mockResolvedValue([]); + sut.videoInterfaces = { dri: [], mali: true }; mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.VAAPI } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); - await expect(sut.handleVideoConversion({ id: assetStub.video.id })).resolves.toBe(JobStatus.FAILED); + await expect(sut.handleVideoConversion({ id: assetStub.video.id })).rejects.toThrowError(); expect(mediaMock.transcode).not.toHaveBeenCalled(); }); it('should set options for rkmpp', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true } }); assetMock.getByIds.mockResolvedValue([assetStub.video]); @@ -2171,8 +2195,6 @@ describe(MediaService.name, () => { }); it('should set vbr options for rkmpp when max bitrate is enabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats); mediaMock.probe.mockResolvedValue(probeStub.videoStreamVp9); systemMock.get.mockResolvedValue({ ffmpeg: { @@ -2196,8 +2218,6 @@ describe(MediaService.name, () => { }); it('should set cqp options for rkmpp when max bitrate is disabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, @@ -2216,8 +2236,6 @@ describe(MediaService.name, () => { }); it('should set OpenCL tonemapping options for rkmpp when OpenCL is available', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, @@ -2240,8 +2258,7 @@ describe(MediaService.name, () => { }); it('should set hardware decoding options for rkmpp when hardware decoding is enabled with no OpenCL on non-HDR file', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats); + sut.videoInterfaces = { dri: ['renderD128'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, @@ -2262,8 +2279,6 @@ describe(MediaService.name, () => { }); it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats); mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: false, crf: 30, maxBitrate: '0' }, @@ -2286,8 +2301,7 @@ describe(MediaService.name, () => { }); it('should use software tone-mapping if opencl is not available', async () => { - storageMock.readdir.mockResolvedValue(['renderD128']); - storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats); + sut.videoInterfaces = { dri: ['renderD128'], mali: false }; mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR); systemMock.get.mockResolvedValue({ ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' }, diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index f433748ec4..7036bd32e8 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { dirname } from 'node:path'; import { StorageCore } from 'src/cores/storage.core'; -import { OnJob } from 'src/decorators'; +import { OnEvent, OnJob } from 'src/decorators'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { @@ -27,7 +27,7 @@ import { JobStatus, QueueName, } from 'src/interfaces/job.interface'; -import { AudioStreamInfo, TranscodeCommand, VideoFormat, VideoStreamInfo } from 'src/interfaces/media.interface'; +import { AudioStreamInfo, VideoFormat, VideoInterfaces, VideoStreamInfo } from 'src/interfaces/media.interface'; import { BaseService } from 'src/services/base.service'; import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; @@ -36,8 +36,13 @@ import { usePagination } from 'src/utils/pagination'; @Injectable() export class MediaService extends BaseService { - private maliOpenCL?: boolean; - private devices?: string[]; + videoInterfaces: VideoInterfaces = { dri: [], mali: false }; + + @OnEvent({ name: 'app.bootstrap' }) + async onBootstrap() { + const [dri, mali] = await Promise.all([this.getDevices(), this.hasMaliOpenCL()]); + this.videoInterfaces = { dri, mali }; + } @OnJob({ name: JobName.QUEUE_GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION }) async handleQueueGenerateThumbnails({ force }: JobOf): Promise { @@ -300,19 +305,19 @@ export class MediaService extends BaseService { const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input, { countFrames: this.logger.isLevelEnabled(LogLevel.DEBUG), // makes frame count more reliable for progress logs }); - const mainVideoStream = this.getMainStream(videoStreams); - const mainAudioStream = this.getMainStream(audioStreams); - if (!mainVideoStream || !format.formatName) { + const videoStream = this.getMainStream(videoStreams); + const audioStream = this.getMainStream(audioStreams); + if (!videoStream || !format.formatName) { return JobStatus.FAILED; } - if (!mainVideoStream.height || !mainVideoStream.width) { + if (!videoStream.height || !videoStream.width) { this.logger.warn(`Skipped transcoding for asset ${asset.id}: no video streams found`); return JobStatus.FAILED; } - const { ffmpeg } = await this.getConfig({ withCache: true }); - const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream); + let { ffmpeg } = await this.getConfig({ withCache: true }); + const target = this.getTranscodeTarget(ffmpeg, videoStream, audioStream); if (target === TranscodeTarget.NONE && !this.isRemuxRequired(ffmpeg, format)) { if (asset.encodedVideoPath) { this.logger.log(`Transcoded video exists for asset ${asset.id}, but is no longer required. Deleting...`); @@ -325,15 +330,7 @@ export class MediaService extends BaseService { return JobStatus.SKIPPED; } - let command: TranscodeCommand; - try { - const config = BaseConfig.create(ffmpeg, await this.getDevices(), await this.hasMaliOpenCL()); - command = config.getCommand(target, mainVideoStream, mainAudioStream); - } catch (error) { - this.logger.error(`An error occurred while configuring transcoding options: ${error}`); - return JobStatus.FAILED; - } - + const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); if (ffmpeg.accel === TranscodeHWAccel.DISABLED) { this.logger.log(`Transcoding video ${asset.id} without hardware acceleration`); } else { @@ -354,8 +351,8 @@ export class MediaService extends BaseService { if (ffmpeg.accelDecode) { try { this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`); - const config = BaseConfig.create({ ...ffmpeg, accelDecode: false }); - command = config.getCommand(target, mainVideoStream, mainAudioStream); + ffmpeg = { ...ffmpeg, accelDecode: false }; + const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); await this.mediaRepository.transcode(input, output, command); partialFallbackSuccess = true; } catch (error: any) { @@ -365,8 +362,8 @@ export class MediaService extends BaseService { if (!partialFallbackSuccess) { this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`); - const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED }); - command = config.getCommand(target, mainVideoStream, mainAudioStream); + ffmpeg = { ...ffmpeg, accel: TranscodeHWAccel.DISABLED }; + const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream); await this.mediaRepository.transcode(input, output, command); } } @@ -507,30 +504,24 @@ export class MediaService extends BaseService { } private async getDevices() { - if (!this.devices) { - try { - this.devices = await this.storageRepository.readdir('/dev/dri'); - } catch { - this.logger.debug('No devices found in /dev/dri.'); - this.devices = []; - } + try { + return await this.storageRepository.readdir('/dev/dri'); + } catch { + this.logger.debug('No devices found in /dev/dri.'); + return []; } - - return this.devices; } private async hasMaliOpenCL() { - if (this.maliOpenCL === undefined) { - try { - const maliIcdStat = await this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'); - const maliDeviceStat = await this.storageRepository.stat('/dev/mali0'); - this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice(); - } catch { - this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping'); - this.maliOpenCL = false; - } + try { + const [maliIcdStat, maliDeviceStat] = await Promise.all([ + this.storageRepository.stat('/etc/OpenCL/vendors/mali.icd'), + this.storageRepository.stat('/dev/mali0'), + ]); + return maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice(); + } catch { + this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping'); + return false; } - - return this.maliOpenCL; } } diff --git a/server/src/utils/media.ts b/server/src/utils/media.ts index 226f95b4bb..678e8cb15a 100644 --- a/server/src/utils/media.ts +++ b/server/src/utils/media.ts @@ -7,6 +7,7 @@ import { VideoCodecHWConfig, VideoCodecSWConfig, VideoFormat, + VideoInterfaces, VideoStreamInfo, } from 'src/interfaces/media.interface'; @@ -14,11 +15,11 @@ export class BaseConfig implements VideoCodecSWConfig { readonly presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast']; protected constructor(protected config: SystemConfigFFmpegDto) {} - static create(config: SystemConfigFFmpegDto, devices: string[] = [], hasMaliOpenCL = false): VideoCodecSWConfig { + static create(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces): VideoCodecSWConfig { if (config.accel === TranscodeHWAccel.DISABLED) { return this.getSWCodecConfig(config); } - return this.getHWCodecConfig(config, devices, hasMaliOpenCL); + return this.getHWCodecConfig(config, interfaces); } private static getSWCodecConfig(config: SystemConfigFFmpegDto) { @@ -41,27 +42,31 @@ export class BaseConfig implements VideoCodecSWConfig { } } - private static getHWCodecConfig(config: SystemConfigFFmpegDto, devices: string[] = [], hasMaliOpenCL = false) { + private static getHWCodecConfig(config: SystemConfigFFmpegDto, interfaces: VideoInterfaces) { let handler: VideoCodecHWConfig; switch (config.accel) { case TranscodeHWAccel.NVENC: { - handler = config.accelDecode ? new NvencHwDecodeConfig(config) : new NvencSwDecodeConfig(config); + handler = config.accelDecode + ? new NvencHwDecodeConfig(config, interfaces) + : new NvencSwDecodeConfig(config, interfaces); break; } case TranscodeHWAccel.QSV: { - handler = config.accelDecode ? new QsvHwDecodeConfig(config, devices) : new QsvSwDecodeConfig(config, devices); + handler = config.accelDecode + ? new QsvHwDecodeConfig(config, interfaces) + : new QsvSwDecodeConfig(config, interfaces); break; } case TranscodeHWAccel.VAAPI: { handler = config.accelDecode - ? new VaapiHwDecodeConfig(config, devices) - : new VaapiSwDecodeConfig(config, devices); + ? new VaapiHwDecodeConfig(config, interfaces) + : new VaapiSwDecodeConfig(config, interfaces); break; } case TranscodeHWAccel.RKMPP: { handler = config.accelDecode - ? new RkmppHwDecodeConfig(config, devices, hasMaliOpenCL) - : new RkmppSwDecodeConfig(config, devices); + ? new RkmppHwDecodeConfig(config, interfaces) + : new RkmppSwDecodeConfig(config, interfaces); break; } default: { @@ -323,13 +328,15 @@ export class BaseConfig implements VideoCodecSWConfig { export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { protected device: string; + protected interfaces: VideoInterfaces; constructor( protected config: SystemConfigFFmpegDto, - devices: string[] = [], + interfaces: VideoInterfaces, ) { super(config); - this.device = this.getDevice(devices); + this.interfaces = interfaces; + this.device = this.getDevice(interfaces); } getSupportedCodecs() { @@ -346,16 +353,16 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig { }); } - getDevice(devices: string[]) { + getDevice({ dri }: VideoInterfaces) { if (this.config.preferredHwDevice === 'auto') { // eslint-disable-next-line unicorn/no-array-reduce - return `/dev/dri/${this.validateDevices(devices).reduce(function (a, b) { + return `/dev/dri/${this.validateDevices(dri).reduce(function (a, b) { return a.localeCompare(b) < 0 ? b : a; })}`; } const deviceName = this.config.preferredHwDevice.replace('/dev/dri/', ''); - if (!devices.includes(deviceName)) { + if (!dri.includes(deviceName)) { throw new Error(`Device '${deviceName}' does not exist. If using Docker, make sure this device is mounted`); } @@ -886,13 +893,6 @@ export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig { } export class RkmppSwDecodeConfig extends BaseHWConfig { - constructor( - protected config: SystemConfigFFmpegDto, - devices: string[] = [], - ) { - super(config, devices); - } - eligibleForTwoPass(): boolean { return false; } @@ -937,16 +937,6 @@ export class RkmppSwDecodeConfig extends BaseHWConfig { } export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { - protected hasMaliOpenCL: boolean; - constructor( - protected config: SystemConfigFFmpegDto, - devices: string[] = [], - hasMaliOpenCL = false, - ) { - super(config, devices); - this.hasMaliOpenCL = hasMaliOpenCL; - } - getBaseInputOptions() { return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate']; } @@ -954,7 +944,7 @@ export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig { getFilterOptions(videoStream: VideoStreamInfo) { if (this.shouldToneMap(videoStream)) { const { primaries, transfer, matrix } = this.getColors(); - if (this.hasMaliOpenCL) { + if (this.interfaces.mali) { return [ // use RKMPP for scaling, OpenCL for tone mapping `scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`, From 25ca3b112483a8fa42d534473525d450dc7b3f3a Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:22:37 -0500 Subject: [PATCH 06/25] refactor(server): use `includeNull` in query for search suggestions (#14626) * use `includeNull` * push down `includeNull` into query, inner joins * remove filter * update sql * fix tests * maybe fix e2e * more e2e tests * handle no exif row * whoops * update sql --- e2e/src/api/specs/search.e2e-spec.ts | 117 ++++++++++++++++++- server/src/interfaces/search.interface.ts | 24 +++- server/src/queries/search.repository.sql | 25 ++-- server/src/repositories/search.repository.ts | 50 +++++--- server/src/services/search.service.spec.ts | 78 +++++++++++-- server/src/services/search.service.ts | 17 +-- 6 files changed, 259 insertions(+), 52 deletions(-) diff --git a/e2e/src/api/specs/search.e2e-spec.ts b/e2e/src/api/specs/search.e2e-spec.ts index 627fbb3e9e..11bb37be18 100644 --- a/e2e/src/api/specs/search.e2e-spec.ts +++ b/e2e/src/api/specs/search.e2e-spec.ts @@ -98,6 +98,7 @@ describe('/search', () => { { latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh { latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge { latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg + { latitude: 0, longitude: 0 }, // null island ]; const updates = coordinates.map((dto, i) => @@ -532,7 +533,7 @@ describe('/search', () => { expect(body).toEqual(errorDto.unauthorized); }); - it('should get suggestions for country', async () => { + it('should get suggestions for country (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=country&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -555,7 +556,29 @@ describe('/search', () => { expect(status).toBe(200); }); - it('should get suggestions for state', async () => { + it('should get suggestions for country', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=country') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Cuba', + 'France', + 'Georgia', + 'Germany', + 'Ghana', + 'Japan', + 'Morocco', + "People's Republic of China", + 'Russian Federation', + 'Singapore', + 'Spain', + 'Switzerland', + 'United States of America', + ]); + expect(status).toBe(200); + }); + + it('should get suggestions for state (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=state&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -579,7 +602,30 @@ describe('/search', () => { expect(status).toBe(200); }); - it('should get suggestions for city', async () => { + it('should get suggestions for state', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=state') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Andalusia', + 'Berlin', + 'Glarus', + 'Greater Accra', + 'Havana', + 'Île-de-France', + 'Marrakesh-Safi', + 'Mississippi', + 'New York', + 'Shanghai', + 'St.-Petersburg', + 'Tbilisi', + 'Tokyo', + 'Virginia', + ]); + expect(status).toBe(200); + }); + + it('should get suggestions for city (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=city&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -604,7 +650,31 @@ describe('/search', () => { expect(status).toBe(200); }); - it('should get suggestions for camera make', async () => { + it('should get suggestions for city', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=city') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Accra', + 'Berlin', + 'Glarus', + 'Havana', + 'Marrakesh', + 'Montalbán de Córdoba', + 'New York City', + 'Novena', + 'Paris', + 'Philadelphia', + 'Saint Petersburg', + 'Shanghai', + 'Stanley', + 'Tbilisi', + 'Tokyo', + ]); + expect(status).toBe(200); + }); + + it('should get suggestions for camera make (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=camera-make&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -621,7 +691,23 @@ describe('/search', () => { expect(status).toBe(200); }); - it('should get suggestions for camera model', async () => { + it('should get suggestions for camera make', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=camera-make') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Apple', + 'Canon', + 'FUJIFILM', + 'NIKON CORPORATION', + 'PENTAX Corporation', + 'samsung', + 'SONY', + ]); + expect(status).toBe(200); + }); + + it('should get suggestions for camera model (including null)', async () => { const { status, body } = await request(app) .get('/search/suggestions?type=camera-model&includeNull=true') .set('Authorization', `Bearer ${admin.accessToken}`); @@ -642,5 +728,26 @@ describe('/search', () => { ]); expect(status).toBe(200); }); + + it('should get suggestions for camera model', async () => { + const { status, body } = await request(app) + .get('/search/suggestions?type=camera-model') + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([ + 'Canon EOS 7D', + 'Canon EOS R5', + 'DSLR-A550', + 'FinePix S3Pro', + 'iPhone 7', + 'NIKON D700', + 'NIKON D750', + 'NIKON D80', + 'PENTAX K10D', + 'SM-F711N', + 'SM-S906U', + 'SM-T970', + ]); + expect(status).toBe(200); + }); }); }); diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index 87bf1bc4b1..d59291c883 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -170,6 +170,22 @@ export interface AssetDuplicateResult { distance: number; } +export interface GetStatesOptions { + country?: string; +} + +export interface GetCitiesOptions extends GetStatesOptions { + state?: string; +} + +export interface GetCameraModelsOptions { + make?: string; +} + +export interface GetCameraMakesOptions { + model?: string; +} + export interface ISearchRepository { searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated; searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated; @@ -183,8 +199,8 @@ export interface ISearchRepository { getDimensionSize(): Promise; setDimensionSize(dimSize: number): Promise; getCountries(userIds: string[]): Promise>; - getStates(userIds: string[], country?: string): Promise>; - getCities(userIds: string[], country?: string, state?: string): Promise>; - getCameraMakes(userIds: string[], model?: string): Promise>; - getCameraModels(userIds: string[], make?: string): Promise>; + getStates(userIds: string[], options: GetStatesOptions): Promise>; + getCities(userIds: string[], options: GetCitiesOptions): Promise>; + getCameraMakes(userIds: string[], options: GetCameraMakesOptions): Promise>; + getCameraModels(userIds: string[], options: GetCameraModelsOptions): Promise>; } diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 7de61ad03c..1084375059 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -585,52 +585,57 @@ SELECT DISTINCT ON ("exif"."country") "exif"."country" AS "country" FROM "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" IN ($1) + AND "exif"."country" != '' + AND "exif"."country" IS NOT NULL -- SearchRepository.getStates SELECT DISTINCT ON ("exif"."state") "exif"."state" AS "state" FROM "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" IN ($1) - AND "exif"."country" = $2 + AND "exif"."state" != '' + AND "exif"."state" IS NOT NULL -- SearchRepository.getCities SELECT DISTINCT ON ("exif"."city") "exif"."city" AS "city" FROM "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" IN ($1) - AND "exif"."country" = $2 - AND "exif"."state" = $3 + AND "exif"."city" != '' + AND "exif"."city" IS NOT NULL -- SearchRepository.getCameraMakes SELECT DISTINCT ON ("exif"."make") "exif"."make" AS "make" FROM "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" IN ($1) - AND "exif"."model" = $2 + AND "exif"."make" != '' + AND "exif"."make" IS NOT NULL -- SearchRepository.getCameraModels SELECT DISTINCT ON ("exif"."model") "exif"."model" AS "model" FROM "exif" "exif" - LEFT JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" + INNER JOIN "assets" "asset" ON "asset"."id" = "exif"."assetId" AND ("asset"."deletedAt" IS NULL) WHERE "asset"."ownerId" IN ($1) - AND "exif"."make" = $2 + AND "exif"."model" != '' + AND "exif"."model" IS NOT NULL diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index ba7d779e02..0a529f2f6e 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -17,6 +17,10 @@ import { AssetSearchOptions, FaceEmbeddingSearch, FaceSearchResult, + GetCameraMakesOptions, + GetCameraModelsOptions, + GetCitiesOptions, + GetStatesOptions, ISearchRepository, SearchPaginationOptions, SmartSearchOptions, @@ -342,23 +346,27 @@ export class SearchRepository implements ISearchRepository { @GenerateSql({ params: [[DummyValue.UUID]] }) async getCountries(userIds: string[]): Promise { - const results = await this.exifRepository + const query = this.exifRepository .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') + .innerJoin('exif.asset', 'asset') .where('asset.ownerId IN (:...userIds )', { userIds }) + .andWhere(`exif.country != ''`) + .andWhere('exif.country IS NOT NULL') .select('exif.country', 'country') - .distinctOn(['exif.country']) - .getRawMany<{ country: string }>(); + .distinctOn(['exif.country']); - return results.map(({ country }) => country).filter((item) => item !== ''); + const results = await query.getRawMany<{ country: string }>(); + return results.map(({ country }) => country); } @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getStates(userIds: string[], country: string | undefined): Promise { + async getStates(userIds: string[], { country }: GetStatesOptions): Promise { const query = this.exifRepository .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') + .innerJoin('exif.asset', 'asset') .where('asset.ownerId IN (:...userIds )', { userIds }) + .andWhere(`exif.state != ''`) + .andWhere('exif.state IS NOT NULL') .select('exif.state', 'state') .distinctOn(['exif.state']); @@ -367,16 +375,17 @@ export class SearchRepository implements ISearchRepository { } const result = await query.getRawMany<{ state: string }>(); - - return result.map(({ state }) => state).filter((item) => item !== ''); + return result.map(({ state }) => state); } @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING, DummyValue.STRING] }) - async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise { + async getCities(userIds: string[], { country, state }: GetCitiesOptions): Promise { const query = this.exifRepository .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') + .innerJoin('exif.asset', 'asset') .where('asset.ownerId IN (:...userIds )', { userIds }) + .andWhere(`exif.city != ''`) + .andWhere('exif.city IS NOT NULL') .select('exif.city', 'city') .distinctOn(['exif.city']); @@ -389,16 +398,17 @@ export class SearchRepository implements ISearchRepository { } const results = await query.getRawMany<{ city: string }>(); - - return results.map(({ city }) => city).filter((item) => item !== ''); + return results.map(({ city }) => city); } @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getCameraMakes(userIds: string[], model: string | undefined): Promise { + async getCameraMakes(userIds: string[], { model }: GetCameraMakesOptions): Promise { const query = this.exifRepository .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') + .innerJoin('exif.asset', 'asset') .where('asset.ownerId IN (:...userIds )', { userIds }) + .andWhere(`exif.make != ''`) + .andWhere('exif.make IS NOT NULL') .select('exif.make', 'make') .distinctOn(['exif.make']); @@ -407,15 +417,17 @@ export class SearchRepository implements ISearchRepository { } const results = await query.getRawMany<{ make: string }>(); - return results.map(({ make }) => make).filter((item) => item !== ''); + return results.map(({ make }) => make); } @GenerateSql({ params: [[DummyValue.UUID], DummyValue.STRING] }) - async getCameraModels(userIds: string[], make: string | undefined): Promise { + async getCameraModels(userIds: string[], { make }: GetCameraModelsOptions): Promise { const query = this.exifRepository .createQueryBuilder('exif') - .leftJoin('exif.asset', 'asset') + .innerJoin('exif.asset', 'asset') .where('asset.ownerId IN (:...userIds )', { userIds }) + .andWhere(`exif.model != ''`) + .andWhere('exif.model IS NOT NULL') .select('exif.model', 'model') .distinctOn(['exif.model']); @@ -424,7 +436,7 @@ export class SearchRepository implements ISearchRepository { } const results = await query.getRawMany<{ model: string }>(); - return results.map(({ model }) => model).filter((item) => item !== ''); + return results.map(({ model }) => model); } private getRuntimeConfig(numResults?: number): string | undefined { diff --git a/server/src/services/search.service.spec.ts b/server/src/services/search.service.spec.ts index 0f95d88083..3933526167 100644 --- a/server/src/services/search.service.spec.ts +++ b/server/src/services/search.service.spec.ts @@ -59,20 +59,84 @@ describe(SearchService.name, () => { }); describe('getSearchSuggestions', () => { - it('should return search suggestions (including null)', async () => { - searchMock.getCountries.mockResolvedValue(['USA', null]); + it('should return search suggestions for country', async () => { + searchMock.getCountries.mockResolvedValue(['USA']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), + ).resolves.toEqual(['USA']); + expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); + }); + + it('should return search suggestions for country (including null)', async () => { + searchMock.getCountries.mockResolvedValue(['USA']); await expect( sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }), ).resolves.toEqual(['USA', null]); expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); }); - it('should return search suggestions (without null)', async () => { - searchMock.getCountries.mockResolvedValue(['USA', null]); + it('should return search suggestions for state', async () => { + searchMock.getStates.mockResolvedValue(['California']); await expect( - sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }), - ).resolves.toEqual(['USA']); - expect(searchMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]); + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.STATE }), + ).resolves.toEqual(['California']); + expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for state (including null)', async () => { + searchMock.getStates.mockResolvedValue(['California']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.STATE }), + ).resolves.toEqual(['California', null]); + expect(searchMock.getStates).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for city', async () => { + searchMock.getCities.mockResolvedValue(['Denver']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CITY }), + ).resolves.toEqual(['Denver']); + expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for city (including null)', async () => { + searchMock.getCities.mockResolvedValue(['Denver']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CITY }), + ).resolves.toEqual(['Denver', null]); + expect(searchMock.getCities).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for camera make', async () => { + searchMock.getCameraMakes.mockResolvedValue(['Nikon']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MAKE }), + ).resolves.toEqual(['Nikon']); + expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for camera make (including null)', async () => { + searchMock.getCameraMakes.mockResolvedValue(['Nikon']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MAKE }), + ).resolves.toEqual(['Nikon', null]); + expect(searchMock.getCameraMakes).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for camera model', async () => { + searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.CAMERA_MODEL }), + ).resolves.toEqual(['Fujifilm X100VI']); + expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); + }); + + it('should return search suggestions for camera model (including null)', async () => { + searchMock.getCameraModels.mockResolvedValue(['Fujifilm X100VI']); + await expect( + sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.CAMERA_MODEL }), + ).resolves.toEqual(['Fujifilm X100VI', null]); + expect(searchMock.getCameraModels).toHaveBeenCalledWith([authStub.user1.user.id], expect.anything()); }); }); }); diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index bf5bf9e311..7fc947a8b5 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -108,8 +108,11 @@ export class SearchService extends BaseService { async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) { const userIds = await this.getUserIdsToSearch(auth); - const results = await this.getSuggestions(userIds, dto); - return results.filter((result) => (dto.includeNull ? true : result !== null)); + const suggestions = await this.getSuggestions(userIds, dto); + if (dto.includeNull) { + suggestions.push(null); + } + return suggestions; } private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) { @@ -118,19 +121,19 @@ export class SearchService extends BaseService { return this.searchRepository.getCountries(userIds); } case SearchSuggestionType.STATE: { - return this.searchRepository.getStates(userIds, dto.country); + return this.searchRepository.getStates(userIds, dto); } case SearchSuggestionType.CITY: { - return this.searchRepository.getCities(userIds, dto.country, dto.state); + return this.searchRepository.getCities(userIds, dto); } case SearchSuggestionType.CAMERA_MAKE: { - return this.searchRepository.getCameraMakes(userIds, dto.model); + return this.searchRepository.getCameraMakes(userIds, dto); } case SearchSuggestionType.CAMERA_MODEL: { - return this.searchRepository.getCameraModels(userIds, dto.make); + return this.searchRepository.getCameraModels(userIds, dto); } default: { - return []; + return [] as (string | null)[]; } } } From 9eff1c4b34ace08871c8abf628adc9fcfe37a41d Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 10 Dec 2024 16:22:47 -0500 Subject: [PATCH 07/25] refactor(server): move filters to getByDayOfYear query (#14628) move filters to getByDayOfYear query --- server/src/interfaces/asset.interface.ts | 7 ++++- server/src/queries/asset.repository.sql | 19 ++++++-------- server/src/repositories/asset.repository.ts | 23 +++++++++++++--- server/src/services/asset.service.spec.ts | 15 ++++++++++- server/src/services/asset.service.ts | 29 +++++---------------- 5 files changed, 55 insertions(+), 38 deletions(-) diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 1b32c57d41..b25e42ba0e 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -146,6 +146,11 @@ export interface UpsertFileOptions { export type AssetPathEntity = Pick; +export interface DayOfYearAssets { + yearsAgo: number; + assets: AssetEntity[]; +} + export const IAssetRepository = 'IAssetRepository'; export interface IAssetRepository { @@ -156,7 +161,7 @@ export interface IAssetRepository { select?: FindOptionsSelect, ): Promise; getByIdsWithAllRelations(ids: string[]): Promise; - getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; + getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise; getByChecksum(options: { ownerId: string; checksum: Buffer; libraryId?: string }): Promise; getByChecksums(userId: string, checksums: Buffer[]): Promise; getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise; diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index e7f5b558b0..f4b1b2fea1 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -68,22 +68,19 @@ SELECT FROM "assets" "entity" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" + INNER JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" WHERE ( - "entity"."ownerId" IN ($1) - AND "entity"."isVisible" = true - AND "entity"."isArchived" = false + "files"."type" = $1 AND EXTRACT( - DAY + YEAR + FROM + CURRENT_DATE AT TIME ZONE 'UTC' + ) - EXTRACT( + YEAR FROM "entity"."localDateTime" AT TIME ZONE 'UTC' - ) = $2 - AND EXTRACT( - MONTH - FROM - "entity"."localDateTime" AT TIME ZONE 'UTC' - ) = $3 + ) > 0 ) AND ("entity"."deletedAt" IS NULL) ORDER BY diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index ce7d257b40..b3066a37bc 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -17,6 +17,7 @@ import { AssetUpdateAllOptions, AssetUpdateDuplicateOptions, AssetUpdateOptions, + DayOfYearAssets, IAssetRepository, LivePhotoSearchOptions, MonthDay, @@ -74,8 +75,8 @@ export class AssetRepository implements IAssetRepository { } @GenerateSql({ params: [[DummyValue.UUID], { day: 1, month: 1 }] }) - getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { - return this.repository + async getByDayOfYear(ownerIds: string[], { day, month }: MonthDay): Promise { + const assets = await this.repository .createQueryBuilder('entity') .where( `entity.ownerId IN (:...ownerIds) @@ -90,9 +91,25 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') - .leftJoinAndSelect('entity.files', 'files') + .innerJoinAndSelect('entity.files', 'files') + .where('files.type = :type', { type: AssetFileType.THUMBNAIL }) + .andWhere( + `EXTRACT(YEAR FROM CURRENT_DATE AT TIME ZONE 'UTC') - EXTRACT(YEAR FROM entity.localDateTime AT TIME ZONE 'UTC') > 0`, + ) .orderBy('entity.fileCreatedAt', 'ASC') .getMany(); + + const groups: Record = {}; + const currentYear = new Date().getFullYear(); + for (const asset of assets) { + const yearsAgo = currentYear - asset.localDateTime.getFullYear(); + if (!groups[yearsAgo]) { + groups[yearsAgo] = { yearsAgo, assets: [] }; + } + groups[yearsAgo].assets.push(asset); + } + + return Object.values(groups); } @GenerateSql({ params: [[DummyValue.UUID]] }) diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 9063df9dc2..5aab5032af 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -80,7 +80,20 @@ describe(AssetService.name, () => { const image4 = { ...assetStub.image, localDateTime: new Date(2009, 1, 15) }; partnerMock.getAll.mockResolvedValue([]); - assetMock.getByDayOfYear.mockResolvedValue([image1, image2, image3, image4]); + assetMock.getByDayOfYear.mockResolvedValue([ + { + yearsAgo: 1, + assets: [image1, image2], + }, + { + yearsAgo: 9, + assets: [image3], + }, + { + yearsAgo: 15, + assets: [image4], + }, + ]); await expect(sut.getMemoryLane(authStub.admin, { day: 15, month: 1 })).resolves.toEqual([ { yearsAgo: 1, title: '1 year ago', assets: [mapAsset(image1), mapAsset(image2)] }, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 98d6ec00f6..8751037119 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -43,28 +43,13 @@ export class AssetService extends BaseService { }); const userIds = [auth.user.id, ...partnerIds]; - const assets = await this.assetRepository.getByDayOfYear(userIds, dto); - const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile); - const groups: Record = {}; - const currentYear = new Date().getFullYear(); - for (const asset of assetsWithThumbnails) { - const yearsAgo = currentYear - asset.localDateTime.getFullYear(); - if (!groups[yearsAgo]) { - groups[yearsAgo] = []; - } - groups[yearsAgo].push(asset); - } - - return Object.keys(groups) - .map(Number) - .sort((a, b) => a - b) - .filter((yearsAgo) => yearsAgo > 0) - .map((yearsAgo) => ({ - yearsAgo, - // TODO move this to clients - title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`, - assets: groups[yearsAgo].map((asset) => mapAsset(asset, { auth })), - })); + const groups = await this.assetRepository.getByDayOfYear(userIds, dto); + return groups.map(({ yearsAgo, assets }) => ({ + yearsAgo, + // TODO move this to clients + title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} ago`, + assets: assets.map((asset) => mapAsset(asset, { auth })), + })); } async getStatistics(auth: AuthDto, dto: AssetStatsDto) { From 345f918784ca8701b34c5d28be2edd74a89b0cb2 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Wed, 11 Dec 2024 01:42:45 +0400 Subject: [PATCH 08/25] chore(docs): stronger discouraging of non-Linux installations (#14620) * no windows! * 2 * 3 * Update docs/docs/install/requirements.md Co-authored-by: bo0tzz * Update requirements.md --------- Co-authored-by: bo0tzz --- docs/docs/administration/backup-and-restore.md | 7 ++++++- docs/docs/install/requirements.md | 7 +++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 9ae4e3e51f..1f8d489728 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -65,12 +65,17 @@ docker compose up -d # Start remainder of Immich apps docker compose down -v # CAUTION! Deletes all Immich data to start from scratch ## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database # Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch +## You should mount the backup (as a volume, example: - 'C:\path\to\backup\dump.sql':/dump.sql) into the immich_postgres container using the docker-compose.yml docker compose pull # Update to latest version of Immich (if desired) docker compose create # Create Docker containers for Immich apps without running them docker start immich_postgres # Start Postgres server sleep 10 # Wait for Postgres server to start up +docker exec -it immich_postgres bash # Enter the Docker shell and run the following command # Check the database user if you deviated from the default -gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup +cat "/dump.sql" \ +| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \ +| psql --username=postgres # Restore Backup +exit # Exit the Docker shell docker compose up -d # Start remainder of Immich apps ``` diff --git a/docs/docs/install/requirements.md b/docs/docs/install/requirements.md index 74c4a2f831..6dc613389e 100644 --- a/docs/docs/install/requirements.md +++ b/docs/docs/install/requirements.md @@ -18,8 +18,11 @@ Immich requires the command `docker compose` - the similarly named `docker-compo ## Hardware - **OS**: Recommended Linux operating system (Ubuntu, Debian, etc). - - Windows is supported with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/). - - macOS is supported with [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/). + - Non-Linux OSes tend to provide a poor Docker experience and are strongly discouraged. + Our ability to assist with setup or troubleshooting on non-Linux OSes will be severely reduced. + If you still want to try to use a non-Linux OS, you can set it up as follows: + - Windows: [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/). + - macOS: [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/). - **RAM**: Minimum 4GB, recommended 6GB. - **CPU**: Minimum 2 cores, recommended 4 cores. - **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions. From 70b4647a21df9c6d6ed875a61ca8d70e2bc88f7d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 10 Dec 2024 15:55:59 -0600 Subject: [PATCH 09/25] chore(mobile): post release tasks (#14603) --- mobile/ios/Runner/Info.plist | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index e85afdc852..28d21e266e 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,7 +58,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.122.0 + 1.122.2 CFBundleSignature ???? CFBundleVersion From f6909a3b110fb4a82ffab8f9f865dd19d737107c Mon Sep 17 00:00:00 2001 From: vladd11 Date: Wed, 11 Dec 2024 00:58:14 +0300 Subject: [PATCH 10/25] chore(docs): add Kodi plugin for Immich to the Community Projects list (#14586) --- docs/src/components/community-projects.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 596bf9dfc4..2dbab979f2 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -89,6 +89,11 @@ const projects: CommunityProjectProps[] = [ 'Share your Immich photos and albums in a safe way without exposing your Immich instance to the public.', url: 'https://github.com/alangrainger/immich-public-proxy', }, + { + title: 'Immich Kodi', + description: 'Unofficial Kodi plugin for Immich.', + url: 'https://github.com/vladd11/immich-kodi', + }, ]; function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element { From 7cae25c28b6ecdcb7bc0cd8976212d53abe5e883 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 15:59:45 -0600 Subject: [PATCH 11/25] chore(deps): update prom/prometheus docker digest to 565ee86 (#14535) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 704f3bdfc8..d58b20ef76 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -91,7 +91,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:3b9b2a15d376334da8c286d995777d3b9315aa666d2311170ada6059a517b74f + image: prom/prometheus@sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From bcc438eafbd6908306a41bc82afe0fd2dfe6fecc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 22:00:01 +0000 Subject: [PATCH 12/25] fix(deps): update dependency python-multipart to v0.0.18 [security] (#14458) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/poetry.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 867b76c9e5..229db1303b 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2746,13 +2746,13 @@ cli = ["click (>=5.0)"] [[package]] name = "python-multipart" -version = "0.0.17" +version = "0.0.19" description = "A streaming multipart parser for Python" optional = false python-versions = ">=3.8" files = [ - {file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"}, - {file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"}, + {file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"}, + {file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"}, ] [[package]] From 5814a1b22320b75137086c35407e9864519ad17b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 10 Dec 2024 17:07:16 -0600 Subject: [PATCH 13/25] chore(deps): update docker/build-push-action action to v6.10.0 (#14631) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 7052fa6ef9..da383c3e2d 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.10.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 034fbe0008..7ec0cc0947 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -174,7 +174,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.10.0 with: context: ${{ env.context }} file: ${{ env.file }} @@ -265,7 +265,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.9.0 + uses: docker/build-push-action@v6.10.0 with: context: ${{ env.context }} file: ${{ env.file }} From 11f585d0adf384cbabae78a8ec3b4709449cb90f Mon Sep 17 00:00:00 2001 From: dvbthien <89862334+dvbthien@users.noreply.github.com> Date: Wed, 11 Dec 2024 23:30:56 +0700 Subject: [PATCH 14/25] refactor(mobile): refactor theme management (#14415) --- mobile/lib/constants/colors.dart | 23 + mobile/lib/main.dart | 36 +- mobile/lib/pages/common/settings.page.dart | 1 + mobile/lib/pages/search/map/map.page.dart | 2 +- .../search/map/map_location_picker.page.dart | 2 +- mobile/lib/providers/theme.provider.dart | 74 ++++ mobile/lib/services/app_settings.service.dart | 2 +- .../color_scheme.dart} | 29 +- mobile/lib/theme/dynamic_theme.dart | 38 ++ .../theme_data.dart} | 407 +++++++----------- .../asset_viewer/motion_photo_button.dart | 2 +- .../widgets/asset_viewer/video_position.dart | 2 +- mobile/lib/widgets/backup/error_chip.dart | 2 +- .../lib/widgets/backup/error_chip_text.dart | 2 +- .../lib/widgets/map/map_theme_override.dart | 11 +- mobile/lib/widgets/map/map_thumbnail.dart | 2 +- .../primary_color_setting.dart | 16 +- .../preference_settings/theme_setting.dart | 2 +- .../modules/map/map_theme_override_test.dart | 12 +- 19 files changed, 343 insertions(+), 322 deletions(-) create mode 100644 mobile/lib/constants/colors.dart create mode 100644 mobile/lib/providers/theme.provider.dart rename mobile/lib/{constants/immich_colors.dart => theme/color_scheme.dart} (80%) create mode 100644 mobile/lib/theme/dynamic_theme.dart rename mobile/lib/{utils/immich_app_theme.dart => theme/theme_data.dart} (58%) diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart new file mode 100644 index 0000000000..ade878d6f6 --- /dev/null +++ b/mobile/lib/constants/colors.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; + +enum ImmichColorPreset { + indigo, + deepPurple, + pink, + red, + orange, + yellow, + lime, + green, + cyan, + slateGray +} + +const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; +const String defaultColorPresetName = "indigo"; + +const Color immichBrandColorLight = Color(0xFF4150AF); +const Color immichBrandColorDark = Color(0xFFACCBFA); +const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); +const Color red400 = Color(0xFFEF5350); +const Color grey200 = Color(0xFFEEEEEE); diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 7729972aa2..807212fc65 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -4,23 +4,26 @@ import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:easy_localization/easy_localization.dart'; +import 'package:intl/date_symbol_data_local.dart'; +import 'package:timezone/data/latest.dart'; +import 'package:isar/isar.dart'; +import 'package:logging/logging.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/locale_provider.dart'; -import 'package:immich_mobile/utils/download.dart'; -import 'package:intl/date_symbol_data_local.dart'; -import 'package:timezone/data/latest.dart'; import 'package:immich_mobile/constants/locales.dart'; -import 'package:immich_mobile/services/background.service.dart'; -import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; +import 'package:immich_mobile/providers/locale_provider.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/routing/tab_navigation_observer.dart'; -import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/android_device_asset.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; @@ -30,16 +33,15 @@ import 'package:immich_mobile/entities/ios_device_asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; -import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; -import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/migration.dart'; -import 'package:isar/isar.dart'; -import 'package:logging/logging.dart'; -import 'package:path_provider/path_provider.dart'; +import 'package:immich_mobile/utils/download.dart'; +import 'package:immich_mobile/utils/cache/widgets_binding.dart'; +import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; void main() async { ImmichWidgetsBinding(); @@ -69,12 +71,12 @@ Future initApp() async { } } - await fetchSystemPalette(); + await DynamicTheme.fetchSystemPalette(); // Initialize Immich Logger Service ImmichLogger(); - var log = Logger("ImmichErrorLogger"); + final log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { FlutterError.presentError(details); diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index ba3150c046..3cbded1787 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -133,6 +133,7 @@ class _MobileLayout extends StatelessWidget { ).tr(), subtitle: Text( setting.subtitle, + style: context.textTheme.labelLarge, ).tr(), onTap: () => context.pushRoute(SettingsSubRoute(section: setting)), diff --git a/mobile/lib/pages/search/map/map.page.dart b/mobile/lib/pages/search/map/map.page.dart index 10fe8de541..52ce13f958 100644 --- a/mobile/lib/pages/search/map/map.page.dart +++ b/mobile/lib/pages/search/map/map.page.dart @@ -264,7 +264,7 @@ class MapPage extends HookConsumerWidget { selectedAssets.value = selected ? selection : {}; } - return MapThemeOveride( + return MapThemeOverride( mapBuilder: (style) => context.isMobile // Single-column ? Scaffold( diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index 2fd1e1ee9e..487de69a1e 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -58,7 +58,7 @@ class MapLocationPickerPage extends HookConsumerWidget { controller.value?.animateCamera(CameraUpdate.newLatLng(currentLatLng)); } - return MapThemeOveride( + return MapThemeOverride( mapBuilder: (style) => Builder( builder: (ctx) => Scaffold( backgroundColor: ctx.themeData.cardColor, diff --git a/mobile/lib/providers/theme.provider.dart b/mobile/lib/providers/theme.provider.dart new file mode 100644 index 0000000000..73623bd026 --- /dev/null +++ b/mobile/lib/providers/theme.provider.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/theme/color_scheme.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; + +final immichThemeModeProvider = StateProvider((ref) { + final themeMode = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.themeMode); + + debugPrint("Current themeMode $themeMode"); + + if (themeMode == ThemeMode.light.name) { + return ThemeMode.light; + } else if (themeMode == ThemeMode.dark.name) { + return ThemeMode.dark; + } else { + return ThemeMode.system; + } +}); + +final immichThemePresetProvider = StateProvider((ref) { + final appSettingsProvider = ref.watch(appSettingsServiceProvider); + final primaryColorPreset = + appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); + + debugPrint("Current theme preset $primaryColorPreset"); + + try { + return ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorPreset); + } catch (e) { + debugPrint( + "Theme preset $primaryColorPreset not found. Applying default preset.", + ); + appSettingsProvider.setSetting( + AppSettingsEnum.primaryColor, + defaultColorPresetName, + ); + return defaultColorPreset; + } +}); + +final dynamicThemeSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.dynamicTheme); +}); + +final colorfulInterfaceSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.colorfulInterface); +}); + +// Provider for current selected theme +final immichThemeProvider = StateProvider((ref) { + final primaryColorPreset = ref.read(immichThemePresetProvider); + final useSystemColor = ref.watch(dynamicThemeSettingProvider); + final useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); + final ImmichTheme? dynamicTheme = DynamicTheme.theme; + final currentTheme = (useSystemColor && dynamicTheme != null) + ? dynamicTheme + : primaryColorPreset.themeOfPreset; + + return useColorfulInterface + ? currentTheme + : decolorizeSurfaces(theme: currentTheme); +}); diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index 14d800a4ef..c3fde894d5 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,4 +1,4 @@ -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/theme/color_scheme.dart similarity index 80% rename from mobile/lib/constants/immich_colors.dart rename to mobile/lib/theme/color_scheme.dart index 847887de8c..c01b7cfa5a 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/theme/color_scheme.dart @@ -1,29 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/constants/colors.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; -enum ImmichColorPreset { - indigo, - deepPurple, - pink, - red, - orange, - yellow, - lime, - green, - cyan, - slateGray -} - -const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; -const String defaultColorPresetName = "indigo"; - -const Color immichBrandColorLight = Color(0xFF4150AF); -const Color immichBrandColorDark = Color(0xFFACCBFA); -const Color whiteOpacity75 = Color.fromARGB((0.75 * 255) ~/ 1, 255, 255, 255); -const Color red400 = Color(0xFFEF5350); -const Color grey200 = Color(0xFFEEEEEE); - -final Map _themePresetsMap = { +final Map _themePresets = { ImmichColorPreset.indigo: ImmichTheme( light: ColorScheme.fromSeed( seedColor: immichBrandColorLight, @@ -110,5 +89,5 @@ final Map _themePresetsMap = { }; extension ImmichColorModeExtension on ImmichColorPreset { - ImmichTheme getTheme() => _themePresetsMap[this]!; + ImmichTheme get themeOfPreset => _themePresets[this]!; } diff --git a/mobile/lib/theme/dynamic_theme.dart b/mobile/lib/theme/dynamic_theme.dart new file mode 100644 index 0000000000..39d6b6ee45 --- /dev/null +++ b/mobile/lib/theme/dynamic_theme.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:dynamic_color/dynamic_color.dart'; + +import 'package:immich_mobile/theme/theme_data.dart'; + +abstract final class DynamicTheme { + DynamicTheme._(); + + static ImmichTheme? _theme; + // Method to fetch dynamic system colors + static Future fetchSystemPalette() async { + try { + final corePalette = await DynamicColorPlugin.getCorePalette(); + if (corePalette != null) { + final primaryColor = corePalette.toColorScheme().primary; + debugPrint('dynamic_color: Core palette detected.'); + + // Some palettes do not generate surface container colors accurately, + // so we regenerate all colors using the primary color + _theme = ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + ), + dark: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ), + ); + } + } catch (error) { + debugPrint('dynamic_color: Failed to obtain core palette: $error'); + } + } + + static ImmichTheme? get theme => _theme; + static bool get isAvailable => _theme != null; +} diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/theme/theme_data.dart similarity index 58% rename from mobile/lib/utils/immich_app_theme.dart rename to mobile/lib/theme/theme_data.dart index 2ca4fe3aff..de96e12c5d 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/theme/theme_data.dart @@ -1,11 +1,7 @@ -import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; + import 'package:immich_mobile/constants/locales.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; -import 'package:immich_mobile/providers/app_settings.provider.dart'; -import 'package:immich_mobile/services/app_settings.service.dart'; class ImmichTheme { final ColorScheme light; @@ -14,104 +10,166 @@ class ImmichTheme { const ImmichTheme({required this.light, required this.dark}); } -ImmichTheme? _immichDynamicTheme; -bool get isDynamicThemeAvailable => _immichDynamicTheme != null; +ThemeData getThemeData({ + required ColorScheme colorScheme, + required Locale locale, +}) { + final isDark = colorScheme.brightness == Brightness.dark; -final immichThemeModeProvider = StateProvider((ref) { - var themeMode = ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.themeMode); - - debugPrint("Current themeMode $themeMode"); - - if (themeMode == "light") { - return ThemeMode.light; - } else if (themeMode == "dark") { - return ThemeMode.dark; - } else { - return ThemeMode.system; - } -}); - -final immichThemePresetProvider = StateProvider((ref) { - var appSettingsProvider = ref.watch(appSettingsServiceProvider); - var primaryColorName = - appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); - - debugPrint("Current theme preset $primaryColorName"); - - try { - return ImmichColorPreset.values - .firstWhere((e) => e.name == primaryColorName); - } catch (e) { - debugPrint( - "Theme preset $primaryColorName not found. Applying default preset.", - ); - appSettingsProvider.setSetting( - AppSettingsEnum.primaryColor, - defaultColorPresetName, - ); - return defaultColorPreset; - } -}); - -final dynamicThemeSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.dynamicTheme); -}); - -final colorfulInterfaceSettingProvider = StateProvider((ref) { - return ref - .watch(appSettingsServiceProvider) - .getSetting(AppSettingsEnum.colorfulInterface); -}); - -// Provider for current selected theme -final immichThemeProvider = StateProvider((ref) { - var primaryColor = ref.read(immichThemePresetProvider); - var useSystemColor = ref.watch(dynamicThemeSettingProvider); - var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); - - var currentTheme = (useSystemColor && _immichDynamicTheme != null) - ? _immichDynamicTheme! - : primaryColor.getTheme(); - - return useColorfulInterface - ? currentTheme - : _decolorizeSurfaces(theme: currentTheme); -}); - -// Method to fetch dynamic system colors -Future fetchSystemPalette() async { - try { - final corePalette = await DynamicColorPlugin.getCorePalette(); - if (corePalette != null) { - final primaryColor = corePalette.toColorScheme().primary; - debugPrint('dynamic_color: Core palette detected.'); - - // Some palettes do not generate surface container colors accurately, - // so we regenerate all colors using the primary color - _immichDynamicTheme = ImmichTheme( - light: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.light, + return ThemeData( + useMaterial3: true, + brightness: colorScheme.brightness, + colorScheme: colorScheme, + primaryColor: colorScheme.primary, + hintColor: colorScheme.onSurfaceSecondary, + focusColor: colorScheme.primary, + scaffoldBackgroundColor: colorScheme.surface, + splashColor: colorScheme.primary.withOpacity(0.1), + highlightColor: colorScheme.primary.withOpacity(0.1), + dialogBackgroundColor: colorScheme.surfaceContainer, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: colorScheme.surfaceContainer, + ), + fontFamily: _getFontFamilyFromLocale(locale), + snackBarTheme: SnackBarThemeData( + contentTextStyle: TextStyle( + fontFamily: _getFontFamilyFromLocale(locale), + color: colorScheme.primary, + fontWeight: FontWeight.bold, + ), + backgroundColor: colorScheme.surfaceContainerHighest, + ), + appBarTheme: AppBarTheme( + titleTextStyle: TextStyle( + color: colorScheme.primary, + fontFamily: _getFontFamilyFromLocale(locale), + fontWeight: FontWeight.bold, + fontSize: 18, + ), + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + foregroundColor: colorScheme.primary, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, + ), + textTheme: const TextTheme( + displayLarge: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + ), + displayMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + displaySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + titleSmall: TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + titleMedium: TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: colorScheme.primary, + foregroundColor: isDark ? Colors.black87 : Colors.white, + ), + ), + chipTheme: const ChipThemeData( + side: BorderSide.none, + ), + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, + ), + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + popupMenuTheme: const PopupMenuThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + labelTextStyle: const WidgetStatePropertyAll( + TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, ), - dark: ColorScheme.fromSeed( - seedColor: primaryColor, - brightness: Brightness.dark, + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.primary, ), - ); - } - } catch (e) { - debugPrint('dynamic_color: Failed to obtain core palette.'); - } + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: colorScheme.primary, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: colorScheme.primary, + ), + dropdownMenuTheme: DropdownMenuThemeData( + menuStyle: const MenuStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(15)), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.primary, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: colorScheme.primary, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + ), + ); } // This method replaces all surface shades in ImmichTheme to a static ones // as we are creating the colorscheme through seedColor the default surfaces are // tinted with primary color -ImmichTheme _decolorizeSurfaces({ +ImmichTheme decolorizeSurfaces({ required ImmichTheme theme, }) { return ImmichTheme( @@ -146,167 +204,10 @@ ImmichTheme _decolorizeSurfaces({ ); } -String? getFontFamilyFromLocale(Locale locale) { +String? _getFontFamilyFromLocale(Locale locale) { if (localesNotSupportedByOverpass.contains(locale)) { // Let Flutter use the default font return null; } return 'Overpass'; } - -ThemeData getThemeData({ - required ColorScheme colorScheme, - required Locale locale, -}) { - var isDark = colorScheme.brightness == Brightness.dark; - var primaryColor = colorScheme.primary; - - return ThemeData( - useMaterial3: true, - brightness: colorScheme.brightness, - colorScheme: colorScheme, - primaryColor: primaryColor, - hintColor: colorScheme.onSurfaceSecondary, - focusColor: primaryColor, - scaffoldBackgroundColor: colorScheme.surface, - splashColor: primaryColor.withOpacity(0.1), - highlightColor: primaryColor.withOpacity(0.1), - dialogBackgroundColor: colorScheme.surfaceContainer, - bottomSheetTheme: BottomSheetThemeData( - backgroundColor: colorScheme.surfaceContainer, - ), - fontFamily: getFontFamilyFromLocale(locale), - snackBarTheme: SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: getFontFamilyFromLocale(locale), - color: primaryColor, - fontWeight: FontWeight.bold, - ), - backgroundColor: colorScheme.surfaceContainerHighest, - ), - appBarTheme: AppBarTheme( - titleTextStyle: TextStyle( - color: primaryColor, - fontFamily: getFontFamilyFromLocale(locale), - fontWeight: FontWeight.bold, - fontSize: 18, - ), - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, - foregroundColor: primaryColor, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: primaryColor, - foregroundColor: isDark ? Colors.black87 : Colors.white, - ), - ), - chipTheme: const ChipThemeData( - side: BorderSide.none, - ), - sliderTheme: const SliderThemeData( - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), - trackHeight: 2.0, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - ), - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - ), - navigationBarTheme: NavigationBarThemeData( - backgroundColor: - isDark ? colorScheme.surfaceContainer : colorScheme.surface, - labelTextStyle: const WidgetStatePropertyAll( - TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: primaryColor, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - labelStyle: TextStyle( - color: primaryColor, - ), - hintStyle: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - textSelectionTheme: TextSelectionThemeData( - cursorColor: primaryColor, - ), - dropdownMenuTheme: DropdownMenuThemeData( - menuStyle: MenuStyle( - shape: WidgetStatePropertyAll( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(15), - ), - ), - ), - inputDecorationTheme: InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: primaryColor, - ), - ), - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: colorScheme.outlineVariant, - ), - borderRadius: const BorderRadius.all(Radius.circular(15)), - ), - labelStyle: TextStyle( - color: primaryColor, - ), - hintStyle: const TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - ), - ); -} diff --git a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart index e4dd355554..f5479ab86e 100644 --- a/mobile/lib/widgets/asset_viewer/motion_photo_button.dart +++ b/mobile/lib/widgets/asset_viewer/motion_photo_button.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; class MotionPhotoButton extends ConsumerWidget { diff --git a/mobile/lib/widgets/asset_viewer/video_position.dart b/mobile/lib/widgets/asset_viewer/video_position.dart index b1f70b8686..4d0e7aa17f 100644 --- a/mobile/lib/widgets/asset_viewer/video_position.dart +++ b/mobile/lib/widgets/asset_viewer/video_position.dart @@ -3,7 +3,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_controls_provider.dart'; import 'package:immich_mobile/providers/asset_viewer/video_player_value_provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/formatted_duration.dart'; diff --git a/mobile/lib/widgets/backup/error_chip.dart b/mobile/lib/widgets/backup/error_chip.dart index 4bbc040d4d..4df3e50f64 100644 --- a/mobile/lib/widgets/backup/error_chip.dart +++ b/mobile/lib/widgets/backup/error_chip.dart @@ -1,7 +1,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/error_chip_text.dart'; diff --git a/mobile/lib/widgets/backup/error_chip_text.dart b/mobile/lib/widgets/backup/error_chip_text.dart index 94148da176..540e136722 100644 --- a/mobile/lib/widgets/backup/error_chip_text.dart +++ b/mobile/lib/widgets/backup/error_chip_text.dart @@ -1,7 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; class BackupErrorChipText extends ConsumerWidget { diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index 68a2146bfb..65425f9e78 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -3,21 +3,22 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/locale_provider.dart'; import 'package:immich_mobile/providers/map/map_state.provider.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; +import 'package:immich_mobile/theme/theme_data.dart'; /// Overrides the theme below the widget tree to use the theme data based on the /// map settings instead of the one from the app settings -class MapThemeOveride extends StatefulHookConsumerWidget { +class MapThemeOverride extends StatefulHookConsumerWidget { final ThemeMode? themeMode; final Widget Function(AsyncValue style) mapBuilder; - const MapThemeOveride({required this.mapBuilder, this.themeMode, super.key}); + const MapThemeOverride({required this.mapBuilder, this.themeMode, super.key}); @override - ConsumerState createState() => _MapThemeOverideState(); + ConsumerState createState() => _MapThemeOverrideState(); } -class _MapThemeOverideState extends ConsumerState +class _MapThemeOverrideState extends ConsumerState with WidgetsBindingObserver { late ThemeMode _theme; bool _isDarkTheme = false; diff --git a/mobile/lib/widgets/map/map_thumbnail.dart b/mobile/lib/widgets/map/map_thumbnail.dart index d02c016791..b856f09787 100644 --- a/mobile/lib/widgets/map/map_thumbnail.dart +++ b/mobile/lib/widgets/map/map_thumbnail.dart @@ -62,7 +62,7 @@ class MapThumbnail extends HookConsumerWidget { } } - return MapThemeOveride( + return MapThemeOverride( themeMode: themeMode, mapBuilder: (style) => SizedBox( height: height, diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart index 1c7cd1f207..119407ccad 100644 --- a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -2,12 +2,14 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/constants/colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; +import 'package:immich_mobile/theme/color_scheme.dart'; +import 'package:immich_mobile/theme/dynamic_theme.dart'; class PrimaryColorSetting extends HookConsumerWidget { const PrimaryColorSetting({ @@ -124,7 +126,7 @@ class PrimaryColorSetting extends HookConsumerWidget { style: context.textTheme.titleLarge, ), ), - if (isDynamicThemeAvailable) + if (DynamicTheme.isAvailable) Container( padding: const EdgeInsets.symmetric(horizontal: 20), margin: const EdgeInsets.only(top: 10), @@ -153,16 +155,16 @@ class PrimaryColorSetting extends HookConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 20), child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, - children: ImmichColorPreset.values.map((themePreset) { - var theme = themePreset.getTheme(); + children: ImmichColorPreset.values.map((preset) { + final theme = preset.themeOfPreset; return GestureDetector( - onTap: () => onPrimaryColorChange(themePreset), + onTap: () => onPrimaryColorChange(preset), child: buildPrimaryColorTile( topColor: theme.light.primary, bottomColor: theme.dark.primary, tileSize: tileSize, - showSelector: currentPreset.value == themePreset && + showSelector: currentPreset.value == preset && !systemPrimaryColorSetting.value, ), ); diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 3e1f388e84..b9ba7aa7b7 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,12 +3,12 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/theme.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; -import 'package:immich_mobile/utils/immich_app_theme.dart'; class ThemeSetting extends HookConsumerWidget { const ThemeSetting({ diff --git a/mobile/test/modules/map/map_theme_override_test.dart b/mobile/test/modules/map/map_theme_override_test.dart index c21f9bf166..bd000c8715 100644 --- a/mobile/test/modules/map/map_theme_override_test.dart +++ b/mobile/test/modules/map/map_theme_override_test.dart @@ -35,7 +35,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -53,7 +53,7 @@ void main() { testWidgets("Return error when style is not fetched", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -73,7 +73,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -94,7 +94,7 @@ void main() { testWidgets("Return dark theme style when system is dark", (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -118,7 +118,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); @@ -142,7 +142,7 @@ void main() { (tester) async { AsyncValue? mapStyle; await tester.pumpConsumerWidget( - MapThemeOveride( + MapThemeOverride( mapBuilder: (AsyncValue style) { mapStyle = style; return const Text("Mock"); From e40c7c51ee0cec49a7d1cd33f714401e0308f97f Mon Sep 17 00:00:00 2001 From: Travis Menghini Date: Wed, 11 Dec 2024 10:31:11 -0600 Subject: [PATCH 15/25] feat(web): allow tags to be applied in bulk on search, personID, and memory-viewer pages (#14368) * Allow Tags to be applied in bulk on search page * Added Tags Action To PersonID Page * Fixed Formatting Issues * Added Tags Option to Memory-Viewer --- web/src/lib/components/memory-page/memory-viewer.svelte | 6 ++++++ .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 6 ++++++ .../search/[[photos=photos]]/[[assetId=id]]/+page.svelte | 6 ++++++ 3 files changed, 18 insertions(+) diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 72723670e6..65ef47c9ca 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -46,6 +46,8 @@ import { tweened } from 'svelte/motion'; import { derived as storeDerived } from 'svelte/store'; import { fade } from 'svelte/transition'; + import { preferences, user } from '$lib/stores/user.store'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; type MemoryIndex = { memoryIndex: number; @@ -221,6 +223,7 @@ $effect(() => { handlePromiseError(handleAction(galleryInView ? 'pause' : 'play')); }); + let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 48e194dda4..143a19dd5c 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -58,6 +58,8 @@ import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; + import { preferences, user } from '$lib/stores/user.store'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; interface Props { data: PageData; @@ -337,6 +339,7 @@ let isAllArchive = $derived([...$selectedAssets].every((asset) => asset.isArchived)); let isAllFavorite = $derived([...$selectedAssets].every((asset) => asset.isFavorite)); + let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); {#if viewMode === PersonPageViewMode.UNASSIGN_ASSETS} @@ -391,6 +394,9 @@ $assetStore.removeAssets(assetIds)} /> + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if} $assetStore.removeAssets(assetIds)} /> diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index c800dd7014..7372f05e77 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -44,6 +44,8 @@ import { t } from 'svelte-i18n'; import { onMount, tick } from 'svelte'; import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; + import { preferences, user } from '$lib/stores/user.store'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; const MAX_ASSET_COUNT = 5000; let { isViewing: showAssetViewer } = assetViewingStore; @@ -229,6 +231,7 @@ function getObjectKeys(obj: T): (keyof T)[] { return Object.keys(obj) as (keyof T)[]; } + let isAllUserOwned = $derived([...$selectedAssets].every((asset) => asset.ownerId === $user.id)); @@ -250,6 +253,9 @@ + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if}
From 3053d84e49e2e0a8f7f54c51e5d236d1cdc2a8a2 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 11 Dec 2024 12:23:20 -0500 Subject: [PATCH 16/25] fix(mobile): not being able to zoom into live photos (#14608) fix live photo zoom --- .../lib/pages/common/gallery_viewer.page.dart | 6 ++-- .../common/native_video_viewer.page.dart | 35 +++---------------- .../asset_grid/immich_asset_grid_view.dart | 2 ++ 3 files changed, 10 insertions(+), 33 deletions(-) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 2ea446ea71..5f77f28d8e 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -61,6 +61,7 @@ class GalleryViewerPage extends HookConsumerWidget { final localPosition = useRef(null); final currentIndex = useValueNotifier(initialIndex); final loadAsset = renderList.loadAsset; + final isPlayingMotionVideo = ref.watch(isPlayingMotionVideoProvider); Future precacheNextImage(int index) async { if (!context.mounted) { @@ -249,7 +250,6 @@ class GalleryViewerPage extends HookConsumerWidget { } PhotoViewGalleryPageOptions buildAsset(BuildContext context, int index) { - ref.read(isPlayingMotionVideoProvider.notifier).playing = false; var newAsset = loadAsset(index); final stackId = newAsset.stackId; if (stackId != null && currentIndex.value == index) { @@ -260,7 +260,7 @@ class GalleryViewerPage extends HookConsumerWidget { } } - if (newAsset.isImage && !newAsset.isMotionPhoto) { + if (newAsset.isImage && !isPlayingMotionVideo) { return buildImage(context, newAsset); } return buildVideo(context, newAsset); @@ -275,7 +275,7 @@ class GalleryViewerPage extends HookConsumerWidget { body: Stack( children: [ PhotoViewGallery.builder( - key: const ValueKey('gallery'), + key: ValueKey(isPlayingMotionVideo), scaleStateChangedCallback: (state) { final asset = ref.read(currentAssetProvider); if (asset == null) { diff --git a/mobile/lib/pages/common/native_video_viewer.page.dart b/mobile/lib/pages/common/native_video_viewer.page.dart index 536c7f6303..33acad0fdf 100644 --- a/mobile/lib/pages/common/native_video_viewer.page.dart +++ b/mobile/lib/pages/common/native_video_viewer.page.dart @@ -40,7 +40,6 @@ class NativeVideoViewerPage extends HookConsumerWidget { final controller = useState(null); final lastVideoPosition = useRef(-1); final isBuffering = useRef(false); - final showMotionVideo = useState(false); // When a video is opened through the timeline, `isCurrent` will immediately be true. // When swiping from video A to video B, `isCurrent` will initially be true for video A and false for video B. @@ -50,30 +49,10 @@ class NativeVideoViewerPage extends HookConsumerWidget { final isCurrent = currentAsset.value == asset; // Used to show the placeholder during hero animations for remote videos to avoid a stutter - final isVisible = - useState((Platform.isIOS && asset.isLocal) || asset.isMotionPhoto); + final isVisible = useState(Platform.isIOS && asset.isLocal); final log = Logger('NativeVideoViewerPage'); - ref.listen(isPlayingMotionVideoProvider, (_, value) async { - final videoController = controller.value; - if (!asset.isMotionPhoto || videoController == null || !context.mounted) { - return; - } - - showMotionVideo.value = value; - try { - if (value) { - await videoController.seekTo(0); - await videoController.play(); - } else { - await videoController.pause(); - } - } catch (error) { - log.severe('Error toggling motion video: $error'); - } - }); - Future createSource() async { if (!context.mounted) { return null; @@ -81,7 +60,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { try { final local = asset.local; - if (local != null && !asset.isMotionPhoto) { + if (local != null) { final file = await local.file; if (file == null) { throw Exception('No file found for the video'); @@ -204,9 +183,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback; try { - if (asset.isVideo || showMotionVideo.value) { - await videoController.play(); - } + await videoController.play(); await videoController.setVolume(0.9); } catch (error) { log.severe('Error playing video: $error'); @@ -268,8 +245,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { return; } - if (showMotionVideo.value && - videoController.playbackInfo?.status == PlaybackStatus.stopped && + if (videoController.playbackInfo?.status == PlaybackStatus.stopped && !ref .read(appSettingsServiceProvider) .getSetting(AppSettingsEnum.loopVideo)) { @@ -388,8 +364,7 @@ class NativeVideoViewerPage extends HookConsumerWidget { if (aspectRatio.value != null) Visibility.maintain( key: ValueKey(asset), - visible: - (asset.isVideo || showMotionVideo.value) && isVisible.value, + visible: isVisible.value, child: Center( key: ValueKey(asset), child: AspectRatio( diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 5670aa388f..c38e61a473 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -13,6 +13,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/current_asset.provider.dart'; +import 'package:immich_mobile/providers/asset_viewer/is_motion_video_playing.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; @@ -206,6 +207,7 @@ class ImmichAssetGridViewState extends ConsumerState { heroOffset: widget.heroOffset, onAssetTap: (asset) { ref.read(currentAssetProvider.notifier).set(asset); + ref.read(isPlayingMotionVideoProvider.notifier).playing = false; if (asset.isVideo) { ref.read(showControlsProvider.notifier).show = false; } From 71b48b11e697d5f7e029a294d8614f76416a455f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 11:24:13 -0600 Subject: [PATCH 17/25] chore(deps): update dependency pytest-cov to v6 (#13925) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/poetry.lock | 128 +++++++++++++++++++---------------- 1 file changed, 69 insertions(+), 59 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 229db1303b..bb7cd95149 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -575,63 +575,73 @@ test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"] [[package]] name = "coverage" -version = "7.4.0" +version = "7.6.4" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "coverage-7.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:36b0ea8ab20d6a7564e89cb6135920bc9188fb5f1f7152e94e8300b7b189441a"}, - {file = "coverage-7.4.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0676cd0ba581e514b7f726495ea75aba3eb20899d824636c6f59b0ed2f88c471"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0ca5c71a5a1765a0f8f88022c52b6b8be740e512980362f7fdbb03725a0d6b9"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7c97726520f784239f6c62506bc70e48d01ae71e9da128259d61ca5e9788516"}, - {file = "coverage-7.4.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:815ac2d0f3398a14286dc2cea223a6f338109f9ecf39a71160cd1628786bc6f5"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:80b5ee39b7f0131ebec7968baa9b2309eddb35b8403d1869e08f024efd883566"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:5b2ccb7548a0b65974860a78c9ffe1173cfb5877460e5a229238d985565574ae"}, - {file = "coverage-7.4.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:995ea5c48c4ebfd898eacb098164b3cc826ba273b3049e4a889658548e321b43"}, - {file = "coverage-7.4.0-cp310-cp310-win32.whl", hash = "sha256:79287fd95585ed36e83182794a57a46aeae0b64ca53929d1176db56aacc83451"}, - {file = "coverage-7.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:5b14b4f8760006bfdb6e08667af7bc2d8d9bfdb648351915315ea17645347137"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04387a4a6ecb330c1878907ce0dc04078ea72a869263e53c72a1ba5bbdf380ca"}, - {file = "coverage-7.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea81d8f9691bb53f4fb4db603203029643caffc82bf998ab5b59ca05560f4c06"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74775198b702868ec2d058cb92720a3c5a9177296f75bd97317c787daf711505"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:76f03940f9973bfaee8cfba70ac991825611b9aac047e5c80d499a44079ec0bc"}, - {file = "coverage-7.4.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:485e9f897cf4856a65a57c7f6ea3dc0d4e6c076c87311d4bc003f82cfe199d25"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6ae8c9d301207e6856865867d762a4b6fd379c714fcc0607a84b92ee63feff70"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bf477c355274a72435ceb140dc42de0dc1e1e0bf6e97195be30487d8eaaf1a09"}, - {file = "coverage-7.4.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:83c2dda2666fe32332f8e87481eed056c8b4d163fe18ecc690b02802d36a4d26"}, - {file = "coverage-7.4.0-cp311-cp311-win32.whl", hash = "sha256:697d1317e5290a313ef0d369650cfee1a114abb6021fa239ca12b4849ebbd614"}, - {file = "coverage-7.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:26776ff6c711d9d835557ee453082025d871e30b3fd6c27fcef14733f67f0590"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:13eaf476ec3e883fe3e5fe3707caeb88268a06284484a3daf8250259ef1ba143"}, - {file = "coverage-7.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846f52f46e212affb5bcf131c952fb4075b55aae6b61adc9856222df89cbe3e2"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:26f66da8695719ccf90e794ed567a1549bb2644a706b41e9f6eae6816b398c4a"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:164fdcc3246c69a6526a59b744b62e303039a81e42cfbbdc171c91a8cc2f9446"}, - {file = "coverage-7.4.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:316543f71025a6565677d84bc4df2114e9b6a615aa39fb165d697dba06a54af9"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bb1de682da0b824411e00a0d4da5a784ec6496b6850fdf8c865c1d68c0e318dd"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:0e8d06778e8fbffccfe96331a3946237f87b1e1d359d7fbe8b06b96c95a5407a"}, - {file = "coverage-7.4.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a56de34db7b7ff77056a37aedded01b2b98b508227d2d0979d373a9b5d353daa"}, - {file = "coverage-7.4.0-cp312-cp312-win32.whl", hash = "sha256:51456e6fa099a8d9d91497202d9563a320513fcf59f33991b0661a4a6f2ad450"}, - {file = "coverage-7.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:cd3c1e4cb2ff0083758f09be0f77402e1bdf704adb7f89108007300a6da587d0"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d1bf53c4c8de58d22e0e956a79a5b37f754ed1ffdbf1a260d9dcfa2d8a325e"}, - {file = "coverage-7.4.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:109f5985182b6b81fe33323ab4707011875198c41964f014579cf82cebf2bb85"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cc9d4bc55de8003663ec94c2f215d12d42ceea128da8f0f4036235a119c88ac"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc6d65b21c219ec2072c1293c505cf36e4e913a3f936d80028993dd73c7906b1"}, - {file = "coverage-7.4.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a10a4920def78bbfff4eff8a05c51be03e42f1c3735be42d851f199144897ba"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b8e99f06160602bc64da35158bb76c73522a4010f0649be44a4e167ff8555952"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7d360587e64d006402b7116623cebf9d48893329ef035278969fa3bbf75b697e"}, - {file = "coverage-7.4.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:29f3abe810930311c0b5d1a7140f6395369c3db1be68345638c33eec07535105"}, - {file = "coverage-7.4.0-cp38-cp38-win32.whl", hash = "sha256:5040148f4ec43644702e7b16ca864c5314ccb8ee0751ef617d49aa0e2d6bf4f2"}, - {file = "coverage-7.4.0-cp38-cp38-win_amd64.whl", hash = "sha256:9864463c1c2f9cb3b5db2cf1ff475eed2f0b4285c2aaf4d357b69959941aa555"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:936d38794044b26c99d3dd004d8af0035ac535b92090f7f2bb5aa9c8e2f5cd42"}, - {file = "coverage-7.4.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:799c8f873794a08cdf216aa5d0531c6a3747793b70c53f70e98259720a6fe2d7"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7defbb9737274023e2d7af02cac77043c86ce88a907c58f42b580a97d5bcca9"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a1526d265743fb49363974b7aa8d5899ff64ee07df47dd8d3e37dcc0818f09ed"}, - {file = "coverage-7.4.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bf635a52fc1ea401baf88843ae8708591aa4adff875e5c23220de43b1ccf575c"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:756ded44f47f330666843b5781be126ab57bb57c22adbb07d83f6b519783b870"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0eb3c2f32dabe3a4aaf6441dde94f35687224dfd7eb2a7f47f3fd9428e421058"}, - {file = "coverage-7.4.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bfd5db349d15c08311702611f3dccbef4b4e2ec148fcc636cf8739519b4a5c0f"}, - {file = "coverage-7.4.0-cp39-cp39-win32.whl", hash = "sha256:53d7d9158ee03956e0eadac38dfa1ec8068431ef8058fe6447043db1fb40d932"}, - {file = "coverage-7.4.0-cp39-cp39-win_amd64.whl", hash = "sha256:cfd2a8b6b0d8e66e944d47cdec2f47c48fef2ba2f2dff5a9a75757f64172857e"}, - {file = "coverage-7.4.0-pp38.pp39.pp310-none-any.whl", hash = "sha256:c530833afc4707fe48524a44844493f36d8727f04dcce91fb978c414a8556cc6"}, - {file = "coverage-7.4.0.tar.gz", hash = "sha256:707c0f58cb1712b8809ece32b68996ee1e609f71bd14615bd8f87a1293cb610e"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f8ae553cba74085db385d489c7a792ad66f7f9ba2ee85bfa508aeb84cf0ba07"}, + {file = "coverage-7.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8165b796df0bd42e10527a3f493c592ba494f16ef3c8b531288e3d0d72c1f6f0"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7c8b95bf47db6d19096a5e052ffca0a05f335bc63cef281a6e8fe864d450a72"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ed9281d1b52628e81393f5eaee24a45cbd64965f41857559c2b7ff19385df51"}, + {file = "coverage-7.6.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0809082ee480bb8f7416507538243c8863ac74fd8a5d2485c46f0f7499f2b491"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d541423cdd416b78626b55f123412fcf979d22a2c39fce251b350de38c15c15b"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58809e238a8a12a625c70450b48e8767cff9eb67c62e6154a642b21ddf79baea"}, + {file = "coverage-7.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c9b8e184898ed014884ca84c70562b4a82cbc63b044d366fedc68bc2b2f3394a"}, + {file = "coverage-7.6.4-cp310-cp310-win32.whl", hash = "sha256:6bd818b7ea14bc6e1f06e241e8234508b21edf1b242d49831831a9450e2f35fa"}, + {file = "coverage-7.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:06babbb8f4e74b063dbaeb74ad68dfce9186c595a15f11f5d5683f748fa1d172"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:73d2b73584446e66ee633eaad1a56aad577c077f46c35ca3283cd687b7715b0b"}, + {file = "coverage-7.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:51b44306032045b383a7a8a2c13878de375117946d68dcb54308111f39775a25"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3fb02fe73bed561fa12d279a417b432e5b50fe03e8d663d61b3d5990f29546"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed8fe9189d2beb6edc14d3ad19800626e1d9f2d975e436f84e19efb7fa19469b"}, + {file = "coverage-7.6.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b369ead6527d025a0fe7bd3864e46dbee3aa8f652d48df6174f8d0bac9e26e0e"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ade3ca1e5f0ff46b678b66201f7ff477e8fa11fb537f3b55c3f0568fbfe6e718"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:27fb4a050aaf18772db513091c9c13f6cb94ed40eacdef8dad8411d92d9992db"}, + {file = "coverage-7.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4f704f0998911abf728a7783799444fcbbe8261c4a6c166f667937ae6a8aa522"}, + {file = "coverage-7.6.4-cp311-cp311-win32.whl", hash = "sha256:29155cd511ee058e260db648b6182c419422a0d2e9a4fa44501898cf918866cf"}, + {file = "coverage-7.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:8902dd6a30173d4ef09954bfcb24b5d7b5190cf14a43170e386979651e09ba19"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:12394842a3a8affa3ba62b0d4ab7e9e210c5e366fbac3e8b2a68636fb19892c2"}, + {file = "coverage-7.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b6b4c83d8e8ea79f27ab80778c19bc037759aea298da4b56621f4474ffeb117"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d5b8007f81b88696d06f7df0cb9af0d3b835fe0c8dbf489bad70b45f0e45613"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b57b768feb866f44eeed9f46975f3d6406380275c5ddfe22f531a2bf187eda27"}, + {file = "coverage-7.6.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5915fcdec0e54ee229926868e9b08586376cae1f5faa9bbaf8faf3561b393d52"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0b58c672d14f16ed92a48db984612f5ce3836ae7d72cdd161001cc54512571f2"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:2fdef0d83a2d08d69b1f2210a93c416d54e14d9eb398f6ab2f0a209433db19e1"}, + {file = "coverage-7.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cf717ee42012be8c0cb205dbbf18ffa9003c4cbf4ad078db47b95e10748eec5"}, + {file = "coverage-7.6.4-cp312-cp312-win32.whl", hash = "sha256:7bb92c539a624cf86296dd0c68cd5cc286c9eef2d0c3b8b192b604ce9de20a17"}, + {file = "coverage-7.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:1032e178b76a4e2b5b32e19d0fd0abbce4b58e77a1ca695820d10e491fa32b08"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:023bf8ee3ec6d35af9c1c6ccc1d18fa69afa1cb29eaac57cb064dbb262a517f9"}, + {file = "coverage-7.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0ac3d42cb51c4b12df9c5f0dd2f13a4f24f01943627120ec4d293c9181219ba"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8fe4984b431f8621ca53d9380901f62bfb54ff759a1348cd140490ada7b693c"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5fbd612f8a091954a0c8dd4c0b571b973487277d26476f8480bfa4b2a65b5d06"}, + {file = "coverage-7.6.4-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dacbc52de979f2823a819571f2e3a350a7e36b8cb7484cdb1e289bceaf35305f"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dab4d16dfef34b185032580e2f2f89253d302facba093d5fa9dbe04f569c4f4b"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:862264b12ebb65ad8d863d51f17758b1684560b66ab02770d4f0baf2ff75da21"}, + {file = "coverage-7.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5beb1ee382ad32afe424097de57134175fea3faf847b9af002cc7895be4e2a5a"}, + {file = "coverage-7.6.4-cp313-cp313-win32.whl", hash = "sha256:bf20494da9653f6410213424f5f8ad0ed885e01f7e8e59811f572bdb20b8972e"}, + {file = "coverage-7.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:182e6cd5c040cec0a1c8d415a87b67ed01193ed9ad458ee427741c7d8513d963"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a181e99301a0ae128493a24cfe5cfb5b488c4e0bf2f8702091473d033494d04f"}, + {file = "coverage-7.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:df57bdbeffe694e7842092c5e2e0bc80fff7f43379d465f932ef36f027179806"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bcd1069e710600e8e4cf27f65c90c7843fa8edfb4520fb0ccb88894cad08b11"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:99b41d18e6b2a48ba949418db48159d7a2e81c5cc290fc934b7d2380515bd0e3"}, + {file = "coverage-7.6.4-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a6b1e54712ba3474f34b7ef7a41e65bd9037ad47916ccb1cc78769bae324c01a"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:53d202fd109416ce011578f321460795abfe10bb901b883cafd9b3ef851bacfc"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:c48167910a8f644671de9f2083a23630fbf7a1cb70ce939440cd3328e0919f70"}, + {file = "coverage-7.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cc8ff50b50ce532de2fa7a7daae9dd12f0a699bfcd47f20945364e5c31799fef"}, + {file = "coverage-7.6.4-cp313-cp313t-win32.whl", hash = "sha256:b8d3a03d9bfcaf5b0141d07a88456bb6a4c3ce55c080712fec8418ef3610230e"}, + {file = "coverage-7.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:f3ddf056d3ebcf6ce47bdaf56142af51bb7fad09e4af310241e9db7a3a8022e1"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9cb7fa111d21a6b55cbf633039f7bc2749e74932e3aa7cb7333f675a58a58bf3"}, + {file = "coverage-7.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:11a223a14e91a4693d2d0755c7a043db43d96a7450b4f356d506c2562c48642c"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a413a096c4cbac202433c850ee43fa326d2e871b24554da8327b01632673a076"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00a1d69c112ff5149cabe60d2e2ee948752c975d95f1e1096742e6077affd376"}, + {file = "coverage-7.6.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f76846299ba5c54d12c91d776d9605ae33f8ae2b9d1d3c3703cf2db1a67f2c0"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fe439416eb6380de434886b00c859304338f8b19f6f54811984f3420a2e03858"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:0294ca37f1ba500667b1aef631e48d875ced93ad5e06fa665a3295bdd1d95111"}, + {file = "coverage-7.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6f01ba56b1c0e9d149f9ac85a2f999724895229eb36bd997b61e62999e9b0901"}, + {file = "coverage-7.6.4-cp39-cp39-win32.whl", hash = "sha256:bc66f0bf1d7730a17430a50163bb264ba9ded56739112368ba985ddaa9c3bd09"}, + {file = "coverage-7.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:c481b47f6b5845064c65a7bc78bc0860e635a9b055af0df46fdf1c58cebf8e8f"}, + {file = "coverage-7.6.4-pp39.pp310-none-any.whl", hash = "sha256:3c65d37f3a9ebb703e710befdc489a38683a5b152242664b973a7b7b22348a4e"}, + {file = "coverage-7.6.4.tar.gz", hash = "sha256:29fc0f17b1d3fea332f8001d4558f8214af7f1d87a345f3a133c901d60347c73"}, ] [package.dependencies] @@ -2683,17 +2693,17 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest-cov-6.0.0.tar.gz", hash = "sha256:fde0b595ca248bb8e2d76f020b465f3b107c9632e6a1d1705f17834c89dcadc0"}, + {file = "pytest_cov-6.0.0-py3-none-any.whl", hash = "sha256:eee6f1b9e61008bd34975a4d5bab25801eb31898b032dd55addc93e96fcaaa35"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} +coverage = {version = ">=7.5", extras = ["toml"]} pytest = ">=4.6" [package.extras] From 0c037536422ecae950eb208b6612f558785d6e8b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 11 Dec 2024 14:51:56 -0600 Subject: [PATCH 18/25] fix(server): fix getByDayOfYear query (#14655) * fix(server): fix getByDayOfYear query * generate sql --- server/src/queries/asset.repository.sql | 15 ++++++++++++++- server/src/repositories/asset.repository.ts | 2 +- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index f4b1b2fea1..4694cd20fc 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -71,7 +71,20 @@ FROM INNER JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" WHERE ( - "files"."type" = $1 + "entity"."ownerId" IN ($1) + AND "entity"."isVisible" = true + AND "entity"."isArchived" = false + AND EXTRACT( + DAY + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $2 + AND EXTRACT( + MONTH + FROM + "entity"."localDateTime" AT TIME ZONE 'UTC' + ) = $3 + AND "files"."type" = $4 AND EXTRACT( YEAR FROM diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index b3066a37bc..33d1e2457e 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -92,7 +92,7 @@ export class AssetRepository implements IAssetRepository { ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') .innerJoinAndSelect('entity.files', 'files') - .where('files.type = :type', { type: AssetFileType.THUMBNAIL }) + .andWhere('files.type = :type', { type: AssetFileType.THUMBNAIL }) .andWhere( `EXTRACT(YEAR FROM CURRENT_DATE AT TIME ZONE 'UTC') - EXTRACT(YEAR FROM entity.localDateTime AT TIME ZONE 'UTC') > 0`, ) From c52f1bae81aae8d0c86b2cf3f9384eb92e39ab36 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:15:03 +0000 Subject: [PATCH 19/25] chore: version v1.122.3 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | Bin 32447 -> 32447 bytes mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 30 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 03b8061efb..137565a22d 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.35", + "version": "2.2.36", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.35", + "version": "2.2.36", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index b58825b2b9..9b3a417385 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.35", + "version": "2.2.36", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 7ba9125c03..960e00caa9 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.122.3", + "url": "https://v1.122.3.archive.immich.app" + }, { "label": "v1.122.2", "url": "https://v1.122.2.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 011e6b2fdd..ec20557358 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.122.2", + "version": "1.122.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.35", + "version": "2.2.36", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index a47b4bbae9..12316e910c 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.122.2", + "version": "1.122.3", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index cff2a432b3..0f8186c41f 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.122.2" +version = "1.122.3" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 4fc00ce6c7..f3c30770e1 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 170, - "android.injected.version.name" => "1.122.2", + "android.injected.version.code" => 171, + "android.injected.version.name" => "1.122.3", } ) upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index d7604b4283..0574a5e78f 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.122.2" + version_number: "1.122.3" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index d9e10bc316bf4dadd00e5cb55b9f8458e7eb5f68..6bacbc74233b3ebf6320fac7dbb020c4edfdb2b3 100644 GIT binary patch delta 14 Wcmdo0mvR4J#tCy7jW^CytOEczvj)ll delta 14 Wcmdo0mvR4J#tCy7jW*6xtOEczt_H~f diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 39621a953e..c1cf25d008 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.122.2+170 +version: 1.122.3+171 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 706d6a28ee..3afda881cd 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7436,7 +7436,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.122.2", + "version": "1.122.3", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index c0d5d329d1..fa7d83feb5 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 22f480e68d..7a812e87fb 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 68f44d7bed..7770f0c578 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.122.2 + * 1.122.3 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 4ad00c90f7..6d898a9735 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.122.2", + "version": "1.122.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index a7005deafa..385cbccd3d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.122.2", + "version": "1.122.3", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index cab21cd4dc..f3c1f4b12e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.122.2", + "version": "1.122.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 2bd429cc1a..c35344c12f 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.122.2", + "version": "1.122.3", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 11be85feb3016fd9007e8390034e0d24c1a4480b Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:48:50 +0100 Subject: [PATCH 20/25] fix(web): live photo link action (#14668) --- web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 7e233fcd17..b76143142e 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -49,7 +49,7 @@ const isLivePhotoCandidate = selection.length === 2 && selection.some((asset) => asset.type === AssetTypeEnum.Image) && - selection.some((asset) => asset.type === AssetTypeEnum.Image); + selection.some((asset) => asset.type === AssetTypeEnum.Video); isLinkActionAvailable = isAllOwned && (isLivePhoto || isLivePhotoCandidate); }); From 58d63d9f1ce3a27a2562ca9f427393d7039e8c68 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:10:51 -0600 Subject: [PATCH 21/25] chore(deps): update grafana/grafana docker tag to v11.4.0 (#14633) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index d58b20ef76..8521390079 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -103,7 +103,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.3.1-ubuntu@sha256:7ca40d20250157abd70a907a93617a70c9b0ad9d7e59e8e6b5c8140781350d6a + image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c volumes: - grafana-data:/var/lib/grafana From 59d6af54c7a22a66c2212bed85dc8f05a7150b11 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:12:44 -0600 Subject: [PATCH 22/25] chore(deps): update node.js to v22.12.0 (#14650) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/.nvmrc | 2 +- cli/package.json | 2 +- docs/.nvmrc | 2 +- docs/package.json | 2 +- e2e/.nvmrc | 2 +- e2e/package.json | 2 +- open-api/typescript-sdk/.nvmrc | 2 +- open-api/typescript-sdk/package.json | 2 +- server/.nvmrc | 2 +- server/package.json | 2 +- web/.nvmrc | 2 +- web/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/.nvmrc b/cli/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/cli/package.json b/cli/package.json index 9b3a417385..86f54cc342 100644 --- a/cli/package.json +++ b/cli/package.json @@ -67,6 +67,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } diff --git a/docs/.nvmrc b/docs/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/docs/package.json b/docs/package.json index 6b0595a7b0..498a0c4d7c 100644 --- a/docs/package.json +++ b/docs/package.json @@ -56,6 +56,6 @@ "node": ">=20" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/e2e/package.json b/e2e/package.json index 12316e910c..9e9ce3b362 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -53,6 +53,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 7a812e87fb..c05d21b696 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } diff --git a/server/.nvmrc b/server/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/server/package.json b/server/package.json index 385cbccd3d..f57c0e557b 100644 --- a/server/package.json +++ b/server/package.json @@ -139,6 +139,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } diff --git a/web/.nvmrc b/web/.nvmrc index 7af24b7ddb..1d9b7831ba 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -22.11.0 +22.12.0 diff --git a/web/package.json b/web/package.json index c35344c12f..84158674a8 100644 --- a/web/package.json +++ b/web/package.json @@ -87,6 +87,6 @@ "thumbhash": "^0.1.1" }, "volta": { - "node": "22.11.0" + "node": "22.12.0" } } From 6abe696d0bee48f82dd5d33416e1db414f212749 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 12 Dec 2024 17:13:42 +0100 Subject: [PATCH 23/25] fix(web): allow minimizing upload panel (#14663) --- .../components/shared-components/upload-panel.svelte | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/web/src/lib/components/shared-components/upload-panel.svelte b/web/src/lib/components/shared-components/upload-panel.svelte index 7dd6d25596..2381b5a423 100644 --- a/web/src/lib/components/shared-components/upload-panel.svelte +++ b/web/src/lib/components/shared-components/upload-panel.svelte @@ -17,19 +17,9 @@ let { stats, isDismissible, isUploading, remainingUploads } = uploadAssetsStore; - const autoHide = () => { - if (!$isUploading && showDetail) { - showDetail = false; - } - - if ($isUploading && !showDetail) { - showDetail = true; - } - }; - $effect(() => { if ($isUploading) { - autoHide(); + showDetail = true; } }); From 40a0bf6ad57a4524ad28de28795769218efa66ec Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:14:34 -0600 Subject: [PATCH 24/25] chore(deps): update terraform cloudflare to v4.48.0 (#14669) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../docs-release/.terraform.lock.hcl | 60 +++++++++---------- .../modules/cloudflare/docs-release/config.tf | 2 +- .../cloudflare/docs/.terraform.lock.hcl | 60 +++++++++---------- deployment/modules/cloudflare/docs/config.tf | 2 +- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index 160c4f7ba5..00222921f1 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.46.0" - constraints = "4.46.0" + version = "4.48.0" + constraints = "4.48.0" hashes = [ - "h1:3U4N3bbMacXTAdyaEwT305kETMETh1jZmGApmN6gdyE=", - "h1:3fhZhGNgtS9ugcZ2CIH6kk8LzN6yPxqOdkDUZqkP3+w=", - "h1:JWluJxBRSr8GVUhWVv83xse9SmbpwCLctCDddMXUnVk=", - "h1:KDHwakGt+3iBKXaoALCCAolPaJgpEHbkh3BfjnpuqoM=", - "h1:QFFZshAvwr9L5TQmsNQC6/sDqokk5pjbP8Ae4BQqMLQ=", - "h1:Qdi+vXwzDNii7ytSaOQtnlqhjZ3ZlRoUkFoi6CD2COI=", - "h1:TPcJXcVb/+C91hUuu8CEn98QUoNgLtnHfd4sgAOV+5k=", - "h1:WDy5wiNroXaCnw+r8rJnCP+J1RVsm2Qu3AOZ/iV4lLo=", - "h1:hMuL+dwHj3JbePqYcDrn/ZQN9R0WzeJX0AIDJ02Iteo=", - "h1:hQKCaUEARzJKbFt1CePP06E/+CiHWe/H6lc1AwK7y6w=", - "h1:l4DQ3WXmSzR/GBel3m2CRKWtaziVjBoxvUgL63t1GK0=", - "h1:nN9uVSLyrb/DjfZl6rPtCq5j0TX+6WypzNDexdzCQ08=", - "h1:rAX7njl6lKT9XIKMk6pLjVi7u/42wafRolWWgMHMkI0=", - "h1:t2IQYNu8YNykqYlEB+TTX+XpUd5z2flwGw8km9UgbnQ=", - "zh:2ee426ef3389022db0026792fdc4f2980dcf2600e31adf5a31b4bddfa8d68343", - "zh:2f993edb23df55dc1c18150fa187d80aa7d87e6439698ee34b6a6aad23ac2dd7", - "zh:3d6601333975e55979b1b454e50ff9a482ce4e0269dd6c72a50202163a8f4463", - "zh:4e5f48dce22f7a6d618018d65d1d443bb718defa23f514d5c6385860541fbe79", - "zh:5ebf5aea960fc30de381ffd6db20876d249673cf938fe67f1dfb6b9caa1db418", - "zh:80ed3fb901141f53b4b56ddb7eea5f2e0c0830d501387539d2c2b8e0cc7e587a", + "h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=", + "h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=", + "h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=", + "h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=", + "h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=", + "h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=", + "h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=", + "h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=", + "h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=", + "h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=", + "h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=", + "h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=", + "h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=", + "h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=", + "zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c", + "zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997", + "zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b", + "zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb", + "zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153", + "zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8", + "zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f", + "zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04", + "zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937", + "zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:9aeae8b3be4a577ced46987fd9159262c5b4c54a510f66592fbcdb40fef55b10", - "zh:a0479ef2d308c4a7894f1fe77467cd07e04c7b40d281088f4f204af1bdf94ac6", - "zh:a2bdc0c25130665af0b9559942b9813a1ba4889513e7185d4abc9c02e9bb99bd", - "zh:b10be9755fe80395ced6f0bbda38b8c8681714cf1eca1d895be239c75c2ffc2a", - "zh:ba3d55e722d9f48646574ce7c448f0084fe21fa884b5f8b6d6146a82a99c4baa", - "zh:ec1fd0ecaedc787a77d5342b51ae8dea8362a67f1e19123f6521a0e8e012d9e8", - "zh:ed49590e69faef14550179f965b4451b31415b8f6be6d33427ad48f65c76b6cf", - "zh:f4baa3a2dac719ad20dcfa525bc3f737ad95650b8d0de0c648dc9a87f993b2c3", + "zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c", + "zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532", + "zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f", + "zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index f06c083bb0..c5397ea410 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.46.0" + version = "4.48.0" } } } diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index 160c4f7ba5..00222921f1 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.46.0" - constraints = "4.46.0" + version = "4.48.0" + constraints = "4.48.0" hashes = [ - "h1:3U4N3bbMacXTAdyaEwT305kETMETh1jZmGApmN6gdyE=", - "h1:3fhZhGNgtS9ugcZ2CIH6kk8LzN6yPxqOdkDUZqkP3+w=", - "h1:JWluJxBRSr8GVUhWVv83xse9SmbpwCLctCDddMXUnVk=", - "h1:KDHwakGt+3iBKXaoALCCAolPaJgpEHbkh3BfjnpuqoM=", - "h1:QFFZshAvwr9L5TQmsNQC6/sDqokk5pjbP8Ae4BQqMLQ=", - "h1:Qdi+vXwzDNii7ytSaOQtnlqhjZ3ZlRoUkFoi6CD2COI=", - "h1:TPcJXcVb/+C91hUuu8CEn98QUoNgLtnHfd4sgAOV+5k=", - "h1:WDy5wiNroXaCnw+r8rJnCP+J1RVsm2Qu3AOZ/iV4lLo=", - "h1:hMuL+dwHj3JbePqYcDrn/ZQN9R0WzeJX0AIDJ02Iteo=", - "h1:hQKCaUEARzJKbFt1CePP06E/+CiHWe/H6lc1AwK7y6w=", - "h1:l4DQ3WXmSzR/GBel3m2CRKWtaziVjBoxvUgL63t1GK0=", - "h1:nN9uVSLyrb/DjfZl6rPtCq5j0TX+6WypzNDexdzCQ08=", - "h1:rAX7njl6lKT9XIKMk6pLjVi7u/42wafRolWWgMHMkI0=", - "h1:t2IQYNu8YNykqYlEB+TTX+XpUd5z2flwGw8km9UgbnQ=", - "zh:2ee426ef3389022db0026792fdc4f2980dcf2600e31adf5a31b4bddfa8d68343", - "zh:2f993edb23df55dc1c18150fa187d80aa7d87e6439698ee34b6a6aad23ac2dd7", - "zh:3d6601333975e55979b1b454e50ff9a482ce4e0269dd6c72a50202163a8f4463", - "zh:4e5f48dce22f7a6d618018d65d1d443bb718defa23f514d5c6385860541fbe79", - "zh:5ebf5aea960fc30de381ffd6db20876d249673cf938fe67f1dfb6b9caa1db418", - "zh:80ed3fb901141f53b4b56ddb7eea5f2e0c0830d501387539d2c2b8e0cc7e587a", + "h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=", + "h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=", + "h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=", + "h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=", + "h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=", + "h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=", + "h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=", + "h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=", + "h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=", + "h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=", + "h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=", + "h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=", + "h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=", + "h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=", + "zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c", + "zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997", + "zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b", + "zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb", + "zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153", + "zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8", + "zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f", + "zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04", + "zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937", + "zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:9aeae8b3be4a577ced46987fd9159262c5b4c54a510f66592fbcdb40fef55b10", - "zh:a0479ef2d308c4a7894f1fe77467cd07e04c7b40d281088f4f204af1bdf94ac6", - "zh:a2bdc0c25130665af0b9559942b9813a1ba4889513e7185d4abc9c02e9bb99bd", - "zh:b10be9755fe80395ced6f0bbda38b8c8681714cf1eca1d895be239c75c2ffc2a", - "zh:ba3d55e722d9f48646574ce7c448f0084fe21fa884b5f8b6d6146a82a99c4baa", - "zh:ec1fd0ecaedc787a77d5342b51ae8dea8362a67f1e19123f6521a0e8e012d9e8", - "zh:ed49590e69faef14550179f965b4451b31415b8f6be6d33427ad48f65c76b6cf", - "zh:f4baa3a2dac719ad20dcfa525bc3f737ad95650b8d0de0c648dc9a87f993b2c3", + "zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c", + "zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532", + "zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f", + "zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index f06c083bb0..c5397ea410 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.46.0" + version = "4.48.0" } } } From 39732f3371e8a92e46c22b332b7c73013510d5f2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 12 Dec 2024 16:28:27 +0000 Subject: [PATCH 25/25] chore(deps): update base-image to v20241210 (major) (#14670) chore(deps): update base-image to v20241210 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 37b80ff1ee..9b510b72cc 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20241119@sha256:fef1bead6a594ebd6fa54712c3dc4db050173657738db0c21bb91b00f8b56320 AS dev +FROM ghcr.io/immich-app/base-server-dev:20241210@sha256:35c28404b508fc0741fffb39a9de17e3a6acdff0b623bb7b6cdf4a427462a0e2 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -42,7 +42,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20241119@sha256:0ab6c3d0d41924fba45f92c383bcf405abda338602d1140d151963bbbb088759 +FROM ghcr.io/immich-app/base-server-prod:20241210@sha256:076d002070385bc6dc7454ef9419f44341c074bcfc49be5deddbdb4108ae0060 WORKDIR /usr/src/app ENV NODE_ENV=production \