From 6d91c23f65f405642ed9e699127fc7026afc87c7 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Tue, 27 Feb 2024 20:14:58 +0000 Subject: [PATCH 01/44] Version v1.96.0 --- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/axios-client/api.ts | 2 +- open-api/typescript-sdk/axios-client/base.ts | 2 +- open-api/typescript-sdk/axios-client/common.ts | 2 +- open-api/typescript-sdk/axios-client/configuration.ts | 2 +- open-api/typescript-sdk/axios-client/index.ts | 2 +- open-api/typescript-sdk/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 4 ++-- web/package.json | 2 +- 16 files changed, 19 insertions(+), 19 deletions(-) diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index fec5c7213..3c8c7006e 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.95.1" +version = "1.96.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index fc6399051..9b7587587 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" => 123, - "android.injected.version.name" => "1.95.1", + "android.injected.version.code" => 124, + "android.injected.version.name" => "1.96.0", } ) 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 d5a42ad48..939689c68 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Beta" lane :beta do increment_version_number( - version_number: "1.95.1" + version_number: "1.96.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 41e65ee8b..eb8bfb4cc 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.95.1 +- API version: 1.96.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen ## Requirements diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 50d170904..d9ecc90c4 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: "none" -version: 1.95.1+123 +version: 1.96.0+124 isar_version: &isar_version 3.1.0+1 environment: diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 8fec89327..44772e14a 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6458,7 +6458,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.95.1", + "version": "1.96.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index ad36bb493..154fc9969 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.1 + * The version of the OpenAPI document: 1.96.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/base.ts b/open-api/typescript-sdk/axios-client/base.ts index d35330945..bc29eda02 100644 --- a/open-api/typescript-sdk/axios-client/base.ts +++ b/open-api/typescript-sdk/axios-client/base.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.1 + * The version of the OpenAPI document: 1.96.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/common.ts b/open-api/typescript-sdk/axios-client/common.ts index 120ebca55..4fb512c0f 100644 --- a/open-api/typescript-sdk/axios-client/common.ts +++ b/open-api/typescript-sdk/axios-client/common.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.1 + * The version of the OpenAPI document: 1.96.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/configuration.ts b/open-api/typescript-sdk/axios-client/configuration.ts index cd67c859c..6f2bacbbc 100644 --- a/open-api/typescript-sdk/axios-client/configuration.ts +++ b/open-api/typescript-sdk/axios-client/configuration.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.1 + * The version of the OpenAPI document: 1.96.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/axios-client/index.ts b/open-api/typescript-sdk/axios-client/index.ts index 0918c8124..cf9819f95 100644 --- a/open-api/typescript-sdk/axios-client/index.ts +++ b/open-api/typescript-sdk/axios-client/index.ts @@ -4,7 +4,7 @@ * Immich * Immich API * - * The version of the OpenAPI document: 1.95.1 + * The version of the OpenAPI document: 1.96.0 * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index d023f8ef0..5e9752127 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.95.1 + * 1.96.0 * 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 ceac96222..b9d28a915 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.95.1", + "version": "1.96.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.95.1", + "version": "1.96.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@babel/runtime": "^7.22.11", diff --git a/server/package.json b/server/package.json index e7d84dc5a..29ce96464 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.95.1", + "version": "1.96.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 77a875e51..3b748e597 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.1.1", + "version": "1.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.1.1", + "version": "1.2.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@immich/sdk": "file:../open-api/typescript-sdk", diff --git a/web/package.json b/web/package.json index 51f07dded..b7d400e86 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.1.1", + "version": "1.2.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 0d2a656aa13284cd3006c4994e6c09afd70d28b2 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Tue, 27 Feb 2024 22:43:12 +0100 Subject: [PATCH 02/44] chore: add agpl milestone (#7479) add agpl milestone --- docs/src/pages/milestones.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/pages/milestones.tsx b/docs/src/pages/milestones.tsx index 1670b9fc8..e672e0326 100644 --- a/docs/src/pages/milestones.tsx +++ b/docs/src/pages/milestones.tsx @@ -50,12 +50,22 @@ import { mdiVectorCombine, mdiVideo, mdiWeb, + mdiScaleBalance, } from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import Timeline, { DateType, Item } from '../components/timeline'; const items: Item[] = [ + { + icon: mdiScaleBalance, + description: 'Immich switches to AGPLv3 license', + title: 'AGPL License', + release: 'v1.95.0', + tag: 'v1.95.0', + date: new Date(2024, 1, 20), + dateType: DateType.RELEASE, + }, { icon: mdiEyeRefreshOutline, description: 'Automatically import files in external libraries when the operating system detects changes.', From f0ea99cea94294bf91833423be13f2fc4201a2b0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 22:45:29 +0100 Subject: [PATCH 03/44] chore(deps): update dependency @types/node to v20.11.20 (#7474) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- e2e/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 954d1cc3f..649893ec8 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -821,9 +821,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.11.19", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz", - "integrity": "sha512-7xMnVEcZFu0DikYjWOlRq7NTPETrm7teqUT2WkQjrTIkEgUyyGdWsj/Zg8bEJt5TNklzbPD1X3fqfsHw3SpapQ==", + "version": "20.11.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.20.tgz", + "integrity": "sha512-7/rR21OS+fq8IyHTgtLkDK949uzsa6n8BkziAKtPVpugIkO6D+/ooXMvzXxDnZrmtXVfjb1bKQafYpb8s89LOg==", "dev": true, "dependencies": { "undici-types": "~5.26.4" From 87a7825cbc2ddfbcf8379414794d2b5f3919b1d0 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey <10599762+fyfrey@users.noreply.github.com> Date: Wed, 28 Feb 2024 02:32:07 +0100 Subject: [PATCH 04/44] feat(server): rkmpp hardware decoding scaling (#7472) * feat(server): RKMPP hardware decode & scaling * disable hardware decoding for HDR --- server/src/domain/media/media.service.spec.ts | 12 ++++----- server/src/domain/media/media.util.ts | 26 +++++++++++++++---- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/domain/media/media.service.spec.ts index dc5934e7b..094401637 100644 --- a/server/src/domain/media/media.service.spec.ts +++ b/server/src/domain/media/media.service.spec.ts @@ -1799,7 +1799,7 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', { - inputOptions: [], + inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'], outputOptions: [ `-c:v hevc_rkmpp`, '-c:a copy', @@ -1810,9 +1810,9 @@ describe(MediaService.name, () => { '-g 256', '-tag:v hvc1', '-v verbose', - '-vf scale=-2:720,format=yuv420p', + '-vf scale_rkrga=-2:720:format=nv12:afbc=1', '-level 153', - '-rc_mode 3', + '-rc_mode AVBR', '-b:v 10000k', ], twoPass: false, @@ -1834,7 +1834,7 @@ describe(MediaService.name, () => { '/original/path.ext', 'upload/encoded-video/user-id/as/se/asset-id.mp4', { - inputOptions: [], + inputOptions: ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga'], outputOptions: [ `-c:v h264_rkmpp`, '-c:a copy', @@ -1844,9 +1844,9 @@ describe(MediaService.name, () => { '-map 0:1', '-g 256', '-v verbose', - '-vf scale=-2:720,format=yuv420p', + '-vf scale_rkrga=-2:720:format=nv12:afbc=1', '-level 51', - '-rc_mode 2', + '-rc_mode CQP', '-qp_init 30', ], twoPass: false, diff --git a/server/src/domain/media/media.util.ts b/server/src/domain/media/media.util.ts index e5890bdd0..d5f08ab0d 100644 --- a/server/src/domain/media/media.util.ts +++ b/server/src/domain/media/media.util.ts @@ -14,7 +14,7 @@ class BaseConfig implements VideoCodecSWConfig { getOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) { const options = { - inputOptions: this.getBaseInputOptions(), + inputOptions: this.getBaseInputOptions(videoStream), outputOptions: [...this.getBaseOutputOptions(target, videoStream, audioStream), '-v verbose'], twoPass: this.eligibleForTwoPass(), } as TranscodeOptions; @@ -30,7 +30,8 @@ class BaseConfig implements VideoCodecSWConfig { return options; } - getBaseInputOptions(): string[] { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getBaseInputOptions(videoStream: VideoStreamInfo): string[] { return []; } @@ -611,10 +612,25 @@ export class RKMPPConfig extends BaseHWConfig { return false; } - getBaseInputOptions() { + getBaseInputOptions(videoStream: VideoStreamInfo) { if (this.devices.length === 0) { throw new Error('No RKMPP device found'); } + if (this.shouldToneMap(videoStream)) { + // disable hardware decoding + return []; + } + return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']; + } + + getFilterOptions(videoStream: VideoStreamInfo) { + if (this.shouldToneMap(videoStream)) { + // use software filter options + return super.getFilterOptions(videoStream); + } + if (this.shouldScale(videoStream)) { + return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`]; + } return []; } @@ -638,10 +654,10 @@ export class RKMPPConfig extends BaseHWConfig { const bitrate = this.getMaxBitrateValue(); if (bitrate > 0) { // -b:v specifies max bitrate, average bitrate is derived automatically... - return ['-rc_mode 3', `-b:v ${bitrate}${this.getBitrateUnit()}`]; + return ['-rc_mode AVBR', `-b:v ${bitrate}${this.getBitrateUnit()}`]; } // use CRF value as QP value - return ['-rc_mode 2', `-qp_init ${this.config.crf}`]; + return ['-rc_mode CQP', `-qp_init ${this.config.crf}`]; } getSupportedCodecs() { From a02a24f349fbc8778dc8d9c57c535cbbbdc47577 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Feb 2024 23:09:40 -0600 Subject: [PATCH 05/44] chore: post release tasks --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/report.xml | 12 ++++++------ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index e9c5dda27..daa6bff38 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -379,7 +379,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -515,7 +515,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 139; + CURRENT_PROJECT_VERSION = 140; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 30ec0055c..e4356050c 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -55,11 +55,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.95.0 + 1.96.0 CFBundleSignature ???? CFBundleVersion - 139 + 140 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 7e27b4085..dc478d8ee 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + From e2c0945bc12398394503161c36307a89d8ca752e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Feb 2024 23:09:48 -0600 Subject: [PATCH 06/44] chore: post release tasks --- mobile/android/fastlane/report.xml | 6 ++-- mobile/pubspec.lock | 56 +++++++++--------------------- 2 files changed, 19 insertions(+), 43 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 585549fdb..c5442f6cd 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 9e379d465..f27351898 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -413,10 +413,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "6.1.4" file_selector_linux: dependency: transitive description: @@ -860,30 +860,6 @@ packages: url: "https://pub.dev" source: hosted version: "4.8.1" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" - url: "https://pub.dev" - source: hosted - version: "10.0.0" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 - url: "https://pub.dev" - source: hosted - version: "2.0.1" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 - url: "https://pub.dev" - source: hosted - version: "2.0.1" lints: dependency: transitive description: @@ -931,18 +907,18 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.16" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.5.0" meta: dependency: "direct overridden" description: @@ -1026,10 +1002,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.8.3" path_provider: dependency: "direct main" description: @@ -1162,10 +1138,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102 url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.2" plugin_platform_interface: dependency: transitive description: @@ -1194,10 +1170,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "4.2.4" provider: dependency: transitive description: @@ -1663,10 +1639,10 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583 url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "11.10.0" wakelock_plus: dependency: "direct main" description: @@ -1711,10 +1687,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.2" win32: dependency: transitive description: From 74d431f88198fe8c07276c2ac3b8890bcfa36ce7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 28 Feb 2024 04:21:31 -0500 Subject: [PATCH 07/44] refactor(server): format and metadata e2e (#7477) * refactor(server): format and metadata e2e * refactor: on upload success waiting --- e2e/package-lock.json | 62 +++ e2e/package.json | 1 + e2e/src/api/specs/asset.e2e-spec.ts | 417 ++++++++++++++++++--- e2e/src/api/specs/audit.e2e-spec.ts | 4 +- e2e/src/api/specs/trash.e2e-spec.ts | 2 +- e2e/src/setup.ts | 8 +- e2e/src/utils.ts | 77 +++- server/e2e/jobs/specs/formats.e2e-spec.ts | 158 -------- server/e2e/jobs/specs/metadata.e2e-spec.ts | 102 ----- 9 files changed, 509 insertions(+), 322 deletions(-) delete mode 100644 server/e2e/jobs/specs/formats.e2e-spec.ts delete mode 100644 server/e2e/jobs/specs/metadata.e2e-spec.ts diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 649893ec8..edf5a1a09 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -17,6 +17,7 @@ "@types/pg": "^8.11.0", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.3.0", + "exiftool-vendored": "^24.5.0", "luxon": "^3.4.4", "pg": "^8.11.3", "socket.io-client": "^4.7.4", @@ -594,6 +595,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@photostructure/tz-lookup": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@photostructure/tz-lookup/-/tz-lookup-9.0.2.tgz", + "integrity": "sha512-H8+tTt7ilJNkFyb+QgPnLEGUjQzGwiMb9n7lwRZNBgSKL3VZs9AkjI1E//FcwPjNafwAH932U92+xTqJiF3Bbw==", + "dev": true + }, "node_modules/@playwright/test": { "version": "1.41.2", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.41.2.tgz", @@ -1074,6 +1081,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true }, + "node_modules/batch-cluster": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/batch-cluster/-/batch-cluster-13.0.0.tgz", + "integrity": "sha512-EreW0Vi8TwovhYUHBXXRA5tthuU2ynGsZFlboyMJHCCUXYa2AjgwnE3ubBOJs2xJLcuXFJbi6c/8pH5+FVj8Og==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1391,6 +1407,43 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/exiftool-vendored": { + "version": "24.5.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-24.5.0.tgz", + "integrity": "sha512-uLGYfeshak3mYn2ucCsebXfNFdOpeAULlMb84wiJv+4B236n+ypgK/vr8bJgAcsIPSRJXFSz9WonvjjQYYqR3w==", + "dev": true, + "dependencies": { + "@photostructure/tz-lookup": "^9.0.1", + "@types/luxon": "^3.4.2", + "batch-cluster": "^13.0.0", + "he": "^1.2.0", + "luxon": "^3.4.4" + }, + "optionalDependencies": { + "exiftool-vendored.exe": "12.76.0", + "exiftool-vendored.pl": "12.76.0" + } + }, + "node_modules/exiftool-vendored.exe": { + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.76.0.tgz", + "integrity": "sha512-lbKPPs31qpjnhFiMRaVxJX+iNcJ+p0NrRSFLHHaX6KTsfMba6e5i6NykSvU3wMiafzUTef1Fen3XQ+8n1tjjNw==", + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/exiftool-vendored.pl": { + "version": "12.76.0", + "resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.76.0.tgz", + "integrity": "sha512-4DxqgnvL71YziVoY27ZMgVfLAWDH3pQLljuV5+ffTnTPvz/BWeV+/bVFwRvDqCD3lkCWds0YfVcsycfJgbQ5fA==", + "dev": true, + "optional": true, + "os": [ + "!win32" + ] + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -1584,6 +1637,15 @@ "node": ">= 0.4" } }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, "node_modules/hexoid": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz", diff --git a/e2e/package.json b/e2e/package.json index 7bbdfd1d9..26a1d7ef3 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -21,6 +21,7 @@ "@types/pg": "^8.11.0", "@types/supertest": "^6.0.2", "@vitest/coverage-v8": "^1.3.0", + "exiftool-vendored": "^24.5.0", "luxon": "^3.4.4", "pg": "^8.11.3", "socket.io-client": "^4.7.4", diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index db1821260..e1f445031 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -1,16 +1,39 @@ import { AssetFileUploadResponseDto, AssetResponseDto, + AssetTypeEnum, LoginResponseDto, SharedLinkType, } from '@immich/sdk'; +import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; +import { createHash } from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { basename, join } from 'node:path'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; -import { apiUtils, app, dbUtils } from 'src/utils'; +import { + apiUtils, + app, + dbUtils, + tempDir, + testAssetDir, + wsUtils, +} from 'src/utils'; import request from 'supertest'; -import { beforeAll, describe, expect, it } from 'vitest'; +import { afterAll, beforeAll, describe, expect, it } from 'vitest'; + +const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`; + +const sha1 = (bytes: Buffer) => + createHash('sha1').update(bytes).digest('base64'); + +const readTags = async (bytes: Buffer, filename: string) => { + const filepath = join(tempDir, filename); + await writeFile(filepath, bytes); + return exiftool.read(filepath); +}; const today = DateTime.fromObject({ year: 2023, @@ -24,25 +47,36 @@ describe('/asset', () => { let user1: LoginResponseDto; let user2: LoginResponseDto; let userStats: LoginResponseDto; - let asset1: AssetFileUploadResponseDto; - let asset2: AssetFileUploadResponseDto; - let asset3: AssetFileUploadResponseDto; - let asset4: AssetFileUploadResponseDto; // user2 asset - let asset5: AssetFileUploadResponseDto; - let asset6: AssetFileUploadResponseDto; + let user1Assets: AssetFileUploadResponseDto[]; + let user2Assets: AssetFileUploadResponseDto[]; + let assetLocation: AssetFileUploadResponseDto; let ws: Socket; beforeAll(async () => { apiUtils.setup(); await dbUtils.reset(); admin = await apiUtils.adminSetup({ onboarding: false }); - [user1, user2, userStats] = await Promise.all([ + + [ws, user1, user2, userStats] = await Promise.all([ + wsUtils.connect(admin.accessToken), apiUtils.userSetup(admin.accessToken, createUserDto.user1), apiUtils.userSetup(admin.accessToken, createUserDto.user2), apiUtils.userSetup(admin.accessToken, createUserDto.user3), ]); - [asset1, asset2, asset3, asset4, asset5, asset6] = await Promise.all([ + // asset location + assetLocation = await apiUtils.createAsset( + admin.accessToken, + {}, + { + filename: 'thompson-springs.jpg', + bytes: await readFile(locationAssetFilepath), + }, + ); + + await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id }); + + user1Assets = await Promise.all([ apiUtils.createAsset(user1.accessToken), apiUtils.createAsset(user1.accessToken), apiUtils.createAsset( @@ -56,10 +90,13 @@ describe('/asset', () => { }, { filename: 'example.mp4' }, ), - apiUtils.createAsset(user2.accessToken), apiUtils.createAsset(user1.accessToken), apiUtils.createAsset(user1.accessToken), + ]); + user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]); + + await Promise.all([ // stats apiUtils.createAsset(userStats.accessToken), apiUtils.createAsset(userStats.accessToken, { isFavorite: true }), @@ -77,7 +114,14 @@ describe('/asset', () => { const person1 = await apiUtils.createPerson(user1.accessToken, { name: 'Test Person', }); - await dbUtils.createFace({ assetId: asset1.id, personId: person1.id }); + await dbUtils.createFace({ + assetId: user1Assets[0].id, + personId: person1.id, + }); + }, 30_000); + + afterAll(() => { + wsUtils.disconnect(ws); }); describe('GET /asset/:id', () => { @@ -99,7 +143,7 @@ describe('/asset', () => { it('should require access', async () => { const { status, body } = await request(app) - .get(`/asset/${asset4.id}`) + .get(`/asset/${user2Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); @@ -107,33 +151,33 @@ describe('/asset', () => { it('should get the asset info', async () => { const { status, body } = await request(app) - .get(`/asset/${asset1.id}`) + .get(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); + expect(body).toMatchObject({ id: user1Assets[0].id }); }); it('should work with a shared link', async () => { const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, - assetIds: [asset1.id], + assetIds: [user1Assets[0].id], }); const { status, body } = await request(app).get( - `/asset/${asset1.id}?key=${sharedLink.key}`, + `/asset/${user1Assets[0].id}?key=${sharedLink.key}`, ); expect(status).toBe(200); - expect(body).toMatchObject({ id: asset1.id }); + expect(body).toMatchObject({ id: user1Assets[0].id }); }); it('should not send people data for shared links for un-authenticated users', async () => { const { status, body } = await request(app) - .get(`/asset/${asset1.id}`) + .get(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toEqual(200); expect(body).toMatchObject({ - id: asset1.id, + id: user1Assets[0].id, isFavorite: false, people: [ { @@ -148,11 +192,11 @@ describe('/asset', () => { const sharedLink = await apiUtils.createSharedLink(user1.accessToken, { type: SharedLinkType.Individual, - assetIds: [asset1.id], + assetIds: [user1Assets[0].id], }); const data = await request(app).get( - `/asset/${asset1.id}?key=${sharedLink.key}`, + `/asset/${user1Assets[0].id}?key=${sharedLink.key}`, ); expect(data.status).toBe(200); expect(data.body).toMatchObject({ people: [] }); @@ -246,11 +290,11 @@ describe('/asset', () => { const assets: AssetResponseDto[] = body; expect(assets.length).toBe(1); expect(assets[0].ownerId).toBe(user1.userId); - // - // assets owned by user2 - expect(assets[0].id).not.toBe(asset4.id); + // assets owned by user1 - expect([asset1.id, asset2.id, asset3.id]).toContain(assets[0].id); + expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id); + // assets owned by user2 + expect([user1Assets.map(({ id }) => id)]).not.toContain(assets[0].id); }); it.each(Array(10))('should return 2 random assets', async () => { @@ -266,9 +310,9 @@ describe('/asset', () => { for (const asset of assets) { expect(asset.ownerId).toBe(user1.userId); // assets owned by user1 - expect([asset1.id, asset2.id, asset3.id]).toContain(asset.id); + expect([user1Assets.map(({ id }) => id)]).toContain(asset.id); // assets owned by user2 - expect(asset.id).not.toBe(asset4.id); + expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.id); } }); @@ -280,7 +324,9 @@ describe('/asset', () => { .set('Authorization', `Bearer ${user2.accessToken}`); expect(status).toBe(200); - expect(body).toEqual([expect.objectContaining({ id: asset4.id })]); + expect(body).toEqual([ + expect.objectContaining({ id: user2Assets[0].id }), + ]); }, ); @@ -312,44 +358,50 @@ describe('/asset', () => { it('should require access', async () => { const { status, body } = await request(app) - .put(`/asset/${asset4.id}`) + .put(`/asset/${user2Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); expect(body).toEqual(errorDto.noPermission); }); it('should favorite an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id); + const before = await apiUtils.getAssetInfo( + user1.accessToken, + user1Assets[0].id, + ); expect(before.isFavorite).toBe(false); const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ isFavorite: true }); - expect(body).toMatchObject({ id: asset1.id, isFavorite: true }); + expect(body).toMatchObject({ id: user1Assets[0].id, isFavorite: true }); expect(status).toEqual(200); }); it('should archive an asset', async () => { - const before = await apiUtils.getAssetInfo(user1.accessToken, asset1.id); + const before = await apiUtils.getAssetInfo( + user1.accessToken, + user1Assets[0].id, + ); expect(before.isArchived).toBe(false); const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ isArchived: true }); - expect(body).toMatchObject({ id: asset1.id, isArchived: true }); + expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true }); expect(status).toEqual(200); }); it('should update date time original', async () => { const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' }); expect(body).toMatchObject({ - id: asset1.id, + id: user1Assets[0].id, exifInfo: expect.objectContaining({ dateTimeOriginal: '2023-11-20T01:11:00.000Z', }), @@ -371,7 +423,7 @@ describe('/asset', () => { { latitude: 12, longitude: 181 }, ]) { const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .send(test) .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(400); @@ -381,12 +433,12 @@ describe('/asset', () => { it('should update gps data', async () => { const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ latitude: 12, longitude: 12 }); expect(body).toMatchObject({ - id: asset1.id, + id: user1Assets[0].id, exifInfo: expect.objectContaining({ latitude: 12, longitude: 12 }), }); expect(status).toEqual(200); @@ -394,11 +446,11 @@ describe('/asset', () => { it('should set the description', async () => { const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ description: 'Test asset description' }); expect(body).toMatchObject({ - id: asset1.id, + id: user1Assets[0].id, exifInfo: expect.objectContaining({ description: 'Test asset description', }), @@ -408,12 +460,12 @@ describe('/asset', () => { it('should return tagged people', async () => { const { status, body } = await request(app) - .put(`/asset/${asset1.id}`) + .put(`/asset/${user1Assets[0].id}`) .set('Authorization', `Bearer ${user1.accessToken}`) .send({ isFavorite: true }); expect(status).toEqual(200); expect(body).toMatchObject({ - id: asset1.id, + id: user1Assets[0].id, isFavorite: true, people: [ { @@ -478,4 +530,279 @@ describe('/asset', () => { expect(after.isTrashed).toBe(true); }); }); + + describe('POST /asset/upload', () => { + const tests = [ + { + input: 'formats/jpg/el_torcal_rocks.jpg', + expected: { + type: AssetTypeEnum.Image, + originalFileName: 'el_torcal_rocks', + resized: true, + exifInfo: { + dateTimeOriginal: '2012-08-05T11:39:59.000Z', + exifImageWidth: 512, + exifImageHeight: 341, + latitude: null, + longitude: null, + focalLength: 75, + iso: 200, + fNumber: 11, + exposureTime: '1/160', + fileSizeInByte: 53_493, + make: 'SONY', + model: 'DSLR-A550', + orientation: null, + description: 'SONY DSC', + }, + }, + }, + { + input: 'formats/heic/IMG_2682.heic', + expected: { + type: AssetTypeEnum.Image, + originalFileName: 'IMG_2682', + resized: true, + fileCreatedAt: '2019-03-21T16:04:22.348Z', + exifInfo: { + dateTimeOriginal: '2019-03-21T16:04:22.348Z', + exifImageWidth: 4032, + exifImageHeight: 3024, + latitude: 41.2203, + longitude: -96.071_625, + make: 'Apple', + model: 'iPhone 7', + lensModel: 'iPhone 7 back camera 3.99mm f/1.8', + fileSizeInByte: 880_703, + exposureTime: '1/887', + iso: 20, + focalLength: 3.99, + fNumber: 1.8, + timeZone: 'America/Chicago', + }, + }, + }, + { + input: 'formats/png/density_plot.png', + expected: { + type: AssetTypeEnum.Image, + originalFileName: 'density_plot', + resized: true, + exifInfo: { + exifImageWidth: 800, + exifImageHeight: 800, + latitude: null, + longitude: null, + fileSizeInByte: 25_408, + }, + }, + }, + { + input: 'formats/raw/Nikon/D80/glarus.nef', + expected: { + type: AssetTypeEnum.Image, + originalFileName: 'glarus', + resized: true, + fileCreatedAt: '2010-07-20T17:27:12.000Z', + exifInfo: { + make: 'NIKON CORPORATION', + model: 'NIKON D80', + exposureTime: '1/200', + fNumber: 10, + focalLength: 18, + iso: 100, + fileSizeInByte: 9_057_784, + dateTimeOriginal: '2010-07-20T17:27:12.000Z', + latitude: null, + longitude: null, + orientation: '1', + }, + }, + }, + { + input: 'formats/raw/Nikon/D700/philadelphia.nef', + expected: { + type: AssetTypeEnum.Image, + originalFileName: 'philadelphia', + resized: true, + fileCreatedAt: '2016-09-22T22:10:29.060Z', + exifInfo: { + make: 'NIKON CORPORATION', + model: 'NIKON D700', + exposureTime: '1/400', + fNumber: 11, + focalLength: 85, + iso: 200, + fileSizeInByte: 15_856_335, + dateTimeOriginal: '2016-09-22T22:10:29.060Z', + latitude: null, + longitude: null, + orientation: '1', + timeZone: 'UTC-5', + }, + }, + }, + ]; + + for (const { input, expected } of tests) { + it(`should generate a thumbnail for ${input}`, async () => { + const filepath = join(testAssetDir, input); + const { id, duplicate } = await apiUtils.createAsset( + admin.accessToken, + {}, + { bytes: await readFile(filepath), filename: basename(filepath) }, + ); + + expect(duplicate).toBe(false); + + await wsUtils.waitForEvent({ event: 'upload', assetId: id }); + + const asset = await apiUtils.getAssetInfo(admin.accessToken, id); + + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo).toMatchObject(expected.exifInfo); + expect(asset).toMatchObject(expected); + }); + } + + it('should handle a duplicate', async () => { + const filepath = 'formats/jpeg/el_torcal_rocks.jpeg'; + const { duplicate } = await apiUtils.createAsset( + admin.accessToken, + {}, + { + bytes: await readFile(join(testAssetDir, filepath)), + filename: basename(filepath), + }, + ); + + expect(duplicate).toBe(true); + }); + + // These hashes were created by copying the image files to a Samsung phone, + // exporting the video from Samsung's stock Gallery app, and hashing them locally. + // This ensures that immich+exiftool are extracting the videos the same way Samsung does. + // DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives + // into the test here. + const motionTests = [ + { + filepath: 'formats/motionphoto/Samsung One UI 5.jpg', + checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=', + }, + { + filepath: 'formats/motionphoto/Samsung One UI 6.jpg', + checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=', + }, + { + filepath: 'formats/motionphoto/Samsung One UI 6.heic', + checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=', + }, + ]; + + for (const { filepath, checksum } of motionTests) { + it(`should extract motionphoto video from ${filepath}`, async () => { + const response = await apiUtils.createAsset( + admin.accessToken, + {}, + { + bytes: await readFile(join(testAssetDir, filepath)), + filename: basename(filepath), + }, + ); + + await wsUtils.waitForEvent({ event: 'upload', assetId: response.id }); + + expect(response.duplicate).toBe(false); + + const asset = await apiUtils.getAssetInfo( + admin.accessToken, + response.id, + ); + expect(asset.livePhotoVideoId).toBeDefined(); + + const video = await apiUtils.getAssetInfo( + admin.accessToken, + asset.livePhotoVideoId as string, + ); + expect(video.checksum).toStrictEqual(checksum); + }); + } + }); + + describe('GET /asset/thumbnail/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/asset/thumbnail/${assetLocation.id}`, + ); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should not include gps data for webp thumbnails', async () => { + const { status, body, type } = await request(app) + .get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + await wsUtils.waitForEvent({ + event: 'upload', + assetId: assetLocation.id, + }); + + expect(status).toBe(200); + expect(body).toBeDefined(); + expect(type).toBe('image/webp'); + + const exifData = await readTags(body, 'thumbnail.webp'); + expect(exifData).not.toHaveProperty('GPSLongitude'); + expect(exifData).not.toHaveProperty('GPSLatitude'); + }); + + it('should not include gps data for jpeg thumbnails', async () => { + const { status, body, type } = await request(app) + .get(`/asset/thumbnail/${assetLocation.id}?format=JPEG`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toBeDefined(); + expect(type).toBe('image/jpeg'); + + const exifData = await readTags(body, 'thumbnail.jpg'); + expect(exifData).not.toHaveProperty('GPSLongitude'); + expect(exifData).not.toHaveProperty('GPSLatitude'); + }); + }); + + describe('GET /asset/file/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get( + `/asset/thumbnail/${assetLocation.id}`, + ); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should download the original', async () => { + const { status, body, type } = await request(app) + .get(`/asset/file/${assetLocation.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toBeDefined(); + expect(type).toBe('image/jpeg'); + + const asset = await apiUtils.getAssetInfo( + admin.accessToken, + assetLocation.id, + ); + + const original = await readFile(locationAssetFilepath); + const originalChecksum = sha1(original); + const downloadChecksum = sha1(body); + + expect(originalChecksum).toBe(downloadChecksum); + expect(downloadChecksum).toBe(asset.checksum); + }); + }); }); diff --git a/e2e/src/api/specs/audit.e2e-spec.ts b/e2e/src/api/specs/audit.e2e-spec.ts index 073106e72..0bc8e6b17 100644 --- a/e2e/src/api/specs/audit.e2e-spec.ts +++ b/e2e/src/api/specs/audit.e2e-spec.ts @@ -29,14 +29,14 @@ describe('/audit', () => { await Promise.all([ deleteAssets( { assetBulkDeleteDto: { ids: [trashedAsset.id] } }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ), updateAsset( { id: archivedAsset.id, updateAssetDto: { isArchived: true }, }, - { headers: asBearerAuth(admin.accessToken) } + { headers: asBearerAuth(admin.accessToken) }, ), ]); diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts index 2de838f98..cb4a8b9dd 100644 --- a/e2e/src/api/specs/trash.e2e-spec.ts +++ b/e2e/src/api/specs/trash.e2e-spec.ts @@ -44,7 +44,7 @@ describe('/trash', () => { .set('Authorization', `Bearer ${admin.accessToken}`); expect(status).toBe(204); - await wsUtils.once(ws, 'on_asset_delete'); + await wsUtils.waitForEvent({ event: 'delete', assetId }); const after = await getAllAssets( {}, diff --git a/e2e/src/setup.ts b/e2e/src/setup.ts index b560a2bbb..04e8d79ac 100644 --- a/e2e/src/setup.ts +++ b/e2e/src/setup.ts @@ -2,25 +2,25 @@ import { spawn, exec } from 'child_process'; export default async () => { let _resolve: () => unknown; - const promise = new Promise((resolve) => (_resolve = resolve)); + const ready = new Promise((resolve) => (_resolve = resolve)); const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' }); child.stdout.on('data', (data) => { const input = data.toString(); console.log(input); - if (input.includes('Immich Server is listening')) { + if (input.includes('Immich Microservices is listening')) { _resolve(); } }); child.stderr.on('data', (data) => console.log(data.toString())); - await promise; + await ready; return async () => { await new Promise((resolve) => - exec('docker compose down', () => resolve()) + exec('docker compose down', () => resolve()), ); }; }; diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 428c88b45..4261e8f67 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -1,5 +1,6 @@ import { AssetFileUploadResponseDto, + AssetResponseDto, CreateAlbumDto, CreateAssetDto, CreateUserDto, @@ -19,10 +20,12 @@ import { updatePerson, } from '@immich/sdk'; import { BrowserContext } from '@playwright/test'; -import { exec, spawn } from 'child_process'; +import { exec, spawn } from 'node:child_process'; import { randomBytes } from 'node:crypto'; import { access } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; import path from 'node:path'; +import { EventEmitter } from 'node:stream'; import { promisify } from 'node:util'; import pg from 'pg'; import { io, type Socket } from 'socket.io-client'; @@ -40,6 +43,7 @@ const directoryExists = (directory: string) => // TODO move test assets into e2e/assets export const testAssetDir = path.resolve(`./../server/test/assets/`); +export const tempDir = tmpdir(); const serverContainerName = 'immich-e2e-server'; const mediaDir = '/usr/src/app/upload'; @@ -47,6 +51,7 @@ const dirs = [ `"${mediaDir}/thumbs"`, `"${mediaDir}/upload"`, `"${mediaDir}/library"`, + `"${mediaDir}/encoded-video"`, ].join(' '); if (!(await directoryExists(`${testAssetDir}/albums`))) { @@ -177,33 +182,85 @@ export interface AdminSetupOptions { onboarding?: boolean; } +export enum SocketEvent { + UPLOAD = 'upload', + DELETE = 'delete', +} + +export type EventType = 'upload' | 'delete'; +export interface WaitOptions { + event: EventType; + assetId: string; + timeout?: number; +} + +const events: Record> = { + upload: new Set(), + delete: new Set(), +}; + +const callbacks: Record void> = {}; + +const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => { + events[event].add(assetId); + const callback = callbacks[assetId]; + if (callback) { + callback(); + delete callbacks[assetId]; + } +}; + export const wsUtils = { connect: async (accessToken: string) => { const websocket = io('http://127.0.0.1:2283', { path: '/api/socket.io', transports: ['websocket'], extraHeaders: { Authorization: `Bearer ${accessToken}` }, - autoConnect: false, + autoConnect: true, forceNew: true, }); return new Promise((resolve) => { - websocket.on('connect', () => resolve(websocket)); - websocket.connect(); + websocket + .on('connect', () => resolve(websocket)) + .on('on_upload_success', (data: AssetResponseDto) => + onEvent({ event: 'upload', assetId: data.id }), + ) + .on('on_asset_delete', (assetId: string) => + onEvent({ event: 'delete', assetId }), + ) + .connect(); }); }, disconnect: (ws: Socket) => { if (ws?.connected) { ws.disconnect(); } + + for (const set of Object.values(events)) { + set.clear(); + } }, - once: (ws: Socket, event: string): Promise => { - return new Promise((resolve, reject) => { - const timeout = setTimeout(() => reject(new Error('Timeout')), 4000); - ws.once(event, (data: T) => { + waitForEvent: async ({ + event, + assetId, + timeout: ms, + }: WaitOptions): Promise => { + const set = events[event]; + if (set.has(assetId)) { + return; + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error(`Timed out waiting for ${event} event`)), + ms || 5000, + ); + + callbacks[assetId] = () => { clearTimeout(timeout); - resolve(data); - }); + resolve(); + }; }); }, }; diff --git a/server/e2e/jobs/specs/formats.e2e-spec.ts b/server/e2e/jobs/specs/formats.e2e-spec.ts deleted file mode 100644 index c8b14d588..000000000 --- a/server/e2e/jobs/specs/formats.e2e-spec.ts +++ /dev/null @@ -1,158 +0,0 @@ -import { LoginResponseDto } from '@app/domain'; -import { AssetType } from '@app/infra/entities'; -import { readFile } from 'node:fs/promises'; -import { basename, join } from 'node:path'; -import { IMMICH_TEST_ASSET_PATH, testApp } from '../../../src/test-utils/utils'; -import { api } from '../../client'; - -const JPEG = { - type: AssetType.IMAGE, - originalFileName: 'el_torcal_rocks', - resized: true, - exifInfo: { - dateTimeOriginal: '2012-08-05T11:39:59.000Z', - exifImageWidth: 512, - exifImageHeight: 341, - latitude: null, - longitude: null, - focalLength: 75, - iso: 200, - fNumber: 11, - exposureTime: '1/160', - fileSizeInByte: 53_493, - make: 'SONY', - model: 'DSLR-A550', - orientation: null, - description: 'SONY DSC', - }, -}; - -const tests = [ - { input: 'formats/jpg/el_torcal_rocks.jpg', expected: JPEG }, - { input: 'formats/jpeg/el_torcal_rocks.jpeg', expected: JPEG }, - { - input: 'formats/heic/IMG_2682.heic', - expected: { - type: AssetType.IMAGE, - originalFileName: 'IMG_2682', - resized: true, - fileCreatedAt: '2019-03-21T16:04:22.348Z', - exifInfo: { - dateTimeOriginal: '2019-03-21T16:04:22.348Z', - exifImageWidth: 4032, - exifImageHeight: 3024, - latitude: 41.2203, - longitude: -96.071_625, - make: 'Apple', - model: 'iPhone 7', - lensModel: 'iPhone 7 back camera 3.99mm f/1.8', - fileSizeInByte: 880_703, - exposureTime: '1/887', - iso: 20, - focalLength: 3.99, - fNumber: 1.8, - timeZone: 'America/Chicago', - }, - }, - }, - { - input: 'formats/png/density_plot.png', - expected: { - type: AssetType.IMAGE, - originalFileName: 'density_plot', - resized: true, - exifInfo: { - exifImageWidth: 800, - exifImageHeight: 800, - latitude: null, - longitude: null, - fileSizeInByte: 25_408, - }, - }, - }, - { - input: 'formats/raw/Nikon/D80/glarus.nef', - expected: { - type: AssetType.IMAGE, - originalFileName: 'glarus', - resized: true, - fileCreatedAt: '2010-07-20T17:27:12.000Z', - exifInfo: { - make: 'NIKON CORPORATION', - model: 'NIKON D80', - exposureTime: '1/200', - fNumber: 10, - focalLength: 18, - iso: 100, - fileSizeInByte: 9_057_784, - dateTimeOriginal: '2010-07-20T17:27:12.000Z', - latitude: null, - longitude: null, - orientation: '1', - }, - }, - }, - { - input: 'formats/raw/Nikon/D700/philadelphia.nef', - expected: { - type: AssetType.IMAGE, - originalFileName: 'philadelphia', - resized: true, - fileCreatedAt: '2016-09-22T22:10:29.060Z', - exifInfo: { - make: 'NIKON CORPORATION', - model: 'NIKON D700', - exposureTime: '1/400', - fNumber: 11, - focalLength: 85, - iso: 200, - fileSizeInByte: 15_856_335, - dateTimeOriginal: '2016-09-22T22:10:29.060Z', - latitude: null, - longitude: null, - orientation: '1', - timeZone: 'UTC-5', - }, - }, - }, -]; - -describe(`Format (e2e)`, () => { - let server: any; - let admin: LoginResponseDto; - - beforeAll(async () => { - const app = await testApp.create(); - server = app.getHttpServer(); - }); - - beforeEach(async () => { - await testApp.reset(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - }); - - afterAll(async () => { - await testApp.teardown(); - }); - - for (const { input, expected } of tests) { - it(`should generate a thumbnail for ${input}`, async () => { - const filepath = join(IMMICH_TEST_ASSET_PATH, input); - const content = await readFile(filepath); - await api.assetApi.upload(server, admin.accessToken, 'test-device-id', { - content, - filename: basename(filepath), - }); - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toHaveLength(1); - - const asset = assets[0]; - - expect(asset.exifInfo).toBeDefined(); - expect(asset.exifInfo).toMatchObject(expected.exifInfo); - expect(asset).toMatchObject(expected); - }); - } -}); diff --git a/server/e2e/jobs/specs/metadata.e2e-spec.ts b/server/e2e/jobs/specs/metadata.e2e-spec.ts deleted file mode 100644 index 5eb75fee2..000000000 --- a/server/e2e/jobs/specs/metadata.e2e-spec.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { AssetResponseDto, LoginResponseDto } from '@app/domain'; -import { AssetController } from '@app/immich'; -import { exiftool } from 'exiftool-vendored'; -import { readFile, writeFile } from 'fs/promises'; -import { - IMMICH_TEST_ASSET_PATH, - IMMICH_TEST_ASSET_TEMP_PATH, - db, - restoreTempFolder, - testApp, -} from '../../../src/test-utils/utils'; -import { api } from '../../client'; - -describe(`${AssetController.name} (e2e)`, () => { - let server: any; - let admin: LoginResponseDto; - - beforeAll(async () => { - server = (await testApp.create()).getHttpServer(); - }); - - beforeEach(async () => { - await testApp.reset(); - await restoreTempFolder(); - await api.authApi.adminSignUp(server); - admin = await api.authApi.adminLogin(server); - }); - - afterAll(async () => { - await testApp.teardown(); - await restoreTempFolder(); - }); - - describe('should strip metadata of', () => { - let assetWithLocation: AssetResponseDto; - - beforeEach(async () => { - const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/metadata/gps-position/thompson-springs.jpg`); - - await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent }); - - const assets = await api.assetApi.getAllAssets(server, admin.accessToken); - - expect(assets).toHaveLength(1); - assetWithLocation = assets[0]; - - expect(assetWithLocation).toEqual( - expect.objectContaining({ - exifInfo: expect.objectContaining({ latitude: 39.115, longitude: -108.400968333333 }), - }), - ); - }); - - it('small webp thumbnails', async () => { - const assetId = assetWithLocation.id; - - const thumbnail = await api.assetApi.getWebpThumbnail(server, admin.accessToken, assetId); - - await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`, thumbnail); - - const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.webp`); - - expect(exifData).not.toHaveProperty('GPSLongitude'); - expect(exifData).not.toHaveProperty('GPSLatitude'); - }); - - it('large jpeg thumbnails', async () => { - const assetId = assetWithLocation.id; - - const thumbnail = await api.assetApi.getJpegThumbnail(server, admin.accessToken, assetId); - - await writeFile(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`, thumbnail); - - const exifData = await exiftool.read(`${IMMICH_TEST_ASSET_TEMP_PATH}/thumbnail.jpg`); - - expect(exifData).not.toHaveProperty('GPSLongitude'); - expect(exifData).not.toHaveProperty('GPSLatitude'); - }); - }); - - describe.each([ - // These hashes were created by copying the image files to a Samsung phone, - // exporting the video from Samsung's stock Gallery app, and hashing them locally. - // This ensures that immich+exiftool are extracting the videos the same way Samsung does. - // DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives - // into the test here. - ['Samsung One UI 5.jpg', 'fr14niqCq6N20HB8rJYEvpsUVtI='], - ['Samsung One UI 6.jpg', 'lT9Uviw/FFJYCjfIxAGPTjzAmmw='], - ['Samsung One UI 6.heic', '/ejgzywvgvzvVhUYVfvkLzFBAF0='], - ])('should extract motionphoto video', (file, checksum) => { - it(`with checksum ${checksum} from ${file}`, async () => { - const fileContent = await readFile(`${IMMICH_TEST_ASSET_PATH}/formats/motionphoto/${file}`); - - const response = await api.assetApi.upload(server, admin.accessToken, 'test-asset-id', { content: fileContent }); - const asset = await api.assetApi.get(server, admin.accessToken, response.id); - expect(asset).toHaveProperty('livePhotoVideoId'); - const video = await api.assetApi.get(server, admin.accessToken, asset.livePhotoVideoId as string); - - expect(video.checksum).toStrictEqual(checksum); - }); - }); -}); From c88184673a70ac3a1d1a2488f59fcdf0623b12a3 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 28 Feb 2024 13:25:08 +0100 Subject: [PATCH 08/44] fix(web): keep notifications in view when scrolling (#7493) --- .../shared-components/notification/notification-list.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/notification/notification-list.svelte b/web/src/lib/components/shared-components/notification/notification-list.svelte index af795072a..bf8d93d5f 100644 --- a/web/src/lib/components/shared-components/notification/notification-list.svelte +++ b/web/src/lib/components/shared-components/notification/notification-list.svelte @@ -10,7 +10,7 @@ {#if $notificationList.length > 0} -
+
{#each $notificationList as notificationInfo (notificationInfo.id)}
From 784d92dbb3bc3fdc11261920f65c88dd42b1c8ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20S=C3=A9hier?= Date: Wed, 28 Feb 2024 16:15:32 +0100 Subject: [PATCH 09/44] chore(deployment): add explicit registry to docker image names (#7496) --- docker/docker-compose.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index f1d16f7e6..6b51e01f1 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -60,12 +60,12 @@ services: redis: container_name: immich_redis - image: redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 + image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:51d6c56749a4243096327e3fb964a48ed92254357108449cb6e23999c37773c5 restart: always database: container_name: immich_postgres - image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_USER: ${DB_USERNAME} From e4f32a045d8003f9d588cf110cd9a02d5b68a874 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 28 Feb 2024 21:20:10 +0100 Subject: [PATCH 10/44] chore: remove watcher polling option (#7480) * remove watcher polling * fix lint * add db migration --- docs/docs/features/libraries.md | 5 +---- .../doc/SystemConfigLibraryWatchDto.md | 2 -- .../system_config_library_watch_dto.dart | 22 +++---------------- .../system_config_library_watch_dto_test.dart | 10 --------- open-api/immich-openapi-specs.json | 10 +-------- open-api/typescript-sdk/axios-client/api.ts | 12 ---------- open-api/typescript-sdk/fetch-client.ts | 2 -- server/src/domain/library/library.service.ts | 9 +------- .../dto/system-config-library.dto.ts | 11 ---------- .../system-config/system-config.core.ts | 2 -- .../system-config.service.spec.ts | 2 -- .../infra/entities/system-config.entity.ts | 4 ---- ...0004123-RemoveLibraryWatchPollingOption.ts | 12 ++++++++++ .../library-settings/library-settings.svelte | 22 ------------------- 14 files changed, 18 insertions(+), 107 deletions(-) create mode 100644 server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index 995d65249..58dd707ea 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -88,10 +88,7 @@ Some basic examples: This feature - currently hidden in the config file - is considered experimental and for advanced users only. If enabled, it will allow automatic watching of the filesystem which means new assets are automatically imported to Immich without needing to rescan. Deleted assets are, as always, marked as offline and can be removed with the "Remove offline files" button. -If your photos are on a network drive you will likely have to enable filesystem polling. The performance hit for polling large libraries is currently unknown, feel free to test this feature and report back. In addition to the boolean feature flag, the configuration file allows customization of the following parameters, please see the [chokidar documentation](https://github.com/paulmillr/chokidar?tab=readme-ov-file#performance) for reference. - -- `usePolling` (default: `false`). -- `interval`. (default: 10000). When using polling, this is how often (in milliseconds) the filesystem is polled. +If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes. ### Nightly job diff --git a/mobile/openapi/doc/SystemConfigLibraryWatchDto.md b/mobile/openapi/doc/SystemConfigLibraryWatchDto.md index baa0be6b0..43b975e21 100644 --- a/mobile/openapi/doc/SystemConfigLibraryWatchDto.md +++ b/mobile/openapi/doc/SystemConfigLibraryWatchDto.md @@ -9,8 +9,6 @@ import 'package:openapi/api.dart'; Name | Type | Description | Notes ------------ | ------------- | ------------- | ------------- **enabled** | **bool** | | -**interval** | **int** | | -**usePolling** | **bool** | | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) diff --git a/mobile/openapi/lib/model/system_config_library_watch_dto.dart b/mobile/openapi/lib/model/system_config_library_watch_dto.dart index 795fd15fd..0bcb6f177 100644 --- a/mobile/openapi/lib/model/system_config_library_watch_dto.dart +++ b/mobile/openapi/lib/model/system_config_library_watch_dto.dart @@ -14,37 +14,25 @@ class SystemConfigLibraryWatchDto { /// Returns a new [SystemConfigLibraryWatchDto] instance. SystemConfigLibraryWatchDto({ required this.enabled, - required this.interval, - required this.usePolling, }); bool enabled; - int interval; - - bool usePolling; - @override bool operator ==(Object other) => identical(this, other) || other is SystemConfigLibraryWatchDto && - other.enabled == enabled && - other.interval == interval && - other.usePolling == usePolling; + other.enabled == enabled; @override int get hashCode => // ignore: unnecessary_parenthesis - (enabled.hashCode) + - (interval.hashCode) + - (usePolling.hashCode); + (enabled.hashCode); @override - String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled, interval=$interval, usePolling=$usePolling]'; + String toString() => 'SystemConfigLibraryWatchDto[enabled=$enabled]'; Map toJson() { final json = {}; json[r'enabled'] = this.enabled; - json[r'interval'] = this.interval; - json[r'usePolling'] = this.usePolling; return json; } @@ -57,8 +45,6 @@ class SystemConfigLibraryWatchDto { return SystemConfigLibraryWatchDto( enabled: mapValueOfType(json, r'enabled')!, - interval: mapValueOfType(json, r'interval')!, - usePolling: mapValueOfType(json, r'usePolling')!, ); } return null; @@ -107,8 +93,6 @@ class SystemConfigLibraryWatchDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'enabled', - 'interval', - 'usePolling', }; } diff --git a/mobile/openapi/test/system_config_library_watch_dto_test.dart b/mobile/openapi/test/system_config_library_watch_dto_test.dart index 74ab53d65..19b5de05f 100644 --- a/mobile/openapi/test/system_config_library_watch_dto_test.dart +++ b/mobile/openapi/test/system_config_library_watch_dto_test.dart @@ -21,16 +21,6 @@ void main() { // TODO }); - // int interval - test('to test the property `interval`', () async { - // TODO - }); - - // bool usePolling - test('to test the property `usePolling`', () async { - // TODO - }); - }); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 44772e14a..9f6b0ce60 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9831,18 +9831,10 @@ "properties": { "enabled": { "type": "boolean" - }, - "interval": { - "type": "integer" - }, - "usePolling": { - "type": "boolean" } }, "required": [ - "enabled", - "interval", - "usePolling" + "enabled" ], "type": "object" }, diff --git a/open-api/typescript-sdk/axios-client/api.ts b/open-api/typescript-sdk/axios-client/api.ts index 154fc9969..0da04a8f2 100644 --- a/open-api/typescript-sdk/axios-client/api.ts +++ b/open-api/typescript-sdk/axios-client/api.ts @@ -4401,18 +4401,6 @@ export interface SystemConfigLibraryWatchDto { * @memberof SystemConfigLibraryWatchDto */ 'enabled': boolean; - /** - * - * @type {number} - * @memberof SystemConfigLibraryWatchDto - */ - 'interval': number; - /** - * - * @type {boolean} - * @memberof SystemConfigLibraryWatchDto - */ - 'usePolling': boolean; } /** * diff --git a/open-api/typescript-sdk/fetch-client.ts b/open-api/typescript-sdk/fetch-client.ts index 5e9752127..fb0d04d2b 100644 --- a/open-api/typescript-sdk/fetch-client.ts +++ b/open-api/typescript-sdk/fetch-client.ts @@ -835,8 +835,6 @@ export type SystemConfigLibraryScanDto = { }; export type SystemConfigLibraryWatchDto = { enabled: boolean; - interval: number; - usePolling: boolean; }; export type SystemConfigLibraryDto = { scan: SystemConfigLibraryScanDto; diff --git a/server/src/domain/library/library.service.ts b/server/src/domain/library/library.service.ts index 7c961963e..6a5982abe 100644 --- a/server/src/domain/library/library.service.ts +++ b/server/src/domain/library/library.service.ts @@ -112,20 +112,13 @@ export class LibraryService extends EventEmitter { ignore: library.exclusionPatterns, }); - const config = await this.configCore.getConfig(); - const { usePolling, interval } = config.library.watch; - - this.logger.debug(`Settings for watcher: usePolling: ${usePolling}, interval: ${interval}`); - let _resolve: () => void; const ready$ = new Promise((resolve) => (_resolve = resolve)); this.watchers[id] = this.storageRepository.watch( library.importPaths, { - usePolling, - interval, - binaryInterval: interval, + usePolling: false, ignoreInitial: true, }, { diff --git a/server/src/domain/system-config/dto/system-config-library.dto.ts b/server/src/domain/system-config/dto/system-config-library.dto.ts index caf73498f..fdbae600f 100644 --- a/server/src/domain/system-config/dto/system-config-library.dto.ts +++ b/server/src/domain/system-config/dto/system-config-library.dto.ts @@ -1,12 +1,9 @@ import { validateCronExpression } from '@app/domain'; -import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsBoolean, - IsInt, IsNotEmpty, IsObject, - IsPositive, IsString, Validate, ValidateIf, @@ -38,14 +35,6 @@ export class SystemConfigLibraryScanDto { export class SystemConfigLibraryWatchDto { @IsBoolean() enabled!: boolean; - - @IsBoolean() - usePolling!: boolean; - - @IsInt() - @IsPositive() - @ApiProperty({ type: 'integer' }) - interval!: number; } export class SystemConfigLibraryDto { diff --git a/server/src/domain/system-config/system-config.core.ts b/server/src/domain/system-config/system-config.core.ts index eb661133b..d32309955 100644 --- a/server/src/domain/system-config/system-config.core.ts +++ b/server/src/domain/system-config/system-config.core.ts @@ -132,8 +132,6 @@ export const defaults = Object.freeze({ }, watch: { enabled: false, - usePolling: false, - interval: 10_000, }, }, server: { diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/domain/system-config/system-config.service.spec.ts index ec0b4b8f4..67a6418f4 100644 --- a/server/src/domain/system-config/system-config.service.spec.ts +++ b/server/src/domain/system-config/system-config.service.spec.ts @@ -136,8 +136,6 @@ const updatedConfig = Object.freeze({ }, watch: { enabled: false, - usePolling: false, - interval: 10_000, }, }, }); diff --git a/server/src/infra/entities/system-config.entity.ts b/server/src/infra/entities/system-config.entity.ts index 8307a0328..edf473435 100644 --- a/server/src/infra/entities/system-config.entity.ts +++ b/server/src/infra/entities/system-config.entity.ts @@ -51,8 +51,6 @@ export enum SystemConfigKey { LIBRARY_SCAN_CRON_EXPRESSION = 'library.scan.cronExpression', LIBRARY_WATCH_ENABLED = 'library.watch.enabled', - LIBRARY_WATCH_USE_POLLING = 'library.watch.usePolling', - LIBRARY_WATCH_INTERVAL = 'library.watch.interval', LOGGING_ENABLED = 'logging.enabled', LOGGING_LEVEL = 'logging.level', @@ -268,8 +266,6 @@ export interface SystemConfig { }; watch: { enabled: boolean; - usePolling: boolean; - interval: number; }; }; server: { diff --git a/server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts b/server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts new file mode 100644 index 000000000..8b7ff3a67 --- /dev/null +++ b/server/src/infra/migrations/1709150004123-RemoveLibraryWatchPollingOption.ts @@ -0,0 +1,12 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveLibraryWatchPollingOption1709150004123 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.usePolling'`); + await queryRunner.query(`DELETE FROM "system_config" WHERE key = 'library.watch.interval'`); + } + + public async down(): Promise { + // noop + } +} diff --git a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte index 746db2c19..c1076abc5 100644 --- a/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte +++ b/web/src/lib/components/admin-page/settings/library-settings/library-settings.svelte @@ -42,28 +42,6 @@ subtitle="Watch external libraries for file changes" bind:checked={config.library.watch.enabled} /> - - - - - -

- Interval of filesystem polling, in milliseconds. Lower values will result in higher CPU usage. -

-
-
From 84fe41df31ebb5899b9045915bb5b71733c0a556 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Wed, 28 Feb 2024 22:39:53 +0100 Subject: [PATCH 11/44] fix(web): re-render albums (#7403) * fix: re-render albums * fix: album description * fix: reactivity * fix album reactivity + components for title and description * only update AssetGrid when albumId changes * remove title and description bindings * remove console.log * chore: fix merge * pr feedback * pr feedback --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --- .../album-page/album-description.svelte | 45 +++ .../components/album-page/album-title.svelte | 42 +++ .../settings/setting-switch.svelte | 4 +- .../(user)/albums/[albumId]/+page.svelte | 297 +++++++----------- 4 files changed, 209 insertions(+), 179 deletions(-) create mode 100644 web/src/lib/components/album-page/album-description.svelte create mode 100644 web/src/lib/components/album-page/album-title.svelte diff --git a/web/src/lib/components/album-page/album-description.svelte b/web/src/lib/components/album-page/album-description.svelte new file mode 100644 index 000000000..9e988ba75 --- /dev/null +++ b/web/src/lib/components/album-page/album-description.svelte @@ -0,0 +1,45 @@ + + +{#if isOwned} +