From 3f1f835df3c95f939aa006819459e7a68897f6a5 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Sep 2022 15:13:18 -0500 Subject: [PATCH 01/53] Update readme for beta release invitation links --- README.md | 5 +++++ mobile/android/fastlane/Fastfile | 9 +++++++-- mobile/android/fastlane/README.md | 6 +++--- .../metadata/android/en-US/changelogs/47.txt | 1 + mobile/android/fastlane/report.xml | 6 +++--- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/ios/fastlane/report.xml | 12 ++++++------ mobile/pubspec.yaml | 2 +- 10 files changed, 32 insertions(+), 21 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt diff --git a/README.md b/README.md index ac776b25c2..305f32648d 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM - [Installation](#installation) - [Update](#update) - [Mobile App](#mobile-app) +- [App Beta Invitation links](#App-Beta-release-channel) - [Development](#development) - [Support](#support) - [Known Issues](#known-issues) @@ -205,7 +206,11 @@ docker-compose pull && docker-compose up -d > *The Play/App Store version might be lagging behind the latest release due to the review process.* +# App Beta release channel +You can opt-in to join app beta release channel by following the links below: +* Android: Invitation link from [web](https://play.google.com/store/apps/details?id=app.alextran.immich) or from [mobile](https://play.google.com/store/apps/details?id=app.alextran.immich) +* iOS: [TestFlight invitation link](https://testflight.apple.com/join/1vYsAa8P)
# Development diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index c0fc2fe307..96589d03bb 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -16,12 +16,17 @@ default_platform(:android) platform :android do - desc "Build Android" - lane :build do + desc "Build Android and Release Testing" + lane :beta do gradle( task: 'bundle', build_type: 'Release', + properties: { + "android.injected.version.code" => 47, + "android.injected.version.name" => "1.30.1", + } ) + upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab', track: 'beta') end desc "Build and Release Android" diff --git a/mobile/android/fastlane/README.md b/mobile/android/fastlane/README.md index fb4b573aac..11cb12000a 100644 --- a/mobile/android/fastlane/README.md +++ b/mobile/android/fastlane/README.md @@ -15,13 +15,13 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do ## Android -### android build +### android beta ```sh -[bundle exec] fastlane android build +[bundle exec] fastlane android beta ``` -Build Android +Build Android and Release Testing ### android release diff --git a/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt new file mode 100644 index 0000000000..a97d899715 --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/47.txt @@ -0,0 +1 @@ +* Improve scroll thumb date info \ No newline at end of file diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 7ec06ddace..33baa83495 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index f5f19fdccc..d3518d6393 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -495,7 +495,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 62; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -522,7 +522,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 60; + CURRENT_PROJECT_VERSION = 62; 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 80140355d9..639862b8b2 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -17,11 +17,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.29.6 + 1.30.1 CFBundleSignature ???? CFBundleVersion - 60 + 62 LSRequiresIPhoneOS MGLMapboxMetricsEnabledSettingShownInApp diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 41089b5b15..19734a042b 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.30.0" + version_number: "1.30.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/ios/fastlane/report.xml b/mobile/ios/fastlane/report.xml index 632cdf7c9b..61fe97cbff 100644 --- a/mobile/ios/fastlane/report.xml +++ b/mobile/ios/fastlane/report.xml @@ -5,32 +5,32 @@ - + - + - + - + - + - + diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 982f2bbf55..47ea6dcff7 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.30.0+46 +version: 1.30.1+47 environment: sdk: ">=2.17.0 <3.0.0" From 8bb656cb17222f76926eb9eb84cdfff808101693 Mon Sep 17 00:00:00 2001 From: Jonas Janz <5434875+PixelJonas@users.noreply.github.com> Date: Sat, 1 Oct 2022 23:01:27 +0200 Subject: [PATCH 02/53] add docker volumes to services (#766) * add docker volumes to services this change adds the volume definitions for /usr/src/app/upload /usr/src/app/.reverse-geocoding-dump to the `immich-server` docker-compose files as /usr/src/app/upload should always be a volume for the containers I also added it to the `Dockerfile` Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com> * remove geocoding-dump volume from docker-compose Signed-off-by: PixelJonas <5434875+PixelJonas@users.noreply.github.com> --- server/Dockerfile | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/Dockerfile b/server/Dockerfile index 5927a68278..c3daf8cda9 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -29,4 +29,6 @@ COPY --from=builder /usr/src/app/dist ./dist RUN npm prune --production +VOLUME /usr/src/app/upload + EXPOSE 3001 From 4342285507b155e340d2bad67acc4929b17521fb Mon Sep 17 00:00:00 2001 From: Deepesh Bhardwaj <70226498+deepesh16b@users.noreply.github.com> Date: Tue, 4 Oct 2022 20:16:06 +0530 Subject: [PATCH 03/53] Updated jpeg thumbnail path (#780) --- server/apps/microservices/src/processors/thumbnail.processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts index eb368f1a82..130aa6c1ad 100644 --- a/server/apps/microservices/src/processors/thumbnail.processor.ts +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -62,7 +62,7 @@ export class ThumbnailGeneratorProcessor { const temp = asset.originalPath.split('/'); const originalFilename = temp[temp.length - 1].split('.')[0]; - const jpegThumbnailPath = resizePath + originalFilename + '.jpeg'; + const jpegThumbnailPath = join(resizePath, `${originalFilename}.jpeg`); if (asset.type == AssetType.IMAGE) { try { From 479f706f8abdbc68e42a20304bb6f88a4226e448 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Oct 2022 15:19:29 -0500 Subject: [PATCH 04/53] fix(mobile): Fix error parsing datetime prevent the timeline to be displayed (#784) --- .../home_page_render_list_provider.dart | 70 +++++++++++-------- 1 file changed, 39 insertions(+), 31 deletions(-) diff --git a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart index edb6733624..276aef9f78 100644 --- a/mobile/lib/modules/home/providers/home_page_render_list_provider.dart +++ b/mobile/lib/modules/home/providers/home_page_render_list_provider.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart'; import 'package:immich_mobile/modules/settings/services/app_settings.service.dart'; @@ -44,43 +45,50 @@ final renderListProvider = StateProvider((ref) { DateTime? lastDate; assetGroups.forEach((groupName, assets) { - final date = DateTime.parse(groupName); + try { + final date = DateTime.parse(groupName); - if (lastDate == null || lastDate!.month != date.month) { + if (lastDate == null || lastDate!.month != date.month) { + elements.add( + RenderAssetGridElement( + RenderAssetGridElementType.monthTitle, + title: groupName, + date: date, + ), + ); + } + + // Add group title elements.add( - RenderAssetGridElement(RenderAssetGridElementType.monthTitle, - title: groupName, date: date), - ); - } - - // Add group title - elements.add( - RenderAssetGridElement( - RenderAssetGridElementType.dayTitle, - title: groupName, - date: date, - relatedAssetList: assets, - ), - ); - - // Add rows - int cursor = 0; - while (cursor < assets.length) { - int rowElements = min(assets.length - cursor, assetsPerRow); - - final rowElement = RenderAssetGridElement( - RenderAssetGridElementType.assetRow, - date: date, - assetRow: RenderAssetGridRow( - assets.sublist(cursor, cursor + rowElements), + RenderAssetGridElement( + RenderAssetGridElementType.dayTitle, + title: groupName, + date: date, + relatedAssetList: assets, ), ); - elements.add(rowElement); - cursor += rowElements; - } + // Add rows + int cursor = 0; + while (cursor < assets.length) { + int rowElements = min(assets.length - cursor, assetsPerRow); - lastDate = date; + final rowElement = RenderAssetGridElement( + RenderAssetGridElementType.assetRow, + date: date, + assetRow: RenderAssetGridRow( + assets.sublist(cursor, cursor + rowElements), + ), + ); + + elements.add(rowElement); + cursor += rowElements; + } + + lastDate = date; + } catch (e) { + debugPrint(e.toString()); + } }); return elements; From ab375cca1a174303d3db939c2470eaf7c0033881 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Oct 2022 15:21:58 -0500 Subject: [PATCH 05/53] Up Version for release --- mobile/android/fastlane/Fastfile | 6 +++--- .../fastlane/metadata/android/en-US/changelogs/48.txt | 1 + mobile/ios/fastlane/Fastfile | 2 +- mobile/pubspec.yaml | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 96589d03bb..9441ea911d 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -23,7 +23,7 @@ platform :android do build_type: 'Release', properties: { "android.injected.version.code" => 47, - "android.injected.version.name" => "1.30.1", + "android.injected.version.name" => "1.30.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', track: 'beta') @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 46, - "android.injected.version.name" => "1.30.0", + "android.injected.version.code" => 48, + "android.injected.version.name" => "1.30.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/android/fastlane/metadata/android/en-US/changelogs/48.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt new file mode 100644 index 0000000000..d6e2aac53c --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/48.txt @@ -0,0 +1 @@ +* Fixed parsing date error prevent timeline to be loaded. \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 19734a042b..581d7586eb 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.30.1" + version_number: "1.30.2" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 47ea6dcff7..4a47d9a2bb 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.30.1+47 +version: 1.30.2+48 environment: sdk: ">=2.17.0 <3.0.0" From 2094204877ef08ffdbcaef620dc394a8e87d1ece Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Oct 2022 15:29:37 -0500 Subject: [PATCH 06/53] Up version for release --- mobile/android/fastlane/report.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/android/fastlane/report.xml b/mobile/android/fastlane/report.xml index 33baa83495..35596dd5e1 100644 --- a/mobile/android/fastlane/report.xml +++ b/mobile/android/fastlane/report.xml @@ -5,17 +5,17 @@ - + - + - + From 536fda04f2175678a4a7a7ff716ba1290369b302 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 4 Oct 2022 15:29:47 -0500 Subject: [PATCH 07/53] Up version for release --- server/apps/immich/src/constants/server_version.constant.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index c0b1e3f43a..23e9fb49eb 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -11,6 +11,6 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, minor: 30, - patch: 0, - build: 46, + patch: 2, + build: 48, }; From 4ec3453558a73a8321cc9ac5f3bf9caec921da1c Mon Sep 17 00:00:00 2001 From: Gediminas Bivainis Date: Wed, 5 Oct 2022 12:19:11 +0200 Subject: [PATCH 08/53] chore: fix github action name --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 60a379d8d5..db79ec0677 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,7 +15,7 @@ jobs: - name: Checkout code uses: actions/checkout@v3 - - name: Run Immich Server 2E2 Test + - name: Run Immich Server E2E Test run: docker-compose -f ./docker/docker-compose.test.yml --env-file ./docker/.env.test up --abort-on-container-exit --exit-code-from immich-server-test server-unit-tests: From 5dfce4db341ce921ae41045f435e216eebde4a85 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Wed, 5 Oct 2022 16:59:35 +0200 Subject: [PATCH 09/53] feat(mobile): background backup progress notifications (#781) * settings to configure upload progress notifications (none/standard/detailed) * use native Android notifications to show progress information * e.g. 50% (30/60) assets * e.g. Uploading asset XYZ - 25% (2/8MB) * no longer show errors if canceled by system (losing network) --- .../kotlin/com/example/mobile/BackupWorker.kt | 94 ++++++++----- mobile/assets/i18n/en-US.json | 4 + .../background.service.dart | 128 ++++++++++++++---- .../services/app_settings.service.dart | 6 +- .../notification_setting.dart | 50 ++++++- 5 files changed, 218 insertions(+), 64 deletions(-) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index 24bbd1785d..116422634c 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -1,5 +1,6 @@ package app.alextran.immich +import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context @@ -47,6 +48,8 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) private var timeBackupStarted: Long = 0L + private var notificationBuilder: NotificationCompat.Builder? = null + private var notificationDetailBuilder: NotificationCompat.Builder? = null override fun startWork(): ListenableFuture { @@ -61,16 +64,14 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct // Create a Notification channel if necessary createChannel() } - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! if (isIgnoringBatteryOptimizations) { // normal background services can only up to 10 minutes // foreground services are allowed to run indefinitely // requires battery optimizations to be disabled (either manually by the user // or by the system learning that immich is important to the user) - setForegroundAsync(createForegroundInfo(title)) - } else { - showBackgroundInfo(title) + val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! + showInfo(getInfoBuilder(title, indeterminate=true).build()) } engine = FlutterEngine(ctx) @@ -154,18 +155,21 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } "updateNotification" -> { val args = call.arguments>()!! - val title = args.get(0) as String - val content = args.get(1) as String - if (isIgnoringBatteryOptimizations) { - setForegroundAsync(createForegroundInfo(title, content)) - } else { - showBackgroundInfo(title, content) + val title = args.get(0) as String? + val content = args.get(1) as String? + val progress = args.get(2) as Int + val max = args.get(3) as Int + val indeterminate = args.get(4) as Boolean + val isDetail = args.get(5) as Boolean + val onlyIfFG = args.get(6) as Boolean + if (!onlyIfFG || isIgnoringBatteryOptimizations) { + showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail) } } "showError" -> { val args = call.arguments>()!! val title = args.get(0) as String - val content = args.get(1) as String + val content = args.get(1) as String? val individualTag = args.get(2) as String? showError(title, content, individualTag) } @@ -182,13 +186,12 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct } } - private fun showError(title: String, content: String, individualTag: String?) { + private fun showError(title: String, content: String?, individualTag: String?) { val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) .setContentTitle(title) .setTicker(title) .setContentText(content) .setSmallIcon(R.mipmap.ic_launcher) - .setOnlyAlertOnce(true) .build() notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) } @@ -197,38 +200,54 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct notificationManager.cancel(NOTIFICATION_ERROR_ID) } - private fun showBackgroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.mipmap.ic_launcher) - .setOnlyAlertOnce(true) - .setOngoing(true) - .build() - notificationManager.notify(NOTIFICATION_ID, notification) - } - private fun clearBackgroundNotification() { notificationManager.cancel(NOTIFICATION_ID) + notificationManager.cancel(NOTIFICATION_DETAIL_ID) } - private fun createForegroundInfo(title: String = NOTIFICATION_DEFAULT_TITLE, content: String? = null): ForegroundInfo { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.mipmap.ic_launcher) - .setOngoing(true) - .build() - return ForegroundInfo(NOTIFICATION_ID, notification) - } + private fun showInfo(notification: Notification, isDetail: Boolean = false) { + val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID + if (isIgnoringBatteryOptimizations) { + setForegroundAsync(ForegroundInfo(id, notification)) + } else { + notificationManager.notify(id, notification) + } + } + + private fun getInfoBuilder( + title: String? = null, + content: String? = null, + isDetail: Boolean = false, + progress: Int = 0, + max: Int = 0, + indeterminate: Boolean = false, + ): NotificationCompat.Builder { + var builder = if(isDetail) notificationDetailBuilder else notificationBuilder + if (builder == null) { + builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setOnlyAlertOnce(true) + .setOngoing(true) + if (isDetail) { + notificationDetailBuilder = builder + } else { + notificationBuilder = builder + } + } + if (title != null) { + builder.setTicker(title).setContentTitle(title) + } + if (content != null) { + builder.setContentText(content) + } + return builder.setProgress(max, progress, indeterminate) + } @RequiresApi(Build.VERSION_CODES.O) private fun createChannel() { val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW) notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_DEFAULT) + val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH) notificationManager.createNotificationChannel(error) } @@ -244,6 +263,7 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct private const val NOTIFICATION_DEFAULT_TITLE = "Immich" private const val NOTIFICATION_ID = 1 private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_DETAIL_ID = 3 private const val ONE_MINUTE = 60000L /** diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c29e842faf..ff313fcbc8 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -134,6 +134,10 @@ "setting_notifications_notify_never": "never", "setting_notifications_subtitle": "Adjust your notification preferences", "setting_notifications_title": "Notifications", + "setting_notifications_total_progress_title": "Show background backup total progress", + "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", + "setting_notifications_single_progress_title": "Show background backup detail progress", + "setting_notifications_single_progress_subtitle": "Detailed upload progress information per asset", "setting_pages_app_bar_settings": "Settings", "share_add": "Add", "share_add_photos": "Add photos", diff --git a/mobile/lib/modules/backup/background_service/background.service.dart b/mobile/lib/modules/backup/background_service/background.service.dart index 1af6dc9816..f5a0086b5b 100644 --- a/mobile/lib/modules/backup/background_service/background.service.dart +++ b/mobile/lib/modules/backup/background_service/background.service.dart @@ -27,11 +27,11 @@ final backgroundServiceProvider = Provider( /// Background backup service class BackgroundService { static const String _portNameLock = "immichLock"; - BackgroundService(); static const MethodChannel _foregroundChannel = MethodChannel('immich/foregroundChannel'); static const MethodChannel _backgroundChannel = MethodChannel('immich/backgroundChannel'); + static final NumberFormat numberFormat = NumberFormat("###0.##"); bool _isBackgroundInitialized = false; CancellationToken? _cancellationToken; bool _canceledBySystem = false; @@ -40,6 +40,10 @@ class BackgroundService { SendPort? _waitingIsolate; ReceivePort? _rp; bool _errorGracePeriodExceeded = true; + int _uploadedAssetsCount = 0; + int _assetsToUploadCount = 0; + int _lastDetailProgressUpdate = 0; + String _lastPrintedProgress = ""; bool get isBackgroundInitialized { return _isBackgroundInitialized; @@ -125,22 +129,29 @@ class BackgroundService { } /// Updates the notification shown by the background service - Future _updateNotification({ - required String title, + Future _updateNotification({ + String? title, String? content, + int progress = 0, + int max = 0, + bool indeterminate = false, + bool isDetail = false, + bool onlyIfFG = false, }) async { if (!Platform.isAndroid) { return true; } try { if (_isBackgroundInitialized) { - return await _backgroundChannel - .invokeMethod('updateNotification', [title, content]); + return _backgroundChannel.invokeMethod( + 'updateNotification', + [title, content, progress, max, indeterminate, isDetail, onlyIfFG], + ); } } catch (error) { debugPrint("[_updateNotification] failed to communicate with plugin"); } - return Future.value(false); + return false; } /// Shows a new priority notification @@ -274,6 +285,7 @@ class BackgroundService { case "onAssetsChanged": final Future translationsLoaded = loadTranslations(); try { + _clearErrorNotifications(); final bool hasAccess = await acquireLock(); if (!hasAccess) { debugPrint("[_callHandler] could not acquire lock, exiting"); @@ -313,19 +325,23 @@ class BackgroundService { apiService.setEndpoint(Hive.box(userInfoBox).get(serverEndpointKey)); apiService.setAccessToken(Hive.box(userInfoBox).get(accessTokenKey)); BackupService backupService = BackupService(apiService); + AppSettingsService settingsService = AppSettingsService(); final Box box = await Hive.openBox(hiveBackupInfoBox); final HiveBackupAlbums? backupAlbumInfo = box.get(backupInfoKey); if (backupAlbumInfo == null) { - _clearErrorNotifications(); return true; } await PhotoManager.setIgnorePermissionCheck(true); do { - final bool backupOk = await _runBackup(backupService, backupAlbumInfo); + final bool backupOk = await _runBackup( + backupService, + settingsService, + backupAlbumInfo, + ); if (backupOk) { await Hive.box(backgroundBackupInfoBox).delete(backupFailedSince); await box.put( @@ -346,9 +362,14 @@ class BackgroundService { Future _runBackup( BackupService backupService, + AppSettingsService settingsService, HiveBackupAlbums backupAlbumInfo, ) async { - _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(); + _errorGracePeriodExceeded = _isErrorGracePeriodExceeded(settingsService); + final bool notifyTotalProgress = settingsService + .getSetting(AppSettingsEnum.backgroundBackupTotalProgress); + final bool notifySingleProgress = settingsService + .getSetting(AppSettingsEnum.backgroundBackupSingleProgress); if (_canceledBySystem) { return false; @@ -372,22 +393,29 @@ class BackgroundService { } if (toUpload.isEmpty) { - _clearErrorNotifications(); return true; } + _assetsToUploadCount = toUpload.length; + _uploadedAssetsCount = 0; + _updateNotification( + title: "backup_background_service_in_progress_notification".tr(), + content: notifyTotalProgress ? _formatAssetBackupProgress() : null, + progress: 0, + max: notifyTotalProgress ? _assetsToUploadCount : 0, + indeterminate: !notifyTotalProgress, + onlyIfFG: !notifyTotalProgress, + ); _cancellationToken = CancellationToken(); final bool ok = await backupService.backupAsset( toUpload, _cancellationToken!, - _onAssetUploaded, - _onProgress, - _onSetCurrentBackupAsset, + notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId) {}, + notifySingleProgress ? _onProgress : (sent, total) {}, + notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, _onBackupError, ); - if (ok) { - _clearErrorNotifications(); - } else { + if (!ok && !_cancellationToken!.isCancelled) { _showErrorNotification( title: "backup_background_service_error_title".tr(), content: "backup_background_service_backup_failed_message".tr(), @@ -396,16 +424,43 @@ class BackgroundService { return ok; } - void _onAssetUploaded(String deviceAssetId, String deviceId) { - debugPrint("Uploaded $deviceAssetId from $deviceId"); + String _formatAssetBackupProgress() { + final int percent = (_uploadedAssetsCount * 100) ~/ _assetsToUploadCount; + return "$percent% ($_uploadedAssetsCount/$_assetsToUploadCount)"; } - void _onProgress(int sent, int total) {} + void _onAssetUploaded(String deviceAssetId, String deviceId) { + debugPrint("Uploaded $deviceAssetId from $deviceId"); + _uploadedAssetsCount++; + _updateNotification( + progress: _uploadedAssetsCount, + max: _assetsToUploadCount, + content: _formatAssetBackupProgress(), + ); + } + + void _onProgress(int sent, int total) { + final int now = Timeline.now; + // limit updates to 10 per second (or Android drops important notifications) + if (now > _lastDetailProgressUpdate + 100000) { + final String msg = _humanReadableBytesProgress(sent, total); + // only update if message actually differs (to stop many useless notification updates on large assets or slow connections) + if (msg != _lastPrintedProgress) { + _lastDetailProgressUpdate = now; + _lastPrintedProgress = msg; + _updateNotification( + progress: sent, + max: total, + isDetail: true, + content: msg, + ); + } + } + } void _onBackupError(ErrorUploadAsset errorAssetInfo) { _showErrorNotification( - title: "Upload failed", - content: "backup_background_service_upload_failure_notification" + title: "backup_background_service_upload_failure_notification" .tr(args: [errorAssetInfo.fileName]), individualTag: errorAssetInfo.id, ); @@ -413,14 +468,17 @@ class BackgroundService { void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { _updateNotification( - title: "backup_background_service_in_progress_notification".tr(), - content: "backup_background_service_current_upload_notification" + title: "backup_background_service_current_upload_notification" .tr(args: [currentUploadAsset.fileName]), + content: "", + isDetail: true, + progress: 0, + max: 0, ); } - bool _isErrorGracePeriodExceeded() { - final int value = AppSettingsService() + bool _isErrorGracePeriodExceeded(AppSettingsService appSettingsService) { + final int value = appSettingsService .getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod); if (value == 0) { return true; @@ -445,6 +503,26 @@ class BackgroundService { assert(false, "Invalid value"); return true; } + + /// prints percentage and absolute progress in useful (kilo/mega/giga)bytes + static String _humanReadableBytesProgress(int bytes, int bytesTotal) { + String unit = "KB"; // Kilobyte + if (bytesTotal >= 0x40000000) { + unit = "GB"; // Gigabyte + bytes >>= 20; + bytesTotal >>= 20; + } else if (bytesTotal >= 0x100000) { + unit = "MB"; // Megabyte + bytes >>= 10; + bytesTotal >>= 10; + } else if (bytesTotal < 0x400) { + return "$bytes / $bytesTotal B"; + } + final int percent = (bytes * 100) ~/ bytesTotal; + final String done = numberFormat.format(bytes / 1024.0); + final String total = numberFormat.format(bytesTotal / 1024.0); + return "$percent% ($done/$total$unit)"; + } } /// entry point called by Kotlin/Java code; needs to be a top-level function diff --git a/mobile/lib/modules/settings/services/app_settings.service.dart b/mobile/lib/modules/settings/services/app_settings.service.dart index 469e6a0d43..292c40c210 100644 --- a/mobile/lib/modules/settings/services/app_settings.service.dart +++ b/mobile/lib/modules/settings/services/app_settings.service.dart @@ -6,7 +6,11 @@ enum AppSettingsEnum { themeMode("themeMode", "system"), // "light","dark","system" tilesPerRow("tilesPerRow", 4), uploadErrorNotificationGracePeriod( - "uploadErrorNotificationGracePeriod", 2), + "uploadErrorNotificationGracePeriod", + 2, + ), + backgroundBackupTotalProgress("backgroundBackupTotalProgress", true), + backgroundBackupSingleProgress("backgroundBackupSingleProgress", false), storageIndicator("storageIndicator", true), thumbnailCacheSize("thumbnailCacheSize", 10000), imageCacheSize("imageCacheSize", 350), diff --git a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart index 1643a830b5..be988e01cb 100644 --- a/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart +++ b/mobile/lib/modules/settings/ui/notification_setting/notification_setting.dart @@ -15,12 +15,20 @@ class NotificationSetting extends HookConsumerWidget { final appSettingService = ref.watch(appSettingsServiceProvider); final sliderValue = useState(0.0); + final totalProgressValue = + useState(AppSettingsEnum.backgroundBackupTotalProgress.defaultValue); + final singleProgressValue = + useState(AppSettingsEnum.backgroundBackupSingleProgress.defaultValue); useEffect( () { sliderValue.value = appSettingService .getSetting(AppSettingsEnum.uploadErrorNotificationGracePeriod) .toDouble(); + totalProgressValue.value = appSettingService + .getSetting(AppSettingsEnum.backgroundBackupTotalProgress); + singleProgressValue.value = appSettingService + .getSetting(AppSettingsEnum.backgroundBackupSingleProgress); return null; }, [], @@ -42,6 +50,22 @@ class NotificationSetting extends HookConsumerWidget { ), ).tr(), children: [ + _buildSwitchListTile( + context, + appSettingService, + totalProgressValue, + AppSettingsEnum.backgroundBackupTotalProgress, + title: 'setting_notifications_total_progress_title'.tr(), + subtitle: 'setting_notifications_total_progress_subtitle'.tr(), + ), + _buildSwitchListTile( + context, + appSettingService, + singleProgressValue, + AppSettingsEnum.backgroundBackupSingleProgress, + title: 'setting_notifications_single_progress_title'.tr(), + subtitle: 'setting_notifications_single_progress_subtitle'.tr(), + ), ListTile( isThreeLine: false, dense: true, @@ -53,7 +77,9 @@ class NotificationSetting extends HookConsumerWidget { value: sliderValue.value, onChanged: (double v) => sliderValue.value = v, onChangeEnd: (double v) => appSettingService.setSetting( - AppSettingsEnum.uploadErrorNotificationGracePeriod, v.toInt()), + AppSettingsEnum.uploadErrorNotificationGracePeriod, + v.toInt(), + ), max: 5.0, divisions: 5, label: formattedValue, @@ -65,6 +91,28 @@ class NotificationSetting extends HookConsumerWidget { } } +SwitchListTile _buildSwitchListTile( + BuildContext context, + AppSettingsService appSettingService, + ValueNotifier valueNotifier, + AppSettingsEnum settingsEnum, { + required String title, + String? subtitle, +}) { + return SwitchListTile( + key: Key(settingsEnum.name), + value: valueNotifier.value, + onChanged: (value) { + valueNotifier.value = value; + appSettingService.setSetting(settingsEnum, value); + }, + activeColor: Theme.of(context).primaryColor, + dense: true, + title: Text(title, style: const TextStyle(fontWeight: FontWeight.bold)), + subtitle: subtitle != null ? Text(subtitle) : null, + ); +} + String _formatSliderValue(double v) { if (v == 0.0) { return 'setting_notifications_notify_immediately'.tr(); From 854c214bc09ec7173dc8e56ca127711c23fee063 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 5 Oct 2022 22:18:57 +0200 Subject: [PATCH 10/53] Fix: Use boolean comparison for DISABLE_REVERSE_GEOCODING config (#787) --- .../src/processors/metadata-extraction.processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index 0860fcdd49..a06972e042 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -96,7 +96,7 @@ export class MetadataExtractionProcessor { private configService: ConfigService, ) { - if (configService.get('DISABLE_REVERSE_GEOCODING') !== 'true') { + if (!configService.get('DISABLE_REVERSE_GEOCODING')) { Logger.log('Initialising Reverse Geocoding'); geocoderInit({ // eslint-disable-next-line @typescript-eslint/ban-ts-comment From 7587f858ae37d2326cca197c70689055a784af54 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 6 Oct 2022 11:25:54 -0500 Subject: [PATCH 11/53] feat(server/web) Add manual job trigger mechanism to the web (#767) --- mobile/openapi/.openapi-generator/FILES | 14 + mobile/openapi/README.md | Bin 8635 -> 9153 bytes mobile/openapi/doc/AllJobStatusResponseDto.md | Bin 0 -> 855 bytes mobile/openapi/doc/CreateJobDto.md | Bin 0 -> 424 bytes mobile/openapi/doc/ExifResponseDto.md | Bin 1263 -> 1260 bytes mobile/openapi/doc/JobApi.md | Bin 0 -> 4228 bytes mobile/openapi/doc/JobCommand.md | Bin 0 -> 376 bytes mobile/openapi/doc/JobCommandDto.md | Bin 0 -> 431 bytes mobile/openapi/doc/JobCounts.md | Bin 0 -> 515 bytes mobile/openapi/doc/JobId.md | Bin 0 -> 371 bytes mobile/openapi/doc/JobStatusResponseDto.md | Bin 0 -> 457 bytes mobile/openapi/doc/JobType.md | Bin 0 -> 373 bytes mobile/openapi/lib/api.dart | Bin 3089 -> 3326 bytes mobile/openapi/lib/api/job_api.dart | Bin 0 -> 5114 bytes mobile/openapi/lib/api_client.dart | Bin 13168 -> 13670 bytes mobile/openapi/lib/api_helper.dart | Bin 3658 -> 3844 bytes .../model/all_job_status_response_dto.dart | Bin 0 -> 6910 bytes .../openapi/lib/model/asset_response_dto.dart | Bin 8733 -> 8545 bytes mobile/openapi/lib/model/create_job_dto.dart | Bin 0 -> 3281 bytes .../openapi/lib/model/exif_response_dto.dart | Bin 10335 -> 10263 bytes mobile/openapi/lib/model/job_command.dart | Bin 0 -> 2550 bytes mobile/openapi/lib/model/job_command_dto.dart | Bin 0 -> 3307 bytes mobile/openapi/lib/model/job_counts.dart | Bin 0 -> 4414 bytes mobile/openapi/lib/model/job_id.dart | Bin 0 -> 2870 bytes .../lib/model/job_status_response_dto.dart | Bin 0 -> 3722 bytes mobile/openapi/lib/model/job_type.dart | Bin 0 -> 2953 bytes .../all_job_status_response_dto_test.dart | Bin 0 -> 1312 bytes mobile/openapi/test/create_job_dto_test.dart | Bin 0 -> 559 bytes mobile/openapi/test/job_api_test.dart | Bin 0 -> 956 bytes mobile/openapi/test/job_command_dto_test.dart | Bin 0 -> 565 bytes mobile/openapi/test/job_command_test.dart | Bin 0 -> 417 bytes mobile/openapi/test/job_counts_test.dart | Bin 0 -> 930 bytes mobile/openapi/test/job_id_test.dart | Bin 0 -> 407 bytes .../test/job_status_response_dto_test.dart | Bin 0 -> 687 bytes mobile/openapi/test/job_type_test.dart | Bin 0 -> 411 bytes server/.dockerignore | 2 +- .../src/api-v1/album/album.service.spec.ts | 3 + .../src/api-v1/asset/asset-repository.ts | 30 ++ .../src/api-v1/asset/asset.controller.ts | 4 +- .../immich/src/api-v1/asset/asset.module.ts | 4 +- .../src/api-v1/asset/asset.service.spec.ts | 3 + .../asset/response-dto/exif-response.dto.ts | 10 +- .../immich/src/api-v1/job/dto/get-job.dto.ts | 21 + .../src/api-v1/job/dto/job-command.dto.ts | 12 + .../immich/src/api-v1/job/job.controller.ts | 43 ++ .../apps/immich/src/api-v1/job/job.module.ts | 82 ++++ .../apps/immich/src/api-v1/job/job.service.ts | 180 ++++++++ .../all-job-status-response.dto.ts | 35 ++ .../response-dto/job-status-response.dto.ts | 6 + .../response-dto/server-info-response.dto.ts | 6 +- server/apps/immich/src/app.module.ts | 3 + .../schedule-tasks/schedule-tasks.module.ts | 12 +- .../schedule-tasks/schedule-tasks.service.ts | 20 +- .../microservices/src/microservices.module.ts | 28 +- .../src/microservices.service.ts | 16 +- .../processors/asset-uploaded.processor.ts | 13 +- .../processors/generate-checksum.processor.ts | 6 +- .../processors/machine-learning.processor.ts | 60 +++ .../metadata-extraction.processor.ts | 56 +-- .../src/processors/thumbnail.processor.ts | 31 +- .../processors/video-transcode.processor.ts | 4 +- server/immich-openapi-specs.json | 2 +- .../job/src/constants/job-name.constant.ts | 11 +- .../job/src/constants/queue-name.constant.ts | 13 +- .../interfaces/machine-learning.interface.ts | 8 + server/package-lock.json | 69 ++- server/package.json | 2 +- web/src/api/api.ts | 4 + web/src/api/open-api/api.ts | 411 +++++++++++++++++- .../admin-page/jobs/job-tile.svelte | 52 +++ .../admin-page/jobs/jobs-panel.svelte | 138 ++++++ .../scrollbar/scrollbar.svelte | 2 +- web/src/lib/models/admin-sidebar-selection.ts | 4 +- web/src/routes/admin/+layout.svelte | 3 + web/src/routes/admin/+page.svelte | 16 +- 75 files changed, 1286 insertions(+), 153 deletions(-) create mode 100644 mobile/openapi/doc/AllJobStatusResponseDto.md create mode 100644 mobile/openapi/doc/CreateJobDto.md create mode 100644 mobile/openapi/doc/JobApi.md create mode 100644 mobile/openapi/doc/JobCommand.md create mode 100644 mobile/openapi/doc/JobCommandDto.md create mode 100644 mobile/openapi/doc/JobCounts.md create mode 100644 mobile/openapi/doc/JobId.md create mode 100644 mobile/openapi/doc/JobStatusResponseDto.md create mode 100644 mobile/openapi/doc/JobType.md create mode 100644 mobile/openapi/lib/api/job_api.dart create mode 100644 mobile/openapi/lib/model/all_job_status_response_dto.dart create mode 100644 mobile/openapi/lib/model/create_job_dto.dart create mode 100644 mobile/openapi/lib/model/job_command.dart create mode 100644 mobile/openapi/lib/model/job_command_dto.dart create mode 100644 mobile/openapi/lib/model/job_counts.dart create mode 100644 mobile/openapi/lib/model/job_id.dart create mode 100644 mobile/openapi/lib/model/job_status_response_dto.dart create mode 100644 mobile/openapi/lib/model/job_type.dart create mode 100644 mobile/openapi/test/all_job_status_response_dto_test.dart create mode 100644 mobile/openapi/test/create_job_dto_test.dart create mode 100644 mobile/openapi/test/job_api_test.dart create mode 100644 mobile/openapi/test/job_command_dto_test.dart create mode 100644 mobile/openapi/test/job_command_test.dart create mode 100644 mobile/openapi/test/job_counts_test.dart create mode 100644 mobile/openapi/test/job_id_test.dart create mode 100644 mobile/openapi/test/job_status_response_dto_test.dart create mode 100644 mobile/openapi/test/job_type_test.dart create mode 100644 server/apps/immich/src/api-v1/job/dto/get-job.dto.ts create mode 100644 server/apps/immich/src/api-v1/job/dto/job-command.dto.ts create mode 100644 server/apps/immich/src/api-v1/job/job.controller.ts create mode 100644 server/apps/immich/src/api-v1/job/job.module.ts create mode 100644 server/apps/immich/src/api-v1/job/job.service.ts create mode 100644 server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts create mode 100644 server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts create mode 100644 server/apps/microservices/src/processors/machine-learning.processor.ts create mode 100644 server/libs/job/src/interfaces/machine-learning.interface.ts create mode 100644 web/src/lib/components/admin-page/jobs/job-tile.svelte create mode 100644 web/src/lib/components/admin-page/jobs/jobs-panel.svelte create mode 100644 web/src/routes/admin/+layout.svelte diff --git a/mobile/openapi/.openapi-generator/FILES b/mobile/openapi/.openapi-generator/FILES index a5d73e1474..3a8edc0030 100644 --- a/mobile/openapi/.openapi-generator/FILES +++ b/mobile/openapi/.openapi-generator/FILES @@ -8,6 +8,7 @@ doc/AdminSignupResponseDto.md doc/AlbumApi.md doc/AlbumCountResponseDto.md doc/AlbumResponseDto.md +doc/AllJobStatusResponseDto.md doc/AssetApi.md doc/AssetCountByTimeBucket.md doc/AssetCountByTimeBucketResponseDto.md @@ -33,6 +34,12 @@ doc/DeviceTypeEnum.md doc/ExifResponseDto.md doc/GetAssetByTimeBucketDto.md doc/GetAssetCountByTimeBucketDto.md +doc/JobApi.md +doc/JobCommand.md +doc/JobCommandDto.md +doc/JobCounts.md +doc/JobId.md +doc/JobStatusResponseDto.md doc/LoginCredentialDto.md doc/LoginResponseDto.md doc/LogoutResponseDto.md @@ -59,6 +66,7 @@ lib/api/album_api.dart lib/api/asset_api.dart lib/api/authentication_api.dart lib/api/device_info_api.dart +lib/api/job_api.dart lib/api/server_info_api.dart lib/api/user_api.dart lib/api_client.dart @@ -74,6 +82,7 @@ lib/model/add_users_dto.dart lib/model/admin_signup_response_dto.dart lib/model/album_count_response_dto.dart lib/model/album_response_dto.dart +lib/model/all_job_status_response_dto.dart lib/model/asset_count_by_time_bucket.dart lib/model/asset_count_by_time_bucket_response_dto.dart lib/model/asset_count_by_user_id_response_dto.dart @@ -96,6 +105,11 @@ lib/model/device_type_enum.dart lib/model/exif_response_dto.dart lib/model/get_asset_by_time_bucket_dto.dart lib/model/get_asset_count_by_time_bucket_dto.dart +lib/model/job_command.dart +lib/model/job_command_dto.dart +lib/model/job_counts.dart +lib/model/job_id.dart +lib/model/job_status_response_dto.dart lib/model/login_credential_dto.dart lib/model/login_response_dto.dart lib/model/logout_response_dto.dart diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 0d037e60b3f81d7d730feb9d662e2f314489add5..3154654d42e96eaef82b230db6e9b5b72552c705 100644 GIT binary patch delta 493 zcmdn(e9(QvV&QtP{3OSMOf7{Pg=j6U^wbi^oE)G~ad1guNolc`R;)%!ezLwkM3r7{ ziZW11VopvLP)RXZi6&5|mX^C~h?bUuK1dkI=hA|xN0huLVkAf-cH{J`fpVTH zwV1{hr{<*q&2`St%}vZp!Qxbq0-)w(hysX1147{r1)IzaGT6 zlg!H`935EcbY@UNmE~3SfU75xREH@9hc>`~kg+7;Bb18M26#*5PL{874Q#nwD(Q)7 z|PA#|=AYO6HyanqBAz{b)5|KPKC*9-#)rPq!$+K9$Tk&zy>xOjll z^e@bWpiOO7R_f{wEO^;SKh{vgE!LKVhzfHdqURO|Ar{>)kQvpB`>{tI10Lx6s&TnEgebhF8pzMk1_TMqaGBR literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/CreateJobDto.md b/mobile/openapi/doc/CreateJobDto.md new file mode 100644 index 0000000000000000000000000000000000000000..64cdbf0184ff4253fb01d4e27533be5bb4de9761 GIT binary patch literal 424 zcma)2!A`?4488j+EcLK8Qo1{w2HgR~rU`MYDm8072rPA!ngbFaPtsLlaDgaN{OsrF z_!Y>31`}N6+0v=Hjbw!G`A`c36a_x8u_!NuQyqip-^?>X*LBuJ0p|lp!Nr`vn$=cW zF%uTonK~)$V=6{*G{Vk!!cTnq2>GdL?4t|fct~i+k{J5pq~rzC`Y#Mhz49T8E0ws7 zQfZ$vY?pIbd0VDhx&TdcleR)(+-48(g{58A;fGvyaimm=V>OQ2N9ZE4A>E}d7lrUpQ3SL0ftYn>T+gIk3jFu|j>jZU zSXz+iL$_Fpk~#M|moLX1vLzc`=81fMvLCj~T&Yfnez7_uKV{lJ+@(^dAN3`x^Yime zS6=0_(pH|j4Sl1rF%TOf(A^YKrM8QLj>o%>bj4J0p?&@bleZL$Xspy8`>D<7mflY$ zhmiPv&d%ew!!E7z(&yHwe)|EheeAEF)kz#*vWH{b#T|L==Pr&w6mEu*=i)hgK!#nA&$f2 zMI3EQC<%gmcrmp_;i@6dF}u$XB_!q>25=2E2?f^t|wBw_%a9 z@rlF>JNTKWwV@y0dtU|*!q|$s!Em>iILM|cJ6Btay1yPAkI%M;!_m>v+2r8o(SEnL zx$yse@c-l7q!J(fInC24(qp*NUb+zhB_iEKcx~rZZTDRevDLk5)T0|pl7#MF_yUi+ z#68qSZgfEyn6uoV>rBe@hR{^VT_k*8UDJ&sJVCNzhs;8zty1T=Jr$xVIwi{Dqo4!Q z9U=S3PsF%vOQ1XYlyW_gNFWh2h5tNEp&ii++W* z&{wBXk_^V9?VY`m;6xxc8Y}QLWu3_t6!kHb7zvN2?N1O{9BI<@q| z(q&mR%{{nsfHrk<@)po4i0Ey3dKz_lv?K}g7P<4FuSAyFbUuU2dP@2iw(6tM{rgB_Gci}db7MVL@n(96FjMX02N}Fsm51pA0ZquQx_S7^;yvsXM=0$w4njpz??^LdcBzRLmh%zQ?xzm|*b+O(zzrJ<{9 literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/JobCommand.md b/mobile/openapi/doc/JobCommand.md new file mode 100644 index 0000000000000000000000000000000000000000..620e0439a58d88370631213520ebcf0a2a47dd83 GIT binary patch literal 376 zcma)1v2Fq}4Bh<|mTmxsguP6(+(1COo=M zw%eiB<8?iqEo$v;Wx4(F6vPx?wdbVzgxv`LuBWT{KW}qhMZrdPLGC1e4Ij)$0B`|| Cp>w|g literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/JobCommandDto.md b/mobile/openapi/doc/JobCommandDto.md new file mode 100644 index 0000000000000000000000000000000000000000..4e87fde8e894b9a9f0e4b5e3902ecca528d0460c GIT binary patch literal 431 zcma)&O-lnY5QgvbD+cz^ZXoGhPu1mTfJxqgm?1H zJ0BHNv_VIwJQ;gZTY9-C?{zQ@6F^nrBWZ*DZgB!I>fxy0EcAVEbYgHmQes?O;x`w& zRk19<;WVSLie0UpJmtN&7B{^+n dtZ~j>A0Gahn|CB_Fp*r~9~S3=zlzrY@C`v2g%1D# literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/JobCounts.md b/mobile/openapi/doc/JobCounts.md new file mode 100644 index 0000000000000000000000000000000000000000..195ed86a6528371228670a047b9b215f8ba0e4fe GIT binary patch literal 515 zcma)3!Ab)$5WVLs2KG=JNP5>(wLK`d6!BIH8#mJ$++;#_Rw?-LCTqb(S~Qo*oA>4o zys3a91{0n3WUiTHfIJ*uqyFF>pI@;uC$&#E$55X vQhQrjYI=`>P02v*DXAgtZVErs^UeJ~XLI%ngN@|!5yuwa3x6%23L(A#!4BY({mfipi2|G+67^ncDiVY#X@M0h;iK9GsK;q*`UXcnctrzR;^PR6k zjucFE+OwsL{@}yH0#p?)*;te_VYk3&nlE!AsO#FAB;b7HB-ne*M-Q1{kt&4VxKKN# zP0Gt8j#^k7Pk6)aHI_D`wn;9E3=j>Msni+fDPID2%HT2QRLb{l2gD y@N+(yt>)U>%5wAVDTpb)>c~lT3EKhwUQafQf8OT2ih_;og1kul7`~Yg0N@Ubk8!O4 literal 0 HcmV?d00001 diff --git a/mobile/openapi/doc/JobStatusResponseDto.md b/mobile/openapi/doc/JobStatusResponseDto.md new file mode 100644 index 0000000000000000000000000000000000000000..13325a5152d58a74153f27d66a95969c21321b5a GIT binary patch literal 457 zcma)&F;BxV5QTUD3QHMEEG6Ax3Zw&wma4EtmEyz)7!secFGxuIc+Q~`r3-km-}$?D z_RA!CaN65)L<2*4gWWx8n&J+kM;D&whT- zuR@L#Omy0_rHfwk+v{f}fU3d`8;kNzIIb|7{%lSJUDsKY1e}kY1ZTJT;wCgNVudh! zEYwkHpYk$^qXBlt6MkVa$!$qPpIj8jA)_5jW~fQ!;RGr57Y1nCr~D@h+<$lqVv4Uia#CZ$p~m}lvw!~QZO*GG*vKx(4~eVcFY_7zz5(kF Ba-skL literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 3c87fc703b255eb136c3a2ee7c0c8a233a76b48e..150d878f63ef12603bdc5dcac6987fd822da16c2 100644 GIT binary patch delta 113 zcmbOz@lSGt5i@gEe$r+cW^qQ|oSb+dJHEIiv81$kvmnbaW^NEaIX^cyF)wBEM0ODr k{zoPl5*sIET0Czf~7p9--}wUHc6P)@K%nyDTkXHpD2dVnbxEP--vE#X#M1o99S0ld(_1iTrIhERXq8$oL>$GUD$ ztC-z1XlmLl&c2Z+vGI7uNy!|D&7R&CM3q zzuoE-J8yxWhN3-MrRYwOE40>a93cmP9l(%_Px24!4*Lza4q%Z9@^>4=dNJ5=u9zx7 zD8`$96?5z37c-lO47u(tQnP{%h%8BnnUz(SQ(mrVF}bx~F9*O(OmK8B$tNTo=+HXjGq2l_@49j7bG7 zDQ{?#Kd`MI2hV>DU`hLDj6fICP~KaEz>Q5rr@3ofiWi|C38r!B= zkva2$43HY{A{BtA7Xm-0lqMJhQpS@s7gRi-7OGE)(#(`dz|uNl)XM3X#aT!bI&Nr} z9{ymJ2w3NcL1)r<5f2vt%6{Uy^d{%R(8(NO*FV@ISF@OHIQQ zWp{7_sPAB9J+S2pcC31GIdptr^8?wGC~1FBnQh%rvF*pV4==Y)VcEahWS(aH6TX8> zD!cvo3Y=EUdf)e&cHg!_P1`;FD@EEV@j4s=|5Cv&opqQ2gbLGyO_1l@3e<&y8r+H literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 13cc02896776f87cf2db7641fe9a1f45e11b2b23..827332e9c342f2cb274ca7de8fbb0e68dc4d30de 100644 GIT binary patch delta 198 zcmey6_AF~diVSy7j#qwCa7kiGY4K!5C3zl4WP$z4+LQU^SSQ=bvTx3p@#NqFs&&rK z%}vZpnS4-I1erTgP9K^3P(~b?>!_;C3D#ShS5gepio`RNR$v9nd4i3EvVf+6R3O}i Q!~;1>K$Ug#UX{&!0C=E9p8x;= delta 25 hcmaEs^&xFTip=CWvfPvRs&H+-Eo0BId7au0J^-7r3hDp= diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 26c90fd0c7e63f0ec053fd0dbe51b1d15c8284d3..7db37768c4fd14966fc090f0a27280abc29c9387 100644 GIT binary patch delta 82 zcmX>l(;~ManO)f{Kgl^iH#adaMN^@gO92RqQcFsU@)QsflP9p-O9Hu`Sd@6C0A(FE Jr?5X`0{~(*8yo-t delta 12 TcmZpXJ0-IrnSHY?$8$CS9;5_l diff --git a/mobile/openapi/lib/model/all_job_status_response_dto.dart b/mobile/openapi/lib/model/all_job_status_response_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..7be7166a77ee9491c708eb93d37ebb66c040ae62 GIT binary patch literal 6910 zcmds5ZExE)5dQ98aVdh@!CYtUry;G=28}!PB?*>h1qQ<~Xqk@L%A!Y7$&57reRrfJ zOBP9Q{W4$$8r!xreFRRtF=_dE5Xux zw$amdZl;R=nG1#D8C&7sHBc& zn_04K`peJLWXY87bnrL>ashJ9Gqx53{Qt7kNm8aYoTcdpIs0sw$@MkYYdn;n8wn2` zfMc}7H8;7+G%(2cJ&XhF6A)I}gbz?Iz|BAGGEOyCemMd#uYO{X20b; z=Wk_>R`v*>-PoKNHu}pbmh9)hL-WbEGP^@-?+0he5)Ju@GnI+#Hm-@#H*Mw5P^CNW zQ(Wkamg+I?FHOZ`tQSqSx2%sx=x6}+u(hW~n@7yO!yB{o4r|cJJAA9AqFeh~zL}IM zM66n83fZ#G`=zOb8C^B{~ z6$uZ5iIJPygwxUCgaWLkk2lI3hM&~Zqj`0gy@e7!QY%13X5f|^STenQE9V?T(gs!4 z#;AK`JmFe1wW02V4Q0u-(3TB@Hc}J@CF&9U$=kxF|=uAAZ8LHdT zj>m;a{aC&Z4SRh!m7{e}DFgPqy zkh%*Kj25R*)8gAO+l7e)j|Ga*+fbo!Dv(0tHjp>O3KTD`4dE?2r6Nu1xIlFf$`0}_ zkm$)H4Te85;RDO&;MN`u!f3c-Y0fn)lw1{%WBUk~f(0^kX?Yl9Hca}Ea|tH=2?+IGsntsNqi)1o;M%LVgdqHq` zuY~3R{M6WV)@y_k8E#%FXBrhm+CiZP?tomGUR9AuwNl@Yq%Nv3!l>%?Q2DK;h^-JM za}CP-qKIYeL-)Gac>x9CNvO>oB6aeFG+ha;>1Gs1@Zs~-<)DjZri0wS+HqqNf`g%r zr+<4C&NH;lQY02pkhPDa#i=~F$Q+|`UOs&69c@FPpa*W)t8k)~`DDAK4afpEe95%? zi~iApTQ^DHSiO!N5tHjq>Lcnm%$)Wi;!{up*psEHk^_a~IHU#N;*Ep9ez{)#rf z(;ivB52_oH?A9$4o;vm(5NaSh_}&h&7u+kggY0c8dy&0THRK|R9&HK~X+nut^`D}U ze-c<^1Nn3M8d4;`D|4bO>CQV3b~(RQ@}A97Tzp``QqB91Wu)Zl zZR!n#Oa+Ow!fg-e_dsZ@)}z(h)QTWg=U#Dwyr&9FtmV;^j*jTc%rMliw)u|NsMVcq zpDHR{9hs^$U24GQbJxvklFs1N<4K8z`3or{>2|%+a3@fHvnX~pD13trvg&^!>rwuA z9aL)U>_CmNHsyxU%Bi>R3)CX&3hT3>b5*EiQ?GWfbx)vr13SgGf)DAPuG@yuX+(`R;9!e4u<=lH*B(cs*~U)Lwmp2|LZzx~MWuhs&xW^Aw8}NSJ^rNL zXN@J@ORRfgmiUNLj9w83eDch3qi`|q;Ua@l^vaw-G0Z&CLJ<~BxKolN4sYD_;h!Zo zV$>rle&YWxuE=YZ{c5q*uhMy&*!}+?;Ev%2=V8*4Vyhf0X&|J*7{}#Rj-(j}9Q`%w zPnpKtFt!aTyAM>tA4#?g3lsx)+q=qte{v9UroJWP7gO^-q^6P!piKTSs0ND?c>ZkGu= zkFUpVFa{pF2|^CC)4=PlV`n{h@!%gYM3?R$_}~j)KD_sRy!YwZ`P}hG;Su>RZ^izG z(==nxk8BEJkB1g@yEqkeTngIoe^T{bs@KzQDwi2bVhzh;32#LmhhhbPL<0|U6R>jz zmQ(HLbbL?sJJogWF`h6TFWGZ^WyY`>ceZ)eb0Tj(aJfYlhBH>gUp5X+dMKDKZ~Ge@ zC20f=X$+w>gPx=b=Ig-qH+>vS1zbp@$jUX7VV`oX<@%m#!^kdB{`Ah-xX~t)E`7;l*gui0i(CKz delta 507 zcmbV}J1j$C6vtB<_f@wgeFzt)p0~6r)j>QGuSsGv*yPenue80=Tbf1?%%*=gwMZmJ zauX{nu`w`OED~n1_)3u&o#fQ8}S0hq6P~sZD%X6+BcO2oBQ&7Xl zFu-?VmRFJI`*FmFamDxGg{e;nV?i^}eKLB`3{< z9->~h8Med*6vZHHu?aV#f+vySlm;;*H7m@LHWS8@p(n<3e~qR@*q7qCkQ6*f@oEO+ zRPdtB6~T-vj&0X$%xJe+=MeZfNME|H#rE!-Hvhh37*}ZsP}YZ*i*6}JRQzJ zG)kIf>DipBhG^sKK`0u9?CYvxGT<}Ha^N%S7)i7l8vbS6em`5ZJN$WP?6aFFWVE!g zNGq0^30I~qm}ZKWvu0jfNa-|6OIpg(!wk1C8~3XJ<4d;GI)ar`l>bl9b9?}SjgD^s diff --git a/mobile/openapi/lib/model/create_job_dto.dart b/mobile/openapi/lib/model/create_job_dto.dart new file mode 100644 index 0000000000000000000000000000000000000000..1eaf6786479c9883b56df79d69e93a2629e552f5 GIT binary patch literal 3281 zcmds3TW{Mo6n^)wI4O!)0Tg-7(~!(*i^d(AwlR=q4})O{v_wZtWl|%l8b<2>zH@ky z<;LCeHed#9OXRtq?>ppZG#HKG?H_m3)88jIliRmcjg# zk5G&x-;_+-@mcojY(TH#UMkJ=rPBFQ)chP;*&1Hvyyhz|Z5rRjs!`e=)L`|RZA{*@ zHrM>GR%le0Y=yrK)A(<>HfUU%!|H{S#xiMhQKCb!6kI!XbI@5WBsY1jlYku=eDU=(8lPE6H&b3hzt@su!g=97454Z+ncq0C|z3>I`A~QV(upN$Y;vII{M1Hu~k#_EOfj}`iK`2MUSW)E2txA>D4ZpC5)7Q?wD_AI?k1JLWLrGjz z1SAY5t^@eGhO#=5tYF4IDRAN&PHe!SC(^WfFhM$`R6r7K(}&UwH{QohWuu3cG;?-zcp2f)lc&lBZO{AI+VSIn1jdSmVDz zAT|w}5)zqPb7n9=+&c5M43Kv6|u%b#541QNB5(}zSt}Nz@Do!wk z;cXa+fQs;95Be{0Y5s!3TM1(*9YPFTf0^E7L#!=X_n)4R87aZh*dgax_K}`6;RtA< z-}0#snp)9YV1v~ykBi&nWY_Gl-_IrKlmoBM;{ePkCx=3+P@jY+M>HDu?U;fo<0}!~RY>_u#ah@3 zmxwELLF!lk)Qg}-IJfYWMNzsXk|l)B0R#|g_UUp-!nAx43(w^u+UTR?fSdmbR;UwQ zrFwWtI|lB!nEnSK8o?bN;S?p|C7y(IhS6D$C;Y0#tpx|Xq8N;yD8u}zvO|o!w{*%M WNw*sTmyd2JbyqTXbG{hh$oU(!q7qyH literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/exif_response_dto.dart b/mobile/openapi/lib/model/exif_response_dto.dart index 199c955e93d5753a09069a8136a6f45a46383719..b81f0e347be97d391f6db148c43efb4ef03b83d5 100644 GIT binary patch delta 176 zcmcZ~Fg;+yWX8!q86_uAW=xwb#Ppv(Gq1#6Av49Ai%UTP$e!HGEIV0)Icf7E=824x zKd|agHfG(sS)R?FX>uB;-R3wB4#v$(I4?3ye$JyXxs<19Gdu55#>s|)y_?VQr7~^a zC%A!A0BDSzMpkito{mD1dS;6HWP4d@t=zbyj+sg$2b=5wT delta 166 zcmbOpa6e$fWX8#B7}F+CW|W*P#PpvlxTGjEFWp`tGi9;?b3SukY3^hNX4%clnL8OL z$FuI8Y{06&S%%G?Y4c1D4#vsR9Cn)*a$aPbJc*}hauJWhW)|L~jGM#wQ<)~8 diff --git a/mobile/openapi/lib/model/job_command.dart b/mobile/openapi/lib/model/job_command.dart new file mode 100644 index 0000000000000000000000000000000000000000..27340280765d97906b1658ed46b80e7e4d33e3e6 GIT binary patch literal 2550 zcmai0+in{-5PjEIOn@Sm0d>{psc@<&MlH7vQoCs6he8k*)GlSQ*(J9m*MboE-+PAK zn`PBCU}-PIxtud|M$_qNI;G3|<=ywcE^ZfpUM?3`bba%AF`=tFy1iS{?{`->*MGf% z8C!nLh4r&{$&c?we5)R`u`=Bnn{HJhFR0eFlX)r&SxW6E?Y$~1WBth!qS%XrORL(a zR{m2fi{o09_^gD*x0lwzxpk+HXGS|Ov`EV+?Z8=*j{Q!#Bv_myTy(v}@mlB776+vtD7@v=*fvHf zWx0`-U2c^Y1%2_hvkY!Nnn!6Z;=`nQ=}o*CpVRC^bQ(;%*Poox$1ee{S6P!dZh+D3 zc>(q;JU&DEL`UYPI%RDtHg0PDtXzOy6a`hrIkhQdOwN(gvE&sayI%Dq1Tde)3K%&= z`?{{) zU7;ebRc&d^&IpYXkbV1>_&!nY*7!wP`9tds{*0w@>>tErwAr+O-$so6_l))qCmamK zbMP2GTECf*u8U&mj3}m_h&=D0isy))Li+7@ytC5RR=20T;ey63JQp2MA^y|oi4|!| z9Wm%z;1I_bqwXman$Sti)S7Uu!T@KICu`VSR4s|%dQ#x2+6qbZ{p&DNbp4z zr*2-aLaRGS-m1kSFuq=~@tWZfgR)qnKui3j8z7BAH~U-k#+sYd+Zyr-gjq6AdjxOV zc-lycB;@4kER)V{;pD*M%)H~g4lT!CW9P^`q2J72LJ2248AqYRbiE$C@%=v;#MstTi?kxx<*~=TNZ5A0>o54}_(ZA860jz~ki~H-!C!eFemU(qPfVIYY4+ zCu|wavXv!NKW`CXdu(G13oD__3JP00UK4O^3OK8(eqHec9u3a2Z-se-`MBkpNp6*@ zV-jM-Ja$6GD#V8LLIoVwW44QLlYSu$Wg-(!QoB?x8Y%}VC6HF@qujB2boN*)r_D{( zTjz2sAfFegvi;+1-*mMMQ(+6w+|i|dsQDL7;BR3=CFFMq_IreOcdx)tOVCi-5hjQ3CT@XDS3}#wq!E< z^_MI!nKpv~y0f5GplU8zEmHhm3{dXS^b;W8bPr|T8aD;4P6^0(TuOdN=Q~g_7&G)0#C#rb{8HIFEZD|;9|Hy@$l?L zwBv)9qm><%@%cmnq!;*3F(9mg@2qOLft6P4U})}^kXtJh7!vd5nBrmtvZ;^>RQ8Q9 zj&dFRYt2{4ch@Sq$&o82BNk=YIacHuHd(0(s)t|b)Ah9rz(E!u^npeEFqXuHMP$Ng z;yQpwHUR5H(wrIl1meWkoSFioo=Vebj$Oel{%}~c8ex`}VMDovwsF4kt)cD;iZ{w~ zcy;b%0(3;Sp@qEU`8Qqw&MAsr6vA#{$+rptUveUsRPvNCd~EKF%wb+RF&qC4a@y5Vtgmz?a&9rkc?66{`#%?P3`{-8DB&WoeT1P0tq| z0csbrN7bST;P@EmJrxECJYCne!wVv866~rCk&Z;8LrT_f`f5x%K;b#H#|lI46lLGA zq4-^UL{~~6WkuoJx})hKrNKt7djJP5t0<==f^-h?$ysp&F`(Za&3&Y(;}4b~#_4mW zxzL%{eH(FVchN?V{|L6L%ETArXLE`Ml)v>=HUFsjG9RApcTIglO&+U9V4k*Tzna6^#GAdZKFEFPJmFQtLa2l)q$bge`H2 z8iuY-J^W9-461}v3=dtDrOP9kLi!wF0Ws%M(_E4?tsmsWb6XN^beVF<&;NxlRE-v^ z4rfMfBko&r^dAsu40m{>QalO*Ozc{c?BOXU%!3x z*8?OY$`^B{ZTYl#@^nO7aU+%H)k5iNAsT)Pt!xdSSG?gXE^Qw7VzpM<4sx(&!?vbc zx3<#!?^bAJmu!W9*G%KD<=UWdZHC!vC5>g$R$`6{#X@lH*v+7_Mo4a|M#&o_vr8tc z-+ry?CDUd!LUk6@0@RvIwiX5ce>NJ`4KoJL)$EmOr8RIn0ysg>KXTh@X@G&`8<-}7 zYY@($nioi!jx*u|h#rDn3RAG!imyB$xT)1@-Jt99q00rsP!HbBdBe8DR~sg*kT)dn z?il?7knIY*1!yMDV>B2x;MlTUau)e5K|by>?!twxMp3yynI*y`^f$0$y?>j3di@Zi>i$48QpE z1wvMln+AsGBlI4yj?e@vMH;%?H_X&b`jv}o0NrJnMBvq1!3IZ5!Gcny!&;A_p z0F!g@kZV97Xnkc(%MC2FS_L6fcZ6WIK!P#B*c3y3og*qV*tyiE8(|!V_3Cx4`2vB{ zVXkg#>}^v~*H;L^R@4=u^-|5Le*Hm*f6twAm$r~bZ&b`7+L1YvsPWN~cn;vR8t`c4}uvuak#cwDsVC_+WU^oMH42hUS=5@-$jbZ0xN~9i2fprBg|p^F-%xMMtInD z@e%H?AChe=VGQ{~wF2j#E-#8P29`woe?0|LVuA;ytC=4QMKM1 z8?j(@%j4l)cHE^o+$S%x#Ob^G6etWk{qAi|bfoFIGim4O6pvN~0ro!${t|eTV;|l zq%u_Cohyk@V^p;Xgafv{7l=Zs_|Bd~g9mT0ZO&;pMUG*Lh;FpnuvvpI0UU^p z;rU%`UJr7bo3^nu!#c6kYhU{WkR*$Glr-a`$CY;>3_{jqwYEJZAc)F>Otm4HA=apj zD%M~6J}f(!E~xFD{b*{h6+3ti?cSF~;6#QOY(DoO-sALPr13>+cY+$YtRjE17@Sjx zD<{Q`y%6#KW1x!=Vf$d%qmABIngG4mLB}882mJ`6rhg5v%S7oD@V#k7W5|CWokIGQ zsSrQ$kUoz5GY?bIB@0jWHm0kfo!}Vu-6V$`dVJT+U_cKLr$I45w0k2JE9Y9zjO=#@gRHGIRZ zTD+QZz!#`N`>98me?4@C;-006`5p15hCjP?_!FQ0esqt!`5F$w^b5N+zt#{qsbtpRJzjtQW zHjW|JNCC{e&ph)ClgVH*f$PV`!^NlB{p`={#q0)d=XbLa+&sYj!vcPPxS8Mn^@h|~ z^J6BgpS~Oa^lre9a-)sKBsVt6rNT?7bmcHhP+@`EkD6y$l*am#7DDaB-X&$_6N~>; z(vrCr1-+KS(z{3N$hmc=%~PYD7uqK>qkuA(XoI;s8B9ryE>T8rNX>7BPJaF|Nw>nf z!GOYfFgchKwJ7D7{@)JJw_TBrqDL zZ6)@sm0xUHg)=G(;Ds9+rO1RAvwyu6>5-R4e1Wa}gcEM7`PFDD)AjQRvJ2{SEj(df zh;&O3xI}hli{!f)7m5Vqb}qM6g}Mbeg93M8o{@;!jg3b{}Y zN6+VBlNuCTbd~a*-ca`39ht+B{Q$u~OyKQX;ODV)_r}kP((fBb^cjTr2`&|z1; zAG`MbKN-Z>jG9MHhd7>Q%x2aa8;jf${57AILo4_g^%9~y_dqu@+ry5lA&=L8%z@4) zol|Nyb*Xa<wv?g>3X+13#be^)NBdkw97A$F4q*Eb@Tr0<{Opc8rXJvUfsr&^) zp0lh8t){1D!aUlBa@rvhtMCY0#$irjML1e)N7p;uM(Ox)G`pLh3WxB@y}PW3TEprD zvqw^6qNx2eB;UPwJvWWY;SL;mIl6j17rm2lWPfz_diK4Od9cqg7sT6_G?39ad+$h5 zQev%JJ`Gra#3p$!&)q$DWCnxYU;yWT+)j>vA772H&u_;U@czT6aSRs|xSHI;=gGx~ z_kSIr7)iceFl~n?$*YqdEybNwnx{*p)1}DyDU`A_e39~;uer2wSc`R`wC&Vj`G##x zT9h``{97qBs!O)UzXj9yX}LCNT$^t7Oi5#zw5eF2L$MTGJ9X3PEEkfSG*|Kt#q5g7 z^tWHrY{j(c_0XLKwFFgg$qJF+?^&;x<;)nkRP!6lY-z5!DbORounHb}00T_IHMgaf z1{g@bfoYfQ41|N3@dTB+BqFi~5K;hkB}^jBd1l2uk2}u3mAvHdREb%})7=qb1(++9 zL(p`LO6RJkBr#F)j2pxBmY9U( zSaBna6N^HMl3?Hne(@)b)8=f?{f*T^-hoyAOXRZD?<(swcp4stZ*eQI;bG%%)^~nB zv*NWk8^dCYoglIidb%`9q94zarI0L#^b7965cb3$c4t2EUSz8K!FPX#@?r5*sDa|d znxz-ij^^GZ?d*tf&*nccxd1;73+QK{Dv{8*Y&b8L2I@ehYJD*z*~49A7rsOP~~zBM#E zD#a^hIlMY`GF1kN<9!R+if3Q>0&pf1cCiq43v<3znD;p++DavlDV0ynosc<?c=jI4@~ELa>IZjK z-D$O9^Bm_lELm=NybC<>{iL}mb4xR>)5CP;b4zVWW{D1j2d@>NHQqVcDC2cuTl}NO zh${1HL*ypW;FnV84}G17opT}~vR$HDgaNdw-pq%JWNow59TZN&77IT`TADu3G*t<~ zqpVk2R#9kCNZBcbZ_bLF##KphFx7LXGjBwv&bkD+J_9t3TGvabsc_e{OMw1=S^-z8 zp|8;24N01g{&jf@^+PblUdBy&TKbC|Yt>W}cl9>ZGoh8>CPehyx;kTt<3OuQ|4?5{ z*wib!K{NETBJ_VKYxsOrWK^v?3q!{x4IGy#<)3gHVOO|CJw?ydu4){6QPiyNReU(0 z3ccCMB2ue^A;hZg_8O5A9d~jihJV+?Xrs-dpSb&N6i NzH$G%ob_;e{Qyq4v|9iG literal 0 HcmV?d00001 diff --git a/mobile/openapi/lib/model/job_type.dart b/mobile/openapi/lib/model/job_type.dart new file mode 100644 index 0000000000000000000000000000000000000000..2cf21674ed5926a6c766760f0651d0138fc05b72 GIT binary patch literal 2953 zcmai0ZExa65dO}um`K%yNYu3Vsmhhh(cm;(6Q~mB)CwVMvDaaHX4lV3Sf5jnYU-2VL0p!hcJGM=0E-(&ccUr6i(n~dKV60GKbkbg8TVodh_=gnvvzF zlxaJ9AN>5jM~~uJD$V0e={OTPzl2hjhNm&l`G!k7sGr5AP}+9hVELBqOk9*U*8Ja6 zXuK}j244%N@!fK55L}z?>xq)aGHGLxqC=4huHALh=`0tLn>bhU8O`jP$@rI_;$+RV z>GjZ^1(ks+xMYP0@c%=v*GqC{4189rXjgFf)&m$~;#2QcFw22~AG}_; zajS{0xhZpdR4UAZiX}4W6B6KzxTd^&2lN~WGgCI)6n0lbA(+z8_oIUjsZnIQKd;^S z(c4qJsSVz8tR~Oiw>Lc-fh_a<5DCBs9k4WQD&+40yt>%ycmJfhEw!v~Sw-3O>%v?% zC35~>d#|LyLumSe^NL0Af`#H{Az>1bfz+TS;Ai1fE8(Ol4JW~>N+$*mj!9H4-_<45 zL`a!suwuzqvTm*h$LiQ3hwEO(607v?P5uzK;wG-EtI@Gcwjfb8{rnwi>03sXy!it3 zD+U9l;&hNcc>Qfgdg91zP|_E5=B~NR1sN|Xc~+-F5{%{duQ0`mq7bO<3^}kQCR$zM zZop#FKIn$S{zzqE#lhD*1yd#oHzq@n9nC5A7w=0)EkgAS;b{Q3YRggS2SoptYd1gN z_g!KCPXyjKvFJ(S4)A_CbcPJ!WYi5V{$%Y>gZ;KW)PjT=4sUG9%c`w)ctMx^ zd#xk&sGvGF!TAYNhtUa8dvlzkv@K2`I@Zi7N{7)NDCHSLeZRye)qhm)v!)w|#Uu+$ z)Pu2pF6pPHqh$a5Nq&_`NdHsG^9X0wH~SKPpzir!0=19ts4%HH#fLKM2k{XZmUD|5 bTPt8od2l2ug(a>XZ`YRT)n4^JkdXfeJT$GJ literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/all_job_status_response_dto_test.dart b/mobile/openapi/test/all_job_status_response_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..0853da9d1b310da5788bcc1fc63b166ca532b043 GIT binary patch literal 1312 zcmb7@K~LL25QXpl71L9Klw!)M0wFcR22mBPf~nM#nb?zLslDs&&Zdnj{(Hw8b7)Bd zK6vf5=DqpmX*Z6eIELwcp1r?JZ_?Xoo+fZ{^(CD^lEF=u!*!NiU3`BhWnJZ`3N4=< zAAUHFx~*DhW2u-MTg+8WC*ZYrR25WHLz*Ygd)2hY@>T`bzi{n})^lO$r&m_w8XH+# zwDM-MPRy;_E-#ICjG7BoNkcVPWP{voZB{EyuBeS(NEt6t7axy`a*5VO(S{L5EtWrU zK_{jqjjf91Iqaq$oxUhxavXQdb3xa%+dGg*U|QFo&Fqemy}KpX3MwURqLonD+kDufptA55nnlxhBR68PnfvQ34qd%P#34j6M}P?*Cy72xXsm5KN!RH@Q;g i+~}3uz#)Nb)eQpZ(^&lrFd&MZ%C}+2`3pz(Df$ZutE_VX literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/create_job_dto_test.dart b/mobile/openapi/test/create_job_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..5ae779231e31d374c4a4b41b5c8dff1ee61360af GIT binary patch literal 559 zcmZutPfx-y6u<9NJWqoJnQ}4`5{VlIqbxD=;HlK@ZB)0mww;C;zPqml4nz*^doTU{ zdrgukNnrk3e-$QIVmj1)4y&XU$q6(Xv*_hG1^PCA+y|<%Sy@Mf;2pYhHfx25*%{$+bx6jQ!T} z^dH2bb&0=@0B`DN3-l#$Z;(5DRPU)%(YF#6bX>^65Pl;7Le@g^7DQN}M3|ir2E%DY zu*Rrf4`MHR@*I!gAQYs?q}XbNk9{Q~gdoIJp#ir+(1w;WZVT%JZoQY`b}P$}%JV2$ Qk*9gk*!dLHg}RIW0A{tehyVZp literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_api_test.dart b/mobile/openapi/test/job_api_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..2b8d82393c63f73b43fb9e3b5fbe160d27f55bde GIT binary patch literal 956 zcmb`EQA@)x6oudWE3QxLU|RJ_oDPTGL{SS`AAAb2yQ|SIiOH=p#Q*M1TSasO1s|H^ zCf_~hG)a;uNnm`FXPYOJ%gNO^pQJE7J)aC9&EPW2;UY^xbo(Y|2 zapzaJ7e;%MnuMCMp_(gnO}k%dRw<2MR7RJK(Y@4SZ(9`i()lQAGeXbG;!&2kZ!BtQ zRl?s^`0L*A2SVs63Bv~vE+v7meKty|(Cs^h;f^Iy4De z`3`B78=y%TAWoTQY1TBnwYk37j;o4wuSrtvuh3h52V~0;lt}HFZx1=~O5^X`-y^K` k|2&9!l`k#ALp!gJ^4aoVF+3T|qQ6Yg(Z4hgukjYW14X7WBLDyZ literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_command_dto_test.dart b/mobile/openapi/test/job_command_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..fc3117027718235eb2d4a8d4e6f2473c495ffd76 GIT binary patch literal 565 zcmZutUrWO<5P#37IG?(~Ty;<47&vSv6J0^-gHPGnrd_a2YLY5L_T9Z?8#3HOE_dYj z?-Iu>j$!_qXO|D@Dt(#fX#$JoQ#yhqgH@KpbCxU@Zx;mf$h#7an@z^o6Xr)*D{Ta? zwBeO(#4Yryx1!{t5v@>ebev_|Y2(f^u=&QVN(vL`_N-YWm9V_gswSA*pyD@Iyx5?zjO{Z*)M4>~wYb%tP}s?s&e(4q zP5(ihurBuH2=JzUwm@G3kNW*yw=JqNahiQ;Swf3a4TkW;00>%1g$>BCK_Osv$`}l% zjBvHleK&}_?CE%9`%ttvt^&!c#G PmSjO==aWzu?j8FD5`?(w literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_command_test.dart b/mobile/openapi/test/job_command_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..df6822c9d498ed8ffb44a8e693325d3869daf779 GIT binary patch literal 417 zcmZvYQA@)x6oudOE3Qx7pf0*6*$^DIlZmb%^}(lbZEw3^o5UnlME2iJ90kEX+;chb zotz|`GtOc2To&hd`96Qzlz9f*-9w&2R=~a};jzee+n2L|Me?Rb>#vuK%O#6ewn|$P zjkcnZ9j#zcgQHqdM?EQ@j$ZagTR)k=?i0V9FoPGC-Un$zTwx#7pbguTb>VR9rsI`X z&ZF`|)}c@~lI(BXO_p_1krSO(ZNU5imAJYP)d8(z>~u#+3|4R0(n=drXe1A_h_6o8 z{}4~k^C%+_OQUxXz69>|>#gp4R5kn1vW6a|n$6*xF@V8l{P;2T$wRPRqu2cir7eh~c|CZ9xu_0EcdO zvcLD6<98C>4d4hu=G_+%W{Pkj|MIOJz2-* z*6o(3S~-u(3z>1C%q7`Y?skTiQjrs-Rs~~zj!Il#h;)wD1wq#%^bDq-SkO=#QfOqz z-+8m^-sm6V$@#DuHb4{F)DHMa;8B0v=~{Ugd~2COg;MoT;U@rKESCyPkZt!V!OfQL zpN;}%3ae|=?=^PYwcY^M7Ojb@E5OvtZ(;$o(X7zz1pJ)9Sl)*9OL*_F_s#&_Ql)Jt zjf}fV;wWw{R$cg?d#Wo_@+D+@1OCXPGjim&vp=B2j&{$e#N`3@Ywl?O1xhbfvG;t| N++qIg*51S__yuvC9&G>s literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_id_test.dart b/mobile/openapi/test/job_id_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..66b6b7656cf74a6ea1667966c0f1e700e80bb360 GIT binary patch literal 407 zcmZvYK}*Ci5QXpg72~P9P&c|K*+p=nU8(36q#itlv7L5t`LKXv6Kvx-i_jd3&vu z^QgR#O$d~&B>QJ~vthkdh($ literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_status_response_dto_test.dart b/mobile/openapi/test/job_status_response_dto_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..09ea08df58cb68d01d58956f4d83f9ec80ae436d GIT binary patch literal 687 zcma)&K~KU!5QXpk72^p=kg6vGA(2=#8l?%=gQsOF1FV+aWp`FX4FBEfBJp4oJ#@R% zyzjj?P17VzVf0+&r?=zDcseS^8Qk1GjJuHKFv$ye%(J_jmlKxN%BzKRG`#4YUnKRY zR@yieOXI{+RoI81gU5xy3OA^!dz{tA8b^(UtUlz<3mZr{+y>m4pXpLYUuj*(Av!if! uaFKqe%UH4oxoDARc{JG{%EXWP{1r=rZNPxnCTRLI=12O!AU2GF`{WDO#NSr{ literal 0 HcmV?d00001 diff --git a/mobile/openapi/test/job_type_test.dart b/mobile/openapi/test/job_type_test.dart new file mode 100644 index 0000000000000000000000000000000000000000..d611a6557003203e7f2bc7bf5a40d24eadeffa6e GIT binary patch literal 411 zcmZvYK~KXl42AFc73S1UDl6=SHX$*o9U6jFLgm0IGOgQ1v`KJM(1iBiNkBcZLmWN% zdwz+tOk^2sUaIo)zStMfo2tlRyL&8B$V=Fl6+D&sZu@!>ut?rD$o_V@xL%4VRi_P; zYz@m+^|XRfkB%BiJq@INI%zdn!~U!T`!75?X-6-aK1RjCu5k#}B8S@(yD*&Hd3$ZN z^QgU4O$b!2BL20zvthl`YljfJ)Ei@GS&D$7Zy%B$5-9q%d_)^R*Bs@dMxw BihlqA literal 0 HcmV?d00001 diff --git a/server/.dockerignore b/server/.dockerignore index 834ab88b61..a66e51e358 100644 --- a/server/.dockerignore +++ b/server/.dockerignore @@ -1,4 +1,4 @@ node_modules/ upload/ dist/ - +.reverse-geocoding-dump diff --git a/server/apps/immich/src/api-v1/album/album.service.spec.ts b/server/apps/immich/src/api-v1/album/album.service.spec.ts index 672d39f9af..2ef7e5530f 100644 --- a/server/apps/immich/src/api-v1/album/album.service.spec.ts +++ b/server/apps/immich/src/api-v1/album/album.service.spec.ts @@ -134,6 +134,9 @@ describe('Album service', () => { getAssetByTimeBucket: jest.fn(), getAssetByChecksum: jest.fn(), getAssetCountByUserId: jest.fn(), + getAssetWithNoEXIF: jest.fn(), + getAssetWithNoThumbnail: jest.fn(), + getAssetWithNoSmartInfo: jest.fn(), }; sut = new AlbumService(albumRepositoryMock, assetRepositoryMock); diff --git a/server/apps/immich/src/api-v1/asset/asset-repository.ts b/server/apps/immich/src/api-v1/asset/asset-repository.ts index 3c88633231..089819af37 100644 --- a/server/apps/immich/src/api-v1/asset/asset-repository.ts +++ b/server/apps/immich/src/api-v1/asset/asset-repository.ts @@ -29,6 +29,9 @@ export interface IAssetRepository { getAssetCountByUserId(userId: string): Promise; getAssetByTimeBucket(userId: string, getAssetByTimeBucketDto: GetAssetByTimeBucketDto): Promise; getAssetByChecksum(userId: string, checksum: Buffer): Promise; + getAssetWithNoThumbnail(): Promise; + getAssetWithNoEXIF(): Promise; + getAssetWithNoSmartInfo(): Promise; } export const ASSET_REPOSITORY = 'ASSET_REPOSITORY'; @@ -40,6 +43,33 @@ export class AssetRepository implements IAssetRepository { private assetRepository: Repository, ) {} + async getAssetWithNoSmartInfo(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.smartInfo', 'si') + .where('asset.resizePath IS NOT NULL') + .andWhere('si.id IS NULL') + .getMany(); + } + + async getAssetWithNoThumbnail(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .where('asset.resizePath IS NULL') + .orWhere('asset.resizePath = :resizePath', { resizePath: '' }) + .orWhere('asset.webpPath IS NULL') + .orWhere('asset.webpPath = :webpPath', { webpPath: '' }) + .getMany(); + } + + async getAssetWithNoEXIF(): Promise { + return await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.exifInfo', 'ei') + .where('ei."assetId" IS NULL') + .getMany(); + } + async getAssetCountByUserId(userId: string): Promise { // Get asset count by AssetType const res = await this.assetRepository diff --git a/server/apps/immich/src/api-v1/asset/asset.controller.ts b/server/apps/immich/src/api-v1/asset/asset.controller.ts index 387671df3a..a045bdce45 100644 --- a/server/apps/immich/src/api-v1/asset/asset.controller.ts +++ b/server/apps/immich/src/api-v1/asset/asset.controller.ts @@ -30,7 +30,7 @@ import { CommunicationGateway } from '../communication/communication.gateway'; import { InjectQueue } from '@nestjs/bull'; import { Queue } from 'bull'; import { IAssetUploadedJob } from '@app/job/index'; -import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { assetUploadedProcessorName } from '@app/job/constants/job-name.constant'; import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto'; import { ApiBearerAuth, ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger'; @@ -59,7 +59,7 @@ export class AssetController { private assetService: AssetService, private backgroundTaskService: BackgroundTaskService, - @InjectQueue(assetUploadedQueueName) + @InjectQueue(QueueNameEnum.ASSET_UPLOADED) private assetUploadedQueue: Queue, ) {} diff --git a/server/apps/immich/src/api-v1/asset/asset.module.ts b/server/apps/immich/src/api-v1/asset/asset.module.ts index 13df9997f9..adc0705078 100644 --- a/server/apps/immich/src/api-v1/asset/asset.module.ts +++ b/server/apps/immich/src/api-v1/asset/asset.module.ts @@ -7,7 +7,7 @@ import { BullModule } from '@nestjs/bull'; import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; import { CommunicationModule } from '../communication/communication.module'; -import { assetUploadedQueueName } from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; @Module({ @@ -16,7 +16,7 @@ import { AssetRepository, ASSET_REPOSITORY } from './asset-repository'; BackgroundTaskModule, TypeOrmModule.forFeature([AssetEntity]), BullModule.registerQueue({ - name: assetUploadedQueueName, + name: QueueNameEnum.ASSET_UPLOADED, defaultJobOptions: { attempts: 3, removeOnComplete: true, diff --git a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts index 3b07d4f74f..89305dcd4b 100644 --- a/server/apps/immich/src/api-v1/asset/asset.service.spec.ts +++ b/server/apps/immich/src/api-v1/asset/asset.service.spec.ts @@ -107,6 +107,9 @@ describe('AssetService', () => { getAssetByTimeBucket: jest.fn(), getAssetByChecksum: jest.fn(), getAssetCountByUserId: jest.fn(), + getAssetWithNoEXIF: jest.fn(), + getAssetWithNoThumbnail: jest.fn(), + getAssetWithNoSmartInfo: jest.fn(), }; sui = new AssetService(assetRepositoryMock, a); diff --git a/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts b/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts index c43c55b4eb..ff86716eca 100644 --- a/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts +++ b/server/apps/immich/src/api-v1/asset/response-dto/exif-response.dto.ts @@ -1,12 +1,16 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; +import { ApiProperty } from '@nestjs/swagger'; export class ExifResponseDto { - id?: string | null = null; + @ApiProperty({ type: 'integer', format: 'int64' }) + id?: number | null = null; make?: string | null = null; model?: string | null = null; imageName?: string | null = null; exifImageWidth?: number | null = null; exifImageHeight?: number | null = null; + + @ApiProperty({ type: 'integer', format: 'int64' }) fileSizeInByte?: number | null = null; orientation?: string | null = null; dateTimeOriginal?: Date | null = null; @@ -25,13 +29,13 @@ export class ExifResponseDto { export function mapExif(entity: ExifEntity): ExifResponseDto { return { - id: entity.id, + id: parseInt(entity.id), make: entity.make, model: entity.model, imageName: entity.imageName, exifImageWidth: entity.exifImageWidth, exifImageHeight: entity.exifImageHeight, - fileSizeInByte: entity.fileSizeInByte, + fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null, orientation: entity.orientation, dateTimeOriginal: entity.dateTimeOriginal, modifyDate: entity.modifyDate, diff --git a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts new file mode 100644 index 0000000000..38289ed134 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEnum, IsNotEmpty } from 'class-validator'; + +export enum JobId { + THUMBNAIL_GENERATION = 'thumbnail-generation', + METADATA_EXTRACTION = 'metadata-extraction', + VIDEO_CONVERSION = 'video-conversion', + MACHINE_LEARNING = 'machine-learning', +} + +export class GetJobDto { + @IsNotEmpty() + @IsEnum(JobId, { + message: `params must be one of ${Object.values(JobId).join()}`, + }) + @ApiProperty({ + enum: JobId, + enumName: 'JobId', + }) + jobId!: string; +} diff --git a/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts b/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts new file mode 100644 index 0000000000..f63f0fa517 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/dto/job-command.dto.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsIn, IsNotEmpty } from 'class-validator'; + +export class JobCommandDto { + @IsNotEmpty() + @IsIn(['start', 'stop']) + @ApiProperty({ + enum: ['start', 'stop'], + enumName: 'JobCommand', + }) + command!: string; +} diff --git a/server/apps/immich/src/api-v1/job/job.controller.ts b/server/apps/immich/src/api-v1/job/job.controller.ts new file mode 100644 index 0000000000..2fbccb7fd8 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.controller.ts @@ -0,0 +1,43 @@ +import { Controller, Get, Body, UseGuards, ValidationPipe, Put, Param } from '@nestjs/common'; +import { JobService } from './job.service'; +import { ApiBearerAuth, ApiTags } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard'; +import { AdminRolesGuard } from '../../middlewares/admin-role-guard.middleware'; +import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto'; +import { GetJobDto } from './dto/get-job.dto'; +import { JobStatusResponseDto } from './response-dto/job-status-response.dto'; + +import { JobCommandDto } from './dto/job-command.dto'; + +@UseGuards(JwtAuthGuard) +@UseGuards(AdminRolesGuard) +@ApiTags('Job') +@ApiBearerAuth() +@Controller('jobs') +export class JobController { + constructor(private readonly jobService: JobService) {} + + @Get() + getAllJobsStatus(): Promise { + return this.jobService.getAllJobsStatus(); + } + + @Get('/:jobId') + getJobStatus(@Param(ValidationPipe) params: GetJobDto): Promise { + return this.jobService.getJobStatus(params); + } + + @Put('/:jobId') + async sendJobCommand( + @Param(ValidationPipe) params: GetJobDto, + @Body(ValidationPipe) body: JobCommandDto, + ): Promise { + if (body.command === 'start') { + return await this.jobService.startJob(params); + } + if (body.command === 'stop') { + return await this.jobService.stopJob(params); + } + return 0; + } +} diff --git a/server/apps/immich/src/api-v1/job/job.module.ts b/server/apps/immich/src/api-v1/job/job.module.ts new file mode 100644 index 0000000000..2cb5beb7bf --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.module.ts @@ -0,0 +1,82 @@ +import { Module } from '@nestjs/common'; +import { JobService } from './job.service'; +import { JobController } from './job.controller'; +import { ImmichJwtService } from '../../modules/immich-jwt/immich-jwt.service'; +import { ImmichJwtModule } from '../../modules/immich-jwt/immich-jwt.module'; +import { JwtModule } from '@nestjs/jwt'; +import { jwtConfig } from '../../config/jwt.config'; +import { UserEntity } from '@app/database/entities/user.entity'; +import { TypeOrmModule } from '@nestjs/typeorm'; +import { BullModule } from '@nestjs/bull'; +import { QueueNameEnum } from '@app/job'; +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { ExifEntity } from '@app/database/entities/exif.entity'; +import { AssetRepository, ASSET_REPOSITORY } from '../asset/asset-repository'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([UserEntity, AssetEntity, ExifEntity]), + ImmichJwtModule, + JwtModule.register(jwtConfig), + BullModule.registerQueue( + { + name: QueueNameEnum.THUMBNAIL_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.ASSET_UPLOADED, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.METADATA_EXTRACTION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.VIDEO_CONVERSION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.CHECKSUM_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.MACHINE_LEARNING, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + ), + ], + controllers: [JobController], + providers: [ + JobService, + ImmichJwtService, + { + provide: ASSET_REPOSITORY, + useClass: AssetRepository, + }, + ], +}) +export class JobModule {} diff --git a/server/apps/immich/src/api-v1/job/job.service.ts b/server/apps/immich/src/api-v1/job/job.service.ts new file mode 100644 index 0000000000..761a70906f --- /dev/null +++ b/server/apps/immich/src/api-v1/job/job.service.ts @@ -0,0 +1,180 @@ +import { + exifExtractionProcessorName, + generateJPEGThumbnailProcessorName, + IMetadataExtractionJob, + IThumbnailGenerationJob, + IVideoTranscodeJob, + MachineLearningJobNameEnum, + QueueNameEnum, + videoMetadataExtractionProcessorName, +} from '@app/job'; +import { InjectQueue } from '@nestjs/bull'; +import { Queue } from 'bull'; +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AllJobStatusResponseDto } from './response-dto/all-job-status-response.dto'; +import { randomUUID } from 'crypto'; +import { ASSET_REPOSITORY, IAssetRepository } from '../asset/asset-repository'; +import { AssetType } from '@app/database/entities/asset.entity'; +import { GetJobDto, JobId } from './dto/get-job.dto'; +import { JobStatusResponseDto } from './response-dto/job-status-response.dto'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; + +@Injectable() +export class JobService { + constructor( + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) + private thumbnailGeneratorQueue: Queue, + + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) + private metadataExtractionQueue: Queue, + + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) + private videoConversionQueue: Queue, + + @InjectQueue(QueueNameEnum.MACHINE_LEARNING) + private machineLearningQueue: Queue, + + @Inject(ASSET_REPOSITORY) + private _assetRepository: IAssetRepository, + ) { + this.thumbnailGeneratorQueue.empty(); + this.metadataExtractionQueue.empty(); + this.videoConversionQueue.empty(); + } + + async startJob(jobDto: GetJobDto): Promise { + switch (jobDto.jobId) { + case JobId.THUMBNAIL_GENERATION: + return this.runThumbnailGenerationJob(); + case JobId.METADATA_EXTRACTION: + return this.runMetadataExtractionJob(); + case JobId.VIDEO_CONVERSION: + return 0; + case JobId.MACHINE_LEARNING: + return this.runMachineLearningPipeline(); + default: + throw new BadRequestException('Invalid job id'); + } + } + + async getAllJobsStatus(): Promise { + const thumbnailGeneratorJobCount = await this.thumbnailGeneratorQueue.getJobCounts(); + const metadataExtractionJobCount = await this.metadataExtractionQueue.getJobCounts(); + const videoConversionJobCount = await this.videoConversionQueue.getJobCounts(); + const machineLearningJobCount = await this.machineLearningQueue.getJobCounts(); + + const response = new AllJobStatusResponseDto(); + response.isThumbnailGenerationActive = Boolean(thumbnailGeneratorJobCount.waiting); + response.thumbnailGenerationQueueCount = thumbnailGeneratorJobCount; + response.isMetadataExtractionActive = Boolean(metadataExtractionJobCount.waiting); + response.metadataExtractionQueueCount = metadataExtractionJobCount; + response.isVideoConversionActive = Boolean(videoConversionJobCount.waiting); + response.videoConversionQueueCount = videoConversionJobCount; + response.isMachineLearningActive = Boolean(machineLearningJobCount.waiting); + response.machineLearningQueueCount = machineLearningJobCount; + + return response; + } + + async getJobStatus(query: GetJobDto): Promise { + const response = new JobStatusResponseDto(); + if (query.jobId === JobId.THUMBNAIL_GENERATION) { + response.isActive = Boolean((await this.thumbnailGeneratorQueue.getJobCounts()).waiting); + response.queueCount = await this.thumbnailGeneratorQueue.getJobCounts(); + } + + if (query.jobId === JobId.METADATA_EXTRACTION) { + response.isActive = Boolean((await this.metadataExtractionQueue.getJobCounts()).waiting); + response.queueCount = await this.metadataExtractionQueue.getJobCounts(); + } + + if (query.jobId === JobId.VIDEO_CONVERSION) { + response.isActive = Boolean((await this.videoConversionQueue.getJobCounts()).waiting); + response.queueCount = await this.videoConversionQueue.getJobCounts(); + } + + return response; + } + + async stopJob(query: GetJobDto): Promise { + switch (query.jobId) { + case JobId.THUMBNAIL_GENERATION: + this.thumbnailGeneratorQueue.empty(); + return 0; + case JobId.METADATA_EXTRACTION: + this.metadataExtractionQueue.empty(); + return 0; + case JobId.VIDEO_CONVERSION: + this.videoConversionQueue.empty(); + return 0; + case JobId.MACHINE_LEARNING: + this.machineLearningQueue.empty(); + return 0; + default: + throw new BadRequestException('Invalid job id'); + } + } + + private async runThumbnailGenerationJob(): Promise { + const jobCount = await this.thumbnailGeneratorQueue.getJobCounts(); + + if (jobCount.waiting > 0) { + throw new BadRequestException('Thumbnail generation job is already running'); + } + + const assetsWithNoThumbnail = await this._assetRepository.getAssetWithNoThumbnail(); + + for (const asset of assetsWithNoThumbnail) { + await this.thumbnailGeneratorQueue.add(generateJPEGThumbnailProcessorName, { asset }, { jobId: randomUUID() }); + } + + return assetsWithNoThumbnail.length; + } + + private async runMetadataExtractionJob(): Promise { + const jobCount = await this.metadataExtractionQueue.getJobCounts(); + + if (jobCount.waiting > 0) { + throw new BadRequestException('Metadata extraction job is already running'); + } + + const assetsWithNoExif = await this._assetRepository.getAssetWithNoEXIF(); + for (const asset of assetsWithNoExif) { + if (asset.type === AssetType.VIDEO) { + await this.metadataExtractionQueue.add( + videoMetadataExtractionProcessorName, + { asset, fileName: asset.id }, + { jobId: randomUUID() }, + ); + } else { + await this.metadataExtractionQueue.add( + exifExtractionProcessorName, + { asset, fileName: asset.id }, + { jobId: randomUUID() }, + ); + } + } + return assetsWithNoExif.length; + } + + private async runMachineLearningPipeline(): Promise { + const jobCount = await this.machineLearningQueue.getJobCounts(); + + if (jobCount.waiting > 0) { + throw new BadRequestException('Metadata extraction job is already running'); + } + + const assetWithNoSmartInfo = await this._assetRepository.getAssetWithNoSmartInfo(); + + for (const asset of assetWithNoSmartInfo) { + await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); + } + + return assetWithNoSmartInfo.length; + } +} diff --git a/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts new file mode 100644 index 0000000000..aeb558acd5 --- /dev/null +++ b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts @@ -0,0 +1,35 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class JobCounts { + active!: number; + completed!: number; + failed!: number; + delayed!: number; + waiting!: number; +} +export class AllJobStatusResponseDto { + isThumbnailGenerationActive!: boolean; + isMetadataExtractionActive!: boolean; + isVideoConversionActive!: boolean; + isMachineLearningActive!: boolean; + + @ApiProperty({ + type: JobCounts, + }) + thumbnailGenerationQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + metadataExtractionQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + videoConversionQueueCount!: JobCounts; + + @ApiProperty({ + type: JobCounts, + }) + machineLearningQueueCount!: JobCounts; +} diff --git a/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts b/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts new file mode 100644 index 0000000000..fe411fa2ef --- /dev/null +++ b/server/apps/immich/src/api-v1/job/response-dto/job-status-response.dto.ts @@ -0,0 +1,6 @@ +import Bull from 'bull'; + +export class JobStatusResponseDto { + isActive!: boolean; + queueCount!: Bull.JobCounts; +} diff --git a/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts b/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts index 444292091b..e844da6899 100644 --- a/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts +++ b/server/apps/immich/src/api-v1/server-info/response-dto/server-info-response.dto.ts @@ -5,13 +5,13 @@ export class ServerInfoResponseDto { diskUse!: string; diskAvailable!: string; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskSizeRaw!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskUseRaw!: number; - @ApiProperty({ type: 'integer' }) + @ApiProperty({ type: 'integer', format: 'int64' }) diskAvailableRaw!: number; @ApiProperty({ type: 'number', format: 'float' }) diff --git a/server/apps/immich/src/app.module.ts b/server/apps/immich/src/app.module.ts index 16f644c030..3aef3d4b4d 100644 --- a/server/apps/immich/src/app.module.ts +++ b/server/apps/immich/src/app.module.ts @@ -15,6 +15,7 @@ import { AppController } from './app.controller'; import { ScheduleModule } from '@nestjs/schedule'; import { ScheduleTasksModule } from './modules/schedule-tasks/schedule-tasks.module'; import { DatabaseModule } from '@app/database'; +import { JobModule } from './api-v1/job/job.module'; @Module({ imports: [ @@ -55,6 +56,8 @@ import { DatabaseModule } from '@app/database'; ScheduleModule.forRoot(), ScheduleTasksModule, + + JobModule, ], controllers: [AppController], providers: [], diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts index 51f70fe8f5..e932a67f97 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.module.ts @@ -3,18 +3,14 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { ScheduleTasksService } from './schedule-tasks.service'; -import { - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, -} from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { ExifEntity } from '@app/database/entities/exif.entity'; @Module({ imports: [ TypeOrmModule.forFeature([AssetEntity, ExifEntity]), BullModule.registerQueue({ - name: videoConversionQueueName, + name: QueueNameEnum.VIDEO_CONVERSION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -22,7 +18,7 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; }, }), BullModule.registerQueue({ - name: thumbnailGeneratorQueueName, + name: QueueNameEnum.THUMBNAIL_GENERATION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -31,7 +27,7 @@ import { ExifEntity } from '@app/database/entities/exif.entity'; }), BullModule.registerQueue({ - name: metadataExtractionQueueName, + name: QueueNameEnum.METADATA_EXTRACTION, defaultJobOptions: { attempts: 3, removeOnComplete: true, diff --git a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts index cbdf3d8b16..d814d8b307 100644 --- a/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts +++ b/server/apps/immich/src/modules/schedule-tasks/schedule-tasks.service.ts @@ -12,11 +12,9 @@ import { generateWEBPThumbnailProcessorName, IMetadataExtractionJob, IVideoTranscodeJob, - metadataExtractionQueueName, mp4ConversionProcessorName, + QueueNameEnum, reverseGeocodingProcessorName, - thumbnailGeneratorQueueName, - videoConversionQueueName, videoMetadataExtractionProcessorName, } from '@app/job'; import { ConfigService } from '@nestjs/config'; @@ -30,13 +28,13 @@ export class ScheduleTasksService { @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, - @InjectQueue(videoConversionQueueName) + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) private videoConversionQueue: Queue, - @InjectQueue(metadataExtractionQueueName) + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) private metadataExtractionQueue: Queue, private configService: ConfigService, @@ -108,11 +106,11 @@ export class ScheduleTasksService { @Cron(CronExpression.EVERY_DAY_AT_3AM) async extractExif() { - const exifAssets = await this.assetRepository.find({ - where: { - exifInfo: IsNull(), - }, - }); + const exifAssets = await this.assetRepository + .createQueryBuilder('asset') + .leftJoinAndSelect('asset.exifInfo', 'ei') + .where('ei."assetId" IS NULL') + .getMany(); for (const asset of exifAssets) { if (asset.type === AssetType.VIDEO) { diff --git a/server/apps/microservices/src/microservices.module.ts b/server/apps/microservices/src/microservices.module.ts index 46b3b6afe0..0353bb08a9 100644 --- a/server/apps/microservices/src/microservices.module.ts +++ b/server/apps/microservices/src/microservices.module.ts @@ -4,13 +4,7 @@ import { AssetEntity } from '@app/database/entities/asset.entity'; import { ExifEntity } from '@app/database/entities/exif.entity'; import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { UserEntity } from '@app/database/entities/user.entity'; -import { - assetUploadedQueueName, - generateChecksumQueueName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, -} from '@app/job/constants/queue-name.constant'; +import { QueueNameEnum } from '@app/job/constants/queue-name.constant'; import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { ConfigModule, ConfigService } from '@nestjs/config'; @@ -19,6 +13,7 @@ import { CommunicationModule } from '../../immich/src/api-v1/communication/commu import { MicroservicesService } from './microservices.service'; import { AssetUploadedProcessor } from './processors/asset-uploaded.processor'; import { GenerateChecksumProcessor } from './processors/generate-checksum.processor'; +import { MachineLearningProcessor } from './processors/machine-learning.processor'; import { MetadataExtractionProcessor } from './processors/metadata-extraction.processor'; import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor'; import { VideoTranscodeProcessor } from './processors/video-transcode.processor'; @@ -42,7 +37,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }), BullModule.registerQueue( { - name: thumbnailGeneratorQueueName, + name: QueueNameEnum.THUMBNAIL_GENERATION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -50,7 +45,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: assetUploadedQueueName, + name: QueueNameEnum.ASSET_UPLOADED, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -58,7 +53,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: metadataExtractionQueueName, + name: QueueNameEnum.METADATA_EXTRACTION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -66,7 +61,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: videoConversionQueueName, + name: QueueNameEnum.VIDEO_CONVERSION, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -74,7 +69,15 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' }, }, { - name: generateChecksumQueueName, + name: QueueNameEnum.CHECKSUM_GENERATION, + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }, + { + name: QueueNameEnum.MACHINE_LEARNING, defaultJobOptions: { attempts: 3, removeOnComplete: true, @@ -92,6 +95,7 @@ import { VideoTranscodeProcessor } from './processors/video-transcode.processor' MetadataExtractionProcessor, VideoTranscodeProcessor, GenerateChecksumProcessor, + MachineLearningProcessor, ConfigService, ], exports: [], diff --git a/server/apps/microservices/src/microservices.service.ts b/server/apps/microservices/src/microservices.service.ts index e10fe46b83..5a03220f97 100644 --- a/server/apps/microservices/src/microservices.service.ts +++ b/server/apps/microservices/src/microservices.service.ts @@ -1,4 +1,4 @@ -import { generateChecksumQueueName } from '@app/job'; +import { QueueNameEnum } from '@app/job'; import { InjectQueue } from '@nestjs/bull'; import { Injectable, OnModuleInit } from '@nestjs/common'; import { Queue } from 'bull'; @@ -6,14 +6,18 @@ import { randomUUID } from 'node:crypto'; @Injectable() export class MicroservicesService implements OnModuleInit { - constructor ( - @InjectQueue(generateChecksumQueueName) + constructor( + @InjectQueue(QueueNameEnum.CHECKSUM_GENERATION) private generateChecksumQueue: Queue, ) {} async onModuleInit() { - await this.generateChecksumQueue.add({}, { - jobId: randomUUID(), delay: 10000 // wait for migration - }); + await this.generateChecksumQueue.add( + {}, + { + jobId: randomUUID(), + delay: 10000, // wait for migration + }, + ); } } diff --git a/server/apps/microservices/src/processors/asset-uploaded.processor.ts b/server/apps/microservices/src/processors/asset-uploaded.processor.ts index 7b70c07482..340d22a06d 100644 --- a/server/apps/microservices/src/processors/asset-uploaded.processor.ts +++ b/server/apps/microservices/src/processors/asset-uploaded.processor.ts @@ -4,30 +4,27 @@ import { IMetadataExtractionJob, IThumbnailGenerationJob, IVideoTranscodeJob, - assetUploadedQueueName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, - videoConversionQueueName, assetUploadedProcessorName, exifExtractionProcessorName, generateJPEGThumbnailProcessorName, mp4ConversionProcessorName, videoMetadataExtractionProcessorName, + QueueNameEnum, } from '@app/job'; import { InjectQueue, Process, Processor } from '@nestjs/bull'; import { Job, Queue } from 'bull'; import { randomUUID } from 'crypto'; -@Processor(assetUploadedQueueName) +@Processor(QueueNameEnum.ASSET_UPLOADED) export class AssetUploadedProcessor { constructor( - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, - @InjectQueue(metadataExtractionQueueName) + @InjectQueue(QueueNameEnum.METADATA_EXTRACTION) private metadataExtractionQueue: Queue, - @InjectQueue(videoConversionQueueName) + @InjectQueue(QueueNameEnum.VIDEO_CONVERSION) private videoConversionQueue: Queue, ) {} diff --git a/server/apps/microservices/src/processors/generate-checksum.processor.ts b/server/apps/microservices/src/processors/generate-checksum.processor.ts index 2dcd1c2bd4..bbf20cccd4 100644 --- a/server/apps/microservices/src/processors/generate-checksum.processor.ts +++ b/server/apps/microservices/src/processors/generate-checksum.processor.ts @@ -1,5 +1,5 @@ import { AssetEntity } from '@app/database/entities/asset.entity'; -import { generateChecksumQueueName } from '@app/job'; +import { QueueNameEnum } from '@app/job'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; @@ -8,7 +8,7 @@ import fs from 'node:fs'; import { FindOptionsWhere, IsNull, MoreThan, QueryFailedError, Repository } from 'typeorm'; // TODO: just temporary task to generate previous uploaded assets. -@Processor(generateChecksumQueueName) +@Processor(QueueNameEnum.CHECKSUM_GENERATION) export class GenerateChecksumProcessor { constructor( @InjectRepository(AssetEntity) @@ -33,7 +33,7 @@ export class GenerateChecksumProcessor { const assets = await this.assetRepository.find({ where: whereStat, take: pageSize, - order: { id: 'ASC' } + order: { id: 'ASC' }, }); if (!assets?.length) { diff --git a/server/apps/microservices/src/processors/machine-learning.processor.ts b/server/apps/microservices/src/processors/machine-learning.processor.ts new file mode 100644 index 0000000000..39c92fd9f0 --- /dev/null +++ b/server/apps/microservices/src/processors/machine-learning.processor.ts @@ -0,0 +1,60 @@ +import { AssetEntity } from '@app/database/entities/asset.entity'; +import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; +import { MachineLearningJobNameEnum, QueueNameEnum } from '@app/job'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; +import { Process, Processor } from '@nestjs/bull'; +import { Logger } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; +import axios from 'axios'; +import { Job } from 'bull'; +import { Repository } from 'typeorm'; + +@Processor(QueueNameEnum.MACHINE_LEARNING) +export class MachineLearningProcessor { + constructor( + @InjectRepository(SmartInfoEntity) + private smartInfoRepository: Repository, + ) {} + + @Process({ name: MachineLearningJobNameEnum.IMAGE_TAGGING, concurrency: 2 }) + async tagImage(job: Job) { + const { asset } = job.data; + + const res = await axios.post('http://immich-machine-learning:3003/image-classifier/tag-image', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.tags = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } + + @Process({ name: MachineLearningJobNameEnum.OBJECT_DETECTION, concurrency: 2 }) + async detectObject(job: Job) { + try { + const { asset }: { asset: AssetEntity } = job.data; + + const res = await axios.post('http://immich-machine-learning:3003/object-detection/detect-object', { + thumbnailPath: asset.resizePath, + }); + + if (res.status == 201 && res.data.length > 0) { + const smartInfo = new SmartInfoEntity(); + smartInfo.assetId = asset.id; + smartInfo.objects = [...res.data]; + + await this.smartInfoRepository.upsert(smartInfo, { + conflictPaths: ['assetId'], + }); + } + } catch (error) { + Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); + } + } +} diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index a06972e042..27f2688d74 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -1,23 +1,19 @@ import { ImmichLogLevel } from '@app/common/constants/log-level.constant'; import { AssetEntity } from '@app/database/entities/asset.entity'; import { ExifEntity } from '@app/database/entities/exif.entity'; -import { SmartInfoEntity } from '@app/database/entities/smart-info.entity'; import { IExifExtractionProcessor, IVideoLengthExtractionProcessor, exifExtractionProcessorName, - imageTaggingProcessorName, - objectDetectionProcessorName, videoMetadataExtractionProcessorName, - metadataExtractionQueueName, reverseGeocodingProcessorName, IReverseGeocodingProcessor, + QueueNameEnum, } from '@app/job'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { InjectRepository } from '@nestjs/typeorm'; -import axios from 'axios'; import { Job } from 'bull'; import exifr from 'exifr'; import ffmpeg from 'fluent-ffmpeg'; @@ -79,7 +75,7 @@ export interface GeoData { distance: number; } -@Processor(metadataExtractionQueueName) +@Processor(QueueNameEnum.METADATA_EXTRACTION) export class MetadataExtractionProcessor { private isGeocodeInitialized = false; private logLevel: ImmichLogLevel; @@ -91,9 +87,6 @@ export class MetadataExtractionProcessor { @InjectRepository(ExifEntity) private exifRepository: Repository, - @InjectRepository(SmartInfoEntity) - private smartInfoRepository: Repository, - private configService: ConfigService, ) { if (!configService.get('DISABLE_REVERSE_GEOCODING')) { @@ -109,7 +102,8 @@ export class MetadataExtractionProcessor { alternateNames: false, }, countries: [], - dumpDirectory: configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || (process.cwd() + '/.reverse-geocoding-dump/'), + dumpDirectory: + configService.get('REVERSE_GEOCODING_DUMP_DIRECTORY') || process.cwd() + '/.reverse-geocoding-dump/', }).then(() => { this.isGeocodeInitialized = true; Logger.log('Reverse Geocoding Initialised'); @@ -273,48 +267,6 @@ export class MetadataExtractionProcessor { } } - @Process({ name: imageTaggingProcessorName, concurrency: 2 }) - async tagImage(job: Job) { - const { asset }: { asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-machine-learning:3003/image-classifier/tag-image', { - thumbnailPath: asset.resizePath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.tags = [...res.data]; - - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } - - @Process({ name: objectDetectionProcessorName, concurrency: 2 }) - async detectObject(job: Job) { - try { - const { asset }: { asset: AssetEntity } = job.data; - - const res = await axios.post('http://immich-machine-learning:3003/object-detection/detect-object', { - thumbnailPath: asset.resizePath, - }); - - if (res.status == 201 && res.data.length > 0) { - const smartInfo = new SmartInfoEntity(); - smartInfo.assetId = asset.id; - smartInfo.objects = [...res.data]; - - await this.smartInfoRepository.upsert(smartInfo, { - conflictPaths: ['assetId'], - }); - } - } catch (error) { - Logger.error(`Failed to trigger object detection pipe line ${String(error)}`); - } - } - @Process({ name: videoMetadataExtractionProcessorName, concurrency: 2 }) async extractVideoMetadata(job: Job) { const { asset, fileName } = job.data; diff --git a/server/apps/microservices/src/processors/thumbnail.processor.ts b/server/apps/microservices/src/processors/thumbnail.processor.ts index 130aa6c1ad..211da0537c 100644 --- a/server/apps/microservices/src/processors/thumbnail.processor.ts +++ b/server/apps/microservices/src/processors/thumbnail.processor.ts @@ -5,11 +5,9 @@ import { WebpGeneratorProcessor, generateJPEGThumbnailProcessorName, generateWEBPThumbnailProcessorName, - imageTaggingProcessorName, - objectDetectionProcessorName, - metadataExtractionQueueName, - thumbnailGeneratorQueueName, JpegGeneratorProcessor, + QueueNameEnum, + MachineLearningJobNameEnum, } from '@app/job'; import { InjectQueue, Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; @@ -25,8 +23,9 @@ import sharp from 'sharp'; import { Repository } from 'typeorm/repository/Repository'; import { join } from 'path'; import { CommunicationGateway } from 'apps/immich/src/api-v1/communication/communication.gateway'; +import { IMachineLearningJob } from '@app/job/interfaces/machine-learning.interface'; -@Processor(thumbnailGeneratorQueueName) +@Processor(QueueNameEnum.THUMBNAIL_GENERATION) export class ThumbnailGeneratorProcessor { private logLevel: ImmichLogLevel; @@ -34,13 +33,13 @@ export class ThumbnailGeneratorProcessor { @InjectRepository(AssetEntity) private assetRepository: Repository, - @InjectQueue(thumbnailGeneratorQueueName) + @InjectQueue(QueueNameEnum.THUMBNAIL_GENERATION) private thumbnailGeneratorQueue: Queue, private wsCommunicationGateway: CommunicationGateway, - @InjectQueue(metadataExtractionQueueName) - private metadataExtractionQueue: Queue, + @InjectQueue(QueueNameEnum.MACHINE_LEARNING) + private machineLearningQueue: Queue, private configService: ConfigService, ) { @@ -80,8 +79,12 @@ export class ThumbnailGeneratorProcessor { asset.resizePath = jpegThumbnailPath; await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset))); } @@ -110,8 +113,12 @@ export class ThumbnailGeneratorProcessor { asset.resizePath = jpegThumbnailPath; await this.thumbnailGeneratorQueue.add(generateWEBPThumbnailProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(imageTaggingProcessorName, { asset }, { jobId: randomUUID() }); - await this.metadataExtractionQueue.add(objectDetectionProcessorName, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add(MachineLearningJobNameEnum.IMAGE_TAGGING, { asset }, { jobId: randomUUID() }); + await this.machineLearningQueue.add( + MachineLearningJobNameEnum.OBJECT_DETECTION, + { asset }, + { jobId: randomUUID() }, + ); this.wsCommunicationGateway.server.to(asset.userId).emit('on_upload_success', JSON.stringify(mapAsset(asset))); } diff --git a/server/apps/microservices/src/processors/video-transcode.processor.ts b/server/apps/microservices/src/processors/video-transcode.processor.ts index 45ea17ab09..2e04a75e0a 100644 --- a/server/apps/microservices/src/processors/video-transcode.processor.ts +++ b/server/apps/microservices/src/processors/video-transcode.processor.ts @@ -1,7 +1,7 @@ import { APP_UPLOAD_LOCATION } from '@app/common/constants'; import { AssetEntity } from '@app/database/entities/asset.entity'; +import { QueueNameEnum } from '@app/job'; import { mp4ConversionProcessorName } from '@app/job/constants/job-name.constant'; -import { videoConversionQueueName } from '@app/job/constants/queue-name.constant'; import { IMp4ConversionProcessor } from '@app/job/interfaces/video-transcode.interface'; import { Process, Processor } from '@nestjs/bull'; import { Logger } from '@nestjs/common'; @@ -11,7 +11,7 @@ import ffmpeg from 'fluent-ffmpeg'; import { existsSync, mkdirSync } from 'fs'; import { Repository } from 'typeorm'; -@Processor(videoConversionQueueName) +@Processor(QueueNameEnum.VIDEO_CONVERSION) export class VideoTranscodeProcessor { constructor( @InjectRepository(AssetEntity) diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index e78036840f..955bfe7c88 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"string","nullable":true,"default":null},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"fileSizeInByte":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer"},"diskUseRaw":{"type":"integer"},"diskAvailableRaw":{"type":"integer"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"delayed":{"type":"number"},"waiting":{"type":"number"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file diff --git a/server/libs/job/src/constants/job-name.constant.ts b/server/libs/job/src/constants/job-name.constant.ts index 002dd7f5a7..7b32c69028 100644 --- a/server/libs/job/src/constants/job-name.constant.ts +++ b/server/libs/job/src/constants/job-name.constant.ts @@ -20,5 +20,12 @@ export const generateWEBPThumbnailProcessorName = 'generate-webp-thumbnail'; export const exifExtractionProcessorName = 'exif-extraction'; export const videoMetadataExtractionProcessorName = 'extract-video-metadata'; export const reverseGeocodingProcessorName = 'reverse-geocoding'; -export const objectDetectionProcessorName = 'detect-object'; -export const imageTaggingProcessorName = 'tag-image'; + +/** + * Machine learning Queue Jobs + */ + +export enum MachineLearningJobNameEnum { + OBJECT_DETECTION = 'detect-object', + IMAGE_TAGGING = 'tag-image', +} diff --git a/server/libs/job/src/constants/queue-name.constant.ts b/server/libs/job/src/constants/queue-name.constant.ts index 15b7d7a6cc..f0f4c4e053 100644 --- a/server/libs/job/src/constants/queue-name.constant.ts +++ b/server/libs/job/src/constants/queue-name.constant.ts @@ -1,5 +1,8 @@ -export const thumbnailGeneratorQueueName = 'thumbnail-generator-queue'; -export const assetUploadedQueueName = 'asset-uploaded-queue'; -export const metadataExtractionQueueName = 'metadata-extraction-queue'; -export const videoConversionQueueName = 'video-conversion-queue'; -export const generateChecksumQueueName = 'generate-checksum-queue'; +export enum QueueNameEnum { + THUMBNAIL_GENERATION = 'thumbnail-generation-queue', + METADATA_EXTRACTION = 'metadata-extraction-queue', + VIDEO_CONVERSION = 'video-conversion-queue', + CHECKSUM_GENERATION = 'generate-checksum-queue', + ASSET_UPLOADED = 'asset-uploaded-queue', + MACHINE_LEARNING = 'machine-learning-queue', +} diff --git a/server/libs/job/src/interfaces/machine-learning.interface.ts b/server/libs/job/src/interfaces/machine-learning.interface.ts new file mode 100644 index 0000000000..13bf5e19d5 --- /dev/null +++ b/server/libs/job/src/interfaces/machine-learning.interface.ts @@ -0,0 +1,8 @@ +import { AssetEntity } from '@app/database/entities/asset.entity'; + +export interface IMachineLearningJob { + /** + * The Asset entity that was saved in the database + */ + asset: AssetEntity; +} diff --git a/server/package-lock.json b/server/package-lock.json index 39730ad635..62ebb7fd08 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -59,7 +59,7 @@ "@nestjs/testing": "^8.4.7", "@openapitools/openapi-generator-cli": "2.5.1", "@types/bcrypt": "^5.0.0", - "@types/bull": "^3.15.7", + "@types/bull": "^3.15.9", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", @@ -2339,9 +2339,9 @@ } }, "node_modules/@types/bull": { - "version": "3.15.7", - "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.7.tgz", - "integrity": "sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g==", + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", "dev": true, "dependencies": { "@types/ioredis": "*", @@ -3764,6 +3764,27 @@ "node": ">= 0.8" } }, + "node_modules/cache-manager": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.0.0.tgz", + "integrity": "sha512-1qKdoeoJKmrf95Zvhr3NpBVAgBESt4TuZomBzn4N2gCFZvHjuUXBK1H8EDVsJdba6/grIgi6WGYb/ncJj+wjtg==", + "optional": true, + "peer": true, + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.14.0" + } + }, + "node_modules/cache-manager/node_modules/lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "peer": true, + "engines": { + "node": ">=12" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -7674,6 +7695,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "optional": true, + "peer": true + }, "node_modules/lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", @@ -12900,9 +12928,9 @@ } }, "@types/bull": { - "version": "3.15.7", - "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.7.tgz", - "integrity": "sha512-7NC7XN5NoS0A+leJ/dR69ZfKaegOlCZaii/xGgKnCyh1UYisRncibImb7VMwrc3OdJcbDJt6+4om70TeNl3J7g==", + "version": "3.15.9", + "resolved": "https://registry.npmjs.org/@types/bull/-/bull-3.15.9.tgz", + "integrity": "sha512-MPUcyPPQauAmynoO3ezHAmCOhbB0pWmYyijr/5ctaCqhbKWsjW0YCod38ZcLzUBprosfZ9dPqfYIcfdKjk7RNQ==", "dev": true, "requires": { "@types/ioredis": "*", @@ -14073,6 +14101,26 @@ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" }, + "cache-manager": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-5.0.0.tgz", + "integrity": "sha512-1qKdoeoJKmrf95Zvhr3NpBVAgBESt4TuZomBzn4N2gCFZvHjuUXBK1H8EDVsJdba6/grIgi6WGYb/ncJj+wjtg==", + "optional": true, + "peer": true, + "requires": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "^7.14.0" + }, + "dependencies": { + "lru-cache": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.14.0.tgz", + "integrity": "sha512-EIRtP1GrSJny0dqb50QXRUNBxHJhcpxHC++M5tD7RYbvLLn5KVWKsbyswSSqDuU15UFi3bgTQIY8nhDMeF6aDQ==", + "optional": true, + "peer": true + } + } + }, "call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -17088,6 +17136,13 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "optional": true, + "peer": true + }, "lodash.defaults": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", diff --git a/server/package.json b/server/package.json index 368765adc1..62c113b7db 100644 --- a/server/package.json +++ b/server/package.json @@ -78,7 +78,7 @@ "@nestjs/testing": "^8.4.7", "@openapitools/openapi-generator-cli": "2.5.1", "@types/bcrypt": "^5.0.0", - "@types/bull": "^3.15.7", + "@types/bull": "^3.15.9", "@types/cookie-parser": "^1.4.3", "@types/cron": "^2.0.0", "@types/express": "^4.17.13", diff --git a/web/src/api/api.ts b/web/src/api/api.ts index 5e2b8f356d..b621c649f2 100644 --- a/web/src/api/api.ts +++ b/web/src/api/api.ts @@ -4,6 +4,7 @@ import { AuthenticationApi, Configuration, DeviceInfoApi, + JobApi, ServerInfoApi, UserApi } from './open-api'; @@ -15,6 +16,8 @@ class ImmichApi { public authenticationApi: AuthenticationApi; public deviceInfoApi: DeviceInfoApi; public serverInfoApi: ServerInfoApi; + public jobApi: JobApi; + private config = new Configuration({ basePath: '/api' }); constructor() { @@ -24,6 +27,7 @@ class ImmichApi { this.authenticationApi = new AuthenticationApi(this.config); this.deviceInfoApi = new DeviceInfoApi(this.config); this.serverInfoApi = new ServerInfoApi(this.config); + this.jobApi = new JobApi(this.config); } public setAccessToken(accessToken: string) { diff --git a/web/src/api/open-api/api.ts b/web/src/api/open-api/api.ts index 27ff36ab88..6654d9a328 100644 --- a/web/src/api/open-api/api.ts +++ b/web/src/api/open-api/api.ts @@ -170,6 +170,61 @@ export interface AlbumResponseDto { */ 'assets': Array; } +/** + * + * @export + * @interface AllJobStatusResponseDto + */ +export interface AllJobStatusResponseDto { + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'thumbnailGenerationQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'metadataExtractionQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'videoConversionQueueCount': JobCounts; + /** + * + * @type {JobCounts} + * @memberof AllJobStatusResponseDto + */ + 'machineLearningQueueCount': JobCounts; + /** + * + * @type {boolean} + * @memberof AllJobStatusResponseDto + */ + 'isThumbnailGenerationActive': boolean; + /** + * + * @type {boolean} + * @memberof AllJobStatusResponseDto + */ + 'isMetadataExtractionActive': boolean; + /** + * + * @type {boolean} + * @memberof AllJobStatusResponseDto + */ + 'isVideoConversionActive': boolean; + /** + * + * @type {boolean} + * @memberof AllJobStatusResponseDto + */ + 'isMachineLearningActive': boolean; +} /** * * @export @@ -683,10 +738,16 @@ export type DeviceTypeEnum = typeof DeviceTypeEnum[keyof typeof DeviceTypeEnum]; export interface ExifResponseDto { /** * - * @type {string} + * @type {number} * @memberof ExifResponseDto */ - 'id'?: string | null; + 'id'?: number | null; + /** + * + * @type {number} + * @memberof ExifResponseDto + */ + 'fileSizeInByte'?: number | null; /** * * @type {string} @@ -717,12 +778,6 @@ export interface ExifResponseDto { * @memberof ExifResponseDto */ 'exifImageHeight'?: number | null; - /** - * - * @type {number} - * @memberof ExifResponseDto - */ - 'fileSizeInByte'?: number | null; /** * * @type {string} @@ -828,6 +883,105 @@ export interface GetAssetCountByTimeBucketDto { */ 'timeGroup': TimeGroupEnum; } +/** + * + * @export + * @enum {string} + */ + +export const JobCommand = { + Start: 'start', + Stop: 'stop' +} as const; + +export type JobCommand = typeof JobCommand[keyof typeof JobCommand]; + + +/** + * + * @export + * @interface JobCommandDto + */ +export interface JobCommandDto { + /** + * + * @type {JobCommand} + * @memberof JobCommandDto + */ + 'command': JobCommand; +} +/** + * + * @export + * @interface JobCounts + */ +export interface JobCounts { + /** + * + * @type {number} + * @memberof JobCounts + */ + 'active': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'completed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'failed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'delayed': number; + /** + * + * @type {number} + * @memberof JobCounts + */ + 'waiting': number; +} +/** + * + * @export + * @enum {string} + */ + +export const JobId = { + ThumbnailGeneration: 'thumbnail-generation', + MetadataExtraction: 'metadata-extraction', + VideoConversion: 'video-conversion', + MachineLearning: 'machine-learning' +} as const; + +export type JobId = typeof JobId[keyof typeof JobId]; + + +/** + * + * @export + * @interface JobStatusResponseDto + */ +export interface JobStatusResponseDto { + /** + * + * @type {boolean} + * @memberof JobStatusResponseDto + */ + 'isActive': boolean; + /** + * + * @type {object} + * @memberof JobStatusResponseDto + */ + 'queueCount': object; +} /** * * @export @@ -3682,6 +3836,247 @@ export class DeviceInfoApi extends BaseAPI { } +/** + * JobApi - axios parameter creator + * @export + */ +export const JobApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllJobsStatus: async (options: AxiosRequestConfig = {}): Promise => { + const localVarPath = `/jobs`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {JobId} jobId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJobStatus: async (jobId: JobId, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'jobId' is not null or undefined + assertParamExists('getJobStatus', 'jobId', jobId) + const localVarPath = `/jobs/{jobId}` + .replace(`{${"jobId"}}`, encodeURIComponent(String(jobId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @param {JobId} jobId + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + sendJobCommand: async (jobId: JobId, jobCommandDto: JobCommandDto, options: AxiosRequestConfig = {}): Promise => { + // verify required parameter 'jobId' is not null or undefined + assertParamExists('sendJobCommand', 'jobId', jobId) + // verify required parameter 'jobCommandDto' is not null or undefined + assertParamExists('sendJobCommand', 'jobCommandDto', jobCommandDto) + const localVarPath = `/jobs/{jobId}` + .replace(`{${"jobId"}}`, encodeURIComponent(String(jobId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'PUT', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(jobCommandDto, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * JobApi - functional programming interface + * @export + */ +export const JobApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = JobApiAxiosParamCreator(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getAllJobsStatus(options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getAllJobsStatus(options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {JobId} jobId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async getJobStatus(jobId: JobId, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.getJobStatus(jobId, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + /** + * + * @param {JobId} jobId + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.sendJobCommand(jobId, jobCommandDto, options); + return createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration); + }, + } +}; + +/** + * JobApi - factory interface + * @export + */ +export const JobApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = JobApiFp(configuration) + return { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getAllJobsStatus(options?: any): AxiosPromise { + return localVarFp.getAllJobsStatus(options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {JobId} jobId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + getJobStatus(jobId: JobId, options?: any): AxiosPromise { + return localVarFp.getJobStatus(jobId, options).then((request) => request(axios, basePath)); + }, + /** + * + * @param {JobId} jobId + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: any): AxiosPromise { + return localVarFp.sendJobCommand(jobId, jobCommandDto, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * JobApi - object-oriented interface + * @export + * @class JobApi + * @extends {BaseAPI} + */ +export class JobApi extends BaseAPI { + /** + * + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JobApi + */ + public getAllJobsStatus(options?: AxiosRequestConfig) { + return JobApiFp(this.configuration).getAllJobsStatus(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {JobId} jobId + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JobApi + */ + public getJobStatus(jobId: JobId, options?: AxiosRequestConfig) { + return JobApiFp(this.configuration).getJobStatus(jobId, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @param {JobId} jobId + * @param {JobCommandDto} jobCommandDto + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof JobApi + */ + public sendJobCommand(jobId: JobId, jobCommandDto: JobCommandDto, options?: AxiosRequestConfig) { + return JobApiFp(this.configuration).sendJobCommand(jobId, jobCommandDto, options).then((request) => request(this.axios, this.basePath)); + } +} + + /** * ServerInfoApi - axios parameter creator * @export diff --git a/web/src/lib/components/admin-page/jobs/job-tile.svelte b/web/src/lib/components/admin-page/jobs/job-tile.svelte new file mode 100644 index 0000000000..5d09434f86 --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/job-tile.svelte @@ -0,0 +1,52 @@ + + +
+
+

{title}

+

{subtitle}

+

+ +

+ + + + + + + + + + + + + + + + +
StatusActiveWaiting
{jobStatus ? 'Active' : 'Idle'}{activeJobCount}{waitingJobCount}
+
+
+ +
+
diff --git a/web/src/lib/components/admin-page/jobs/jobs-panel.svelte b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte new file mode 100644 index 0000000000..54b3a4fc8d --- /dev/null +++ b/web/src/lib/components/admin-page/jobs/jobs-panel.svelte @@ -0,0 +1,138 @@ + + +
+ + + + + + Note that some asset does not have any object detected, this is normal. + +
diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte index b4daac2d5f..6ba5621b1e 100644 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte @@ -94,7 +94,7 @@
(isHover = true)} diff --git a/web/src/lib/models/admin-sidebar-selection.ts b/web/src/lib/models/admin-sidebar-selection.ts index a11914c96b..6ffe6ef4e1 100644 --- a/web/src/lib/models/admin-sidebar-selection.ts +++ b/web/src/lib/models/admin-sidebar-selection.ts @@ -1,5 +1,7 @@ export enum AdminSideBarSelection { - USER_MANAGEMENT = 'User management' + USER_MANAGEMENT = 'User management', + JOBS = 'Jobs', + SETTINGS = 'Settings' } export enum AppSideBarSelection { diff --git a/web/src/routes/admin/+layout.svelte b/web/src/routes/admin/+layout.svelte new file mode 100644 index 0000000000..7270e1de67 --- /dev/null +++ b/web/src/routes/admin/+layout.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 8d18519c2d..5bde9cadfa 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -4,6 +4,7 @@ import { AdminSideBarSelection } from '$lib/models/admin-sidebar-selection'; import SideBarButton from '$lib/components/shared-components/side-bar/side-bar-button.svelte'; import AccountMultipleOutline from 'svelte-material-icons/AccountMultipleOutline.svelte'; + import Cog from 'svelte-material-icons/Cog.svelte'; import NavigationBar from '$lib/components/shared-components/navigation-bar.svelte'; import UserManagement from '$lib/components/admin-page/user-management.svelte'; import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte'; @@ -12,6 +13,7 @@ import StatusBox from '$lib/components/shared-components/status-box.svelte'; import type { PageData } from './$types'; import { api, UserResponseDto } from '@api'; + import JobsPanel from '$lib/components/admin-page/jobs/jobs-panel.svelte'; let selectedAction: AdminSideBarSelection = AdminSideBarSelection.USER_MANAGEMENT; @@ -104,14 +106,21 @@ {/if}
-
+
+
@@ -132,6 +141,9 @@ on:edit-user={editUserHandler} /> {/if} + {#if selectedAction === AdminSideBarSelection.JOBS} + + {/if}
From a3aca4acb5f6830c7b43a8cbbf260e6f9d689321 Mon Sep 17 00:00:00 2001 From: Fynn Petersen-Frey Date: Thu, 6 Oct 2022 18:32:45 +0200 Subject: [PATCH 12/53] feat(mobile) Run background service after being killed (#789) --- .../android/app/src/main/AndroidManifest.xml | 13 ++++++---- .../com/example/mobile/AppClearedService.kt | 25 ------------------- .../example/mobile/BackgroundServicePlugin.kt | 15 +++++------ .../example/mobile/ContentObserverWorker.kt | 7 +++--- .../kotlin/com/example/mobile/ImmichApp.kt | 19 ++++++++++++++ .../kotlin/com/example/mobile/MainActivity.kt | 14 ++--------- 6 files changed, 40 insertions(+), 53 deletions(-) delete mode 100644 mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt create mode 100644 mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index af00aac413..36a967da6c 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,5 +1,5 @@ - - + + - - + + + diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt deleted file mode 100644 index bbdaa27f5f..0000000000 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/AppClearedService.kt +++ /dev/null @@ -1,25 +0,0 @@ -package app.alextran.immich - -import android.app.Service -import android.content.Intent -import android.os.IBinder - -/** - * Catches the event when either the system or the user kills the app - * (does not apply on force close!) - */ -class AppClearedService() : Service() { - - override fun onBind(intent: Intent): IBinder? { - return null - } - - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - return START_NOT_STICKY; - } - - override fun onTaskRemoved(rootIntent: Intent) { - ContentObserverWorker.workManagerAppClearedWorkaround(applicationContext) - stopSelf(); - } -} \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt index bebaa579be..3cb231eaf6 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt @@ -10,7 +10,7 @@ import io.flutter.plugin.common.MethodChannel * Android plugin for Dart `BackgroundService` * * Receives messages/method calls from the foreground Dart side to manage - * the background service, e.g. start (enqueue), stop (cancel) + * the background service, e.g. start (enqueue), stop (cancel) */ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { @@ -38,14 +38,15 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { val ctx = context!! - when(call.method) { + when (call.method) { "enable" -> { val args = call.arguments>()!! ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) - .apply() + .edit() + .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) + .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) + .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) + .apply() ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean) result.success(true) } @@ -54,7 +55,7 @@ class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { val requireUnmeteredNetwork = args.get(0) as Boolean val requireCharging = args.get(1) as Boolean ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging) - result.success(true) + result.success(true) } "disable" -> { ContentObserverWorker.disable(ctx) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt index ecbec640fa..a58ea14518 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt @@ -46,9 +46,6 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx * @param context Android Context */ fun enable(context: Context, immediate: Boolean = false) { - // migration to remove any old active background task - WorkManager.getInstance(context).cancelUniqueWork("immich/photoListener") - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) Log.d(TAG, "enabled ContentObserverWorker") if (immediate) { @@ -123,8 +120,10 @@ class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) } - private fun startBackupWorker(context: Context, delayMilliseconds: Long) { + fun startBackupWorker(context: Context, delayMilliseconds: Long) { val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) + return val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt new file mode 100644 index 0000000000..86b82d2be9 --- /dev/null +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt @@ -0,0 +1,19 @@ +package app.alextran.immich + +import android.app.Application +import androidx.work.Configuration +import androidx.work.WorkManager + +class ImmichApp : Application() { + override fun onCreate() { + super.onCreate() + val config = Configuration.Builder().build() + WorkManager.initialize(this, config) + // always start BackupWorker after WorkManager init; this fixes the following bug: + // After the process is killed (by user or system), the first trigger (taking a new picture) is lost. + // Thus, the BackupWorker is not started. If the system kills the process after each initialization + // (because of low memory etc.), the backup is never performed. + // As a workaround, we also run a backup check when initializing the application + ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) + } +} \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt index 2e6372231d..5df36cb18f 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt @@ -5,21 +5,11 @@ import io.flutter.embedding.engine.FlutterEngine import android.os.Bundle import android.content.Intent -class MainActivity: FlutterActivity() { +class MainActivity : FlutterActivity() { override fun configureFlutterEngine(flutterEngine: FlutterEngine) { super.configureFlutterEngine(flutterEngine) - flutterEngine.getPlugins().add(BackgroundServicePlugin()) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - try { - startService(Intent(getBaseContext(), AppClearedService::class.java)); - } catch (e: Exception) { - // startService must not be called when app is in background (crashes app) - // there is nothing we can do - } + flutterEngine.plugins.add(BackgroundServicePlugin()) } } From 3be46974871ee3d03dfc50211875436d22ed74b7 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 11:34:27 -0500 Subject: [PATCH 13/53] Added docker build and push with PR number as tag for easy testing in production environment --- .github/workflows/build_push_docker_staging.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml index 902a7a62df..f90fdbeb9a 100644 --- a/.github/workflows/build_push_docker_staging.yml +++ b/.github/workflows/build_push_docker_staging.yml @@ -38,6 +38,7 @@ jobs: push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }} tags: | altran1502/immich-server:staging + altran1502/immich-server:${{ github.event.pull_request.number }} build_and_push_machine_learning_staging: runs-on: ubuntu-latest @@ -67,6 +68,7 @@ jobs: push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }} tags: | altran1502/immich-machine-learning:staging + altran1502/immich-machine-learning:${{ github.event.pull_request.number }} build_and_push_web_staging: runs-on: ubuntu-latest @@ -96,6 +98,7 @@ jobs: push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }} tags: | altran1502/immich-web:staging + altran1502/immich-web:${{ github.event.pull_request.number }} build_and_push_nginx_staging: runs-on: ubuntu-latest @@ -124,3 +127,4 @@ jobs: push: ${{ github.event_name == 'pull_request' && github.repository == 'immich-app/immich' }} tags: | altran1502/immich-proxy:staging + altran1502/immich-proxy:${{ github.event.pull_request.number }} From 642811869c0f63f901759f0f33ebd54d65af5b40 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 11:38:56 -0500 Subject: [PATCH 14/53] Fixed staging action runs only in PR --- .github/workflows/build_push_docker_staging.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml index f90fdbeb9a..2e4f884a76 100644 --- a/.github/workflows/build_push_docker_staging.yml +++ b/.github/workflows/build_push_docker_staging.yml @@ -2,8 +2,6 @@ name: Build and Push Docker Image - Staging on: workflow_dispatch: - push: - branches: [main] pull_request: branches: [main] From 46994c33558c9d0b885937f71a69f90108b5debc Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 12:11:12 -0500 Subject: [PATCH 15/53] Up version for release --- mobile/android/fastlane/Fastfile | 4 ++-- .../fastlane/metadata/android/en-US/changelogs/49.txt | 2 ++ mobile/ios/fastlane/Fastfile | 2 +- mobile/pubspec.yaml | 2 +- server/apps/immich/src/constants/server_version.constant.ts | 6 +++--- 5 files changed, 9 insertions(+), 7 deletions(-) create mode 100644 mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 9441ea911d..bfa80f3aba 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" => 48, - "android.injected.version.name" => "1.30.2", + "android.injected.version.code" => 49, + "android.injected.version.name" => "1.31.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/android/fastlane/metadata/android/en-US/changelogs/49.txt b/mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt new file mode 100644 index 0000000000..90360a3c0c --- /dev/null +++ b/mobile/android/fastlane/metadata/android/en-US/changelogs/49.txt @@ -0,0 +1,2 @@ +* Fixed run background service after being killed +* Added background backup progress notifications \ No newline at end of file diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 581d7586eb..1a584c1ad8 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.30.2" + version_number: "1.31.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 4a47d9a2bb..0f5b8b3da6 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.30.2+48 +version: 1.31.0+49 environment: sdk: ">=2.17.0 <3.0.0" diff --git a/server/apps/immich/src/constants/server_version.constant.ts b/server/apps/immich/src/constants/server_version.constant.ts index 23e9fb49eb..072c6776a2 100644 --- a/server/apps/immich/src/constants/server_version.constant.ts +++ b/server/apps/immich/src/constants/server_version.constant.ts @@ -10,7 +10,7 @@ export interface IServerVersion { export const serverVersion: IServerVersion = { major: 1, - minor: 30, - patch: 2, - build: 48, + minor: 31, + patch: 0, + build: 49, }; From 471a60dcb024bbb6ef6f3978f316a7e4f5fd53bd Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 12:43:02 -0500 Subject: [PATCH 16/53] Added explicit type for job count --- mobile/openapi/doc/JobCounts.md | Bin 515 -> 515 bytes mobile/openapi/lib/model/job_counts.dart | Bin 4414 -> 4124 bytes .../all-job-status-response.dto.ts | 5 +++++ server/immich-openapi-specs.json | 2 +- 4 files changed, 6 insertions(+), 1 deletion(-) diff --git a/mobile/openapi/doc/JobCounts.md b/mobile/openapi/doc/JobCounts.md index 195ed86a6528371228670a047b9b215f8ba0e4fe..353b834382b1dbd0ec994edf9a02188c0f8e4dcb 100644 GIT binary patch delta 107 zcmZo>X=a)5&?YmlL`zGdMgfSpw6v1*a|?1(OHxz7Vp>|LvT2E#Iml9zWf>*qQI(V@ QW|m~;r6aUY4rN>i0IyLWasU7T delta 107 zcmZo>X=a)5&?c`mS4&HwMgfSpw6v1*a|?1(OHxz7Vp>|LvT2E#Iml9zWf>*qQI(V@ QW|m~;r6aUY4rN>i0JLu(ivR!s diff --git a/mobile/openapi/lib/model/job_counts.dart b/mobile/openapi/lib/model/job_counts.dart index 5c4f110f7e38740c708804a89fa22c016ed31bcf..dadb72f328bc75cf50c29acbb30baf8db6375563 100644 GIT binary patch delta 355 zcmdm|G)G~>Z${_Lyb^`Psa diff --git a/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts index aeb558acd5..884982bb5e 100644 --- a/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts +++ b/server/apps/immich/src/api-v1/job/response-dto/all-job-status-response.dto.ts @@ -1,10 +1,15 @@ import { ApiProperty } from '@nestjs/swagger'; export class JobCounts { + @ApiProperty({ type: 'integer' }) active!: number; + @ApiProperty({ type: 'integer' }) completed!: number; + @ApiProperty({ type: 'integer' }) failed!: number; + @ApiProperty({ type: 'integer' }) delayed!: number; + @ApiProperty({ type: 'integer' }) waiting!: number; } export class AllJobStatusResponseDto { diff --git a/server/immich-openapi-specs.json b/server/immich-openapi-specs.json index 955bfe7c88..59cc9b6a83 100644 --- a/server/immich-openapi-specs.json +++ b/server/immich-openapi-specs.json @@ -1 +1 @@ -{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"number"},"completed":{"type":"number"},"failed":{"type":"number"},"delayed":{"type":"number"},"waiting":{"type":"number"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file +{"openapi":"3.0.0","paths":{"/user":{"get":{"operationId":"getAllUsers","parameters":[{"name":"isAll","required":true,"in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}}}}}},"tags":["User"],"security":[{"bearer":[]}]},"post":{"operationId":"createUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateUserDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]},"put":{"operationId":"updateUser","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateUserDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/info/{userId}":{"get":{"operationId":"getUserById","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"]}},"/user/me":{"get":{"operationId":"getMyUserInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/count":{"get":{"operationId":"getUserCount","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/UserCountResponseDto"}}}}},"tags":["User"]}},"/user/profile-image":{"post":{"operationId":"createProfileImage","parameters":[],"requestBody":{"required":true,"description":"A new avatar for the user","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/CreateProfileImageDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateProfileImageResponseDto"}}}}},"tags":["User"],"security":[{"bearer":[]}]}},"/user/profile-image/{userId}":{"get":{"operationId":"getProfileImage","parameters":[{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["User"]}},"/asset/upload":{"post":{"operationId":"uploadFile","parameters":[],"requestBody":{"required":true,"description":"Asset Upload Information","content":{"multipart/form-data":{"schema":{"$ref":"#/components/schemas/AssetFileUploadDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetFileUploadResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/download":{"get":{"operationId":"downloadFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/file":{"get":{"operationId":"serveFile","parameters":[{"name":"aid","required":true,"in":"query","schema":{"title":"Device Asset ID","type":"string"}},{"name":"did","required":true,"in":"query","schema":{"title":"Device ID","type":"string"}},{"name":"isThumb","required":false,"in":"query","schema":{"title":"Is serve thumbnail (resize) file","type":"boolean"}},{"name":"isWeb","required":false,"in":"query","schema":{"title":"Is request made from web","type":"boolean"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/thumbnail/{assetId}":{"get":{"operationId":"getAssetThumbnail","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}},{"name":"format","required":false,"in":"query","schema":{"$ref":"#/components/schemas/ThumbnailFormat"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"object"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-objects":{"get":{"operationId":"getCuratedObjects","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedObjectsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/curated-locations":{"get":{"operationId":"getCuratedLocations","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/CuratedLocationsResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search-terms":{"get":{"operationId":"getAssetSearchTerms","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/search":{"post":{"operationId":"searchAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchAssetDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-time-bucket":{"post":{"operationId":"getAssetCountByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetCountByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByTimeBucketResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/count-by-user-id":{"get":{"operationId":"getAssetCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetCountByUserIdResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset":{"get":{"operationId":"getAllAssets","summary":"","description":"Get all AssetEntity belong to the user","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAsset","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeleteAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/DeleteAssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/time-bucket":{"post":{"operationId":"getAssetByTimeBucket","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/GetAssetByTimeBucketDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/{deviceId}":{"get":{"operationId":"getUserAssetsByDeviceId","summary":"","description":"Get all asset of a device that are in the database, ID only.","parameters":[{"name":"deviceId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"type":"string"}}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/assetById/{assetId}":{"get":{"operationId":"getAssetById","summary":"","description":"Get a single asset's information","parameters":[{"name":"assetId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/asset/check":{"post":{"operationId":"checkDuplicateAsset","summary":"","description":"Check duplicated asset before uploading - for Web upload used","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CheckDuplicateAssetResponseDto"}}}}},"tags":["Asset"],"security":[{"bearer":[]}]}},"/auth/login":{"post":{"operationId":"login","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginCredentialDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LoginResponseDto"}}}}},"tags":["Authentication"]}},"/auth/admin-sign-up":{"post":{"operationId":"adminSignUp","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SignUpDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminSignupResponseDto"}}}},"400":{"description":"The server already has an admin"}},"tags":["Authentication"]}},"/auth/validateToken":{"post":{"operationId":"validateAccessToken","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ValidateAccessTokenResponseDto"}}}}},"tags":["Authentication"],"security":[{"bearer":[]}]}},"/auth/logout":{"post":{"operationId":"logout","parameters":[],"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LogoutResponseDto"}}}}},"tags":["Authentication"]}},"/device-info":{"post":{"operationId":"createDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateDeviceInfoDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateDeviceInfo","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateDeviceInfoDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeviceInfoResponseDto"}}}}},"tags":["Device Info"],"security":[{"bearer":[]}]}},"/server-info":{"get":{"operationId":"getServerInfo","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerInfoResponseDto"}}}}},"tags":["Server Info"]}},"/server-info/ping":{"get":{"operationId":"pingServer","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerPingResponse"}}}}},"tags":["Server Info"]}},"/server-info/version":{"get":{"operationId":"getServerVersion","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ServerVersionReponseDto"}}}}},"tags":["Server Info"]}},"/album/count-by-user-id":{"get":{"operationId":"getAlbumCountByUserId","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumCountResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album":{"post":{"operationId":"createAlbum","parameters":[],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/CreateAlbumDto"}}}},"responses":{"201":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"get":{"operationId":"getAllAlbums","parameters":[{"name":"shared","required":false,"in":"query","schema":{"type":"boolean"}},{"name":"assetId","required":false,"in":"query","description":"Only returns albums that contain the asset\nIgnores the shared parameter\nundefined: get all albums","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"array","items":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/users":{"put":{"operationId":"addUsersToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddUsersDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/assets":{"put":{"operationId":"addAssetsToAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AddAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"removeAssetFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/RemoveAssetsDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}":{"get":{"operationId":"getAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]},"delete":{"operationId":"deleteAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]},"patch":{"operationId":"updateAlbumInfo","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/UpdateAlbumDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AlbumResponseDto"}}}}},"tags":["Album"],"security":[{"bearer":[]}]}},"/album/{albumId}/user/{userId}":{"delete":{"operationId":"removeUserFromAlbum","parameters":[{"name":"albumId","required":true,"in":"path","schema":{"type":"string"}},{"name":"userId","required":true,"in":"path","schema":{"type":"string"}}],"responses":{"200":{"description":""}},"tags":["Album"],"security":[{"bearer":[]}]}},"/jobs":{"get":{"operationId":"getAllJobsStatus","parameters":[],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/AllJobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}},"/jobs/{jobId}":{"get":{"operationId":"getJobStatus","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"responses":{"200":{"description":"","content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobStatusResponseDto"}}}}},"tags":["Job"],"security":[{"bearer":[]}]},"put":{"operationId":"sendJobCommand","parameters":[{"name":"jobId","required":true,"in":"path","schema":{"$ref":"#/components/schemas/JobId"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/JobCommandDto"}}}},"responses":{"200":{"description":"","content":{"application/json":{"schema":{"type":"number"}}}}},"tags":["Job"],"security":[{"bearer":[]}]}}},"info":{"title":"Immich","description":"Immich API","version":"1.17.0","contact":{}},"tags":[],"servers":[{"url":"/api"}],"components":{"securitySchemes":{"bearer":{"scheme":"Bearer","bearerFormat":"JWT","type":"http","name":"JWT","description":"Enter JWT token","in":"header"}},"schemas":{"UserResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"},"profileImagePath":{"type":"string"},"shouldChangePassword":{"type":"boolean"},"isAdmin":{"type":"boolean"}},"required":["id","email","firstName","lastName","createdAt","profileImagePath","shouldChangePassword","isAdmin"]},"CreateUserDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"John"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"UserCountResponseDto":{"type":"object","properties":{"userCount":{"type":"integer"}},"required":["userCount"]},"UpdateUserDto":{"type":"object","properties":{"id":{"type":"string"},"password":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"isAdmin":{"type":"boolean"},"shouldChangePassword":{"type":"boolean"},"profileImagePath":{"type":"string"}},"required":["id"]},"CreateProfileImageDto":{"type":"object","properties":{"file":{"type":"string","format":"binary"}},"required":["file"]},"CreateProfileImageResponseDto":{"type":"object","properties":{"userId":{"type":"string"},"profileImagePath":{"type":"string"}},"required":["userId","profileImagePath"]},"AssetFileUploadDto":{"type":"object","properties":{"assetData":{"type":"string","format":"binary"}},"required":["assetData"]},"AssetFileUploadResponseDto":{"type":"object","properties":{"id":{"type":"string"}},"required":["id"]},"ThumbnailFormat":{"type":"string","enum":["JPEG","WEBP"]},"CuratedObjectsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","object","resizePath","deviceAssetId","deviceId"]},"CuratedLocationsResponseDto":{"type":"object","properties":{"id":{"type":"string"},"city":{"type":"string"},"resizePath":{"type":"string"},"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["id","city","resizePath","deviceAssetId","deviceId"]},"SearchAssetDto":{"type":"object","properties":{"searchTerm":{"type":"string"}},"required":["searchTerm"]},"AssetTypeEnum":{"type":"string","enum":["IMAGE","VIDEO","AUDIO","OTHER"]},"ExifResponseDto":{"type":"object","properties":{"id":{"type":"integer","nullable":true,"default":null,"format":"int64"},"fileSizeInByte":{"type":"integer","nullable":true,"default":null,"format":"int64"},"make":{"type":"string","nullable":true,"default":null},"model":{"type":"string","nullable":true,"default":null},"imageName":{"type":"string","nullable":true,"default":null},"exifImageWidth":{"type":"number","nullable":true,"default":null},"exifImageHeight":{"type":"number","nullable":true,"default":null},"orientation":{"type":"string","nullable":true,"default":null},"dateTimeOriginal":{"format":"date-time","type":"string","nullable":true,"default":null},"modifyDate":{"format":"date-time","type":"string","nullable":true,"default":null},"lensModel":{"type":"string","nullable":true,"default":null},"fNumber":{"type":"number","nullable":true,"default":null},"focalLength":{"type":"number","nullable":true,"default":null},"iso":{"type":"number","nullable":true,"default":null},"exposureTime":{"type":"number","nullable":true,"default":null},"latitude":{"type":"number","nullable":true,"default":null},"longitude":{"type":"number","nullable":true,"default":null},"city":{"type":"string","nullable":true,"default":null},"state":{"type":"string","nullable":true,"default":null},"country":{"type":"string","nullable":true,"default":null}}},"SmartInfoResponseDto":{"type":"object","properties":{"id":{"type":"string"},"tags":{"nullable":true,"type":"array","items":{"type":"string"}},"objects":{"nullable":true,"type":"array","items":{"type":"string"}}}},"AssetResponseDto":{"type":"object","properties":{"type":{"$ref":"#/components/schemas/AssetTypeEnum"},"id":{"type":"string"},"deviceAssetId":{"type":"string"},"ownerId":{"type":"string"},"deviceId":{"type":"string"},"originalPath":{"type":"string"},"resizePath":{"type":"string","nullable":true},"createdAt":{"type":"string"},"modifiedAt":{"type":"string"},"isFavorite":{"type":"boolean"},"mimeType":{"type":"string","nullable":true},"duration":{"type":"string"},"webpPath":{"type":"string","nullable":true},"encodedVideoPath":{"type":"string","nullable":true},"exifInfo":{"$ref":"#/components/schemas/ExifResponseDto"},"smartInfo":{"$ref":"#/components/schemas/SmartInfoResponseDto"}},"required":["type","id","deviceAssetId","ownerId","deviceId","originalPath","resizePath","createdAt","modifiedAt","isFavorite","mimeType","duration","webpPath","encodedVideoPath"]},"TimeGroupEnum":{"type":"string","enum":["day","month"]},"GetAssetCountByTimeBucketDto":{"type":"object","properties":{"timeGroup":{"$ref":"#/components/schemas/TimeGroupEnum"}},"required":["timeGroup"]},"AssetCountByTimeBucket":{"type":"object","properties":{"timeBucket":{"type":"string"},"count":{"type":"integer"}},"required":["timeBucket","count"]},"AssetCountByTimeBucketResponseDto":{"type":"object","properties":{"totalCount":{"type":"integer"},"buckets":{"type":"array","items":{"$ref":"#/components/schemas/AssetCountByTimeBucket"}}},"required":["totalCount","buckets"]},"AssetCountByUserIdResponseDto":{"type":"object","properties":{"photos":{"type":"integer"},"videos":{"type":"integer"}},"required":["photos","videos"]},"GetAssetByTimeBucketDto":{"type":"object","properties":{"timeBucket":{"title":"Array of date time buckets","example":["2015-06-01T00:00:00.000Z","2016-02-01T00:00:00.000Z","2016-03-01T00:00:00.000Z"],"type":"array","items":{"type":"string"}}},"required":["timeBucket"]},"DeleteAssetDto":{"type":"object","properties":{"ids":{"title":"Array of asset IDs to delete","example":["bf973405-3f2a-48d2-a687-2ed4167164be","dd41870b-5d00-46d2-924e-1d8489a0aa0f","fad77c3f-deef-4e7e-9608-14c1aa4e559a"],"type":"array","items":{"type":"string"}}},"required":["ids"]},"DeleteAssetStatus":{"type":"string","enum":["SUCCESS","FAILED"]},"DeleteAssetResponseDto":{"type":"object","properties":{"status":{"$ref":"#/components/schemas/DeleteAssetStatus"},"id":{"type":"string"}},"required":["status","id"]},"CheckDuplicateAssetDto":{"type":"object","properties":{"deviceAssetId":{"type":"string"},"deviceId":{"type":"string"}},"required":["deviceAssetId","deviceId"]},"CheckDuplicateAssetResponseDto":{"type":"object","properties":{"isExist":{"type":"boolean"},"id":{"type":"string"}},"required":["isExist"]},"LoginCredentialDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"}},"required":["email","password"]},"LoginResponseDto":{"type":"object","properties":{"accessToken":{"type":"string","readOnly":true},"userId":{"type":"string","readOnly":true},"userEmail":{"type":"string","readOnly":true},"firstName":{"type":"string","readOnly":true},"lastName":{"type":"string","readOnly":true},"profileImagePath":{"type":"string","readOnly":true},"isAdmin":{"type":"boolean","readOnly":true},"shouldChangePassword":{"type":"boolean","readOnly":true}},"required":["accessToken","userId","userEmail","firstName","lastName","profileImagePath","isAdmin","shouldChangePassword"]},"SignUpDto":{"type":"object","properties":{"email":{"type":"string","example":"testuser@email.com"},"password":{"type":"string","example":"password"},"firstName":{"type":"string","example":"Admin"},"lastName":{"type":"string","example":"Doe"}},"required":["email","password","firstName","lastName"]},"AdminSignupResponseDto":{"type":"object","properties":{"id":{"type":"string"},"email":{"type":"string"},"firstName":{"type":"string"},"lastName":{"type":"string"},"createdAt":{"type":"string"}},"required":["id","email","firstName","lastName","createdAt"]},"ValidateAccessTokenResponseDto":{"type":"object","properties":{"authStatus":{"type":"boolean"}},"required":["authStatus"]},"LogoutResponseDto":{"type":"object","properties":{"successful":{"type":"boolean","readOnly":true}},"required":["successful"]},"DeviceTypeEnum":{"type":"string","enum":["IOS","ANDROID","WEB"]},"CreateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"DeviceInfoResponseDto":{"type":"object","properties":{"id":{"type":"integer"},"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"userId":{"type":"string"},"deviceId":{"type":"string"},"createdAt":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["id","deviceType","userId","deviceId","createdAt","isAutoBackup"]},"UpdateDeviceInfoDto":{"type":"object","properties":{"deviceType":{"$ref":"#/components/schemas/DeviceTypeEnum"},"deviceId":{"type":"string"},"isAutoBackup":{"type":"boolean"}},"required":["deviceType","deviceId"]},"ServerInfoResponseDto":{"type":"object","properties":{"diskSizeRaw":{"type":"integer","format":"int64"},"diskUseRaw":{"type":"integer","format":"int64"},"diskAvailableRaw":{"type":"integer","format":"int64"},"diskUsagePercentage":{"type":"number","format":"float"},"diskSize":{"type":"string"},"diskUse":{"type":"string"},"diskAvailable":{"type":"string"}},"required":["diskSizeRaw","diskUseRaw","diskAvailableRaw","diskUsagePercentage","diskSize","diskUse","diskAvailable"]},"ServerPingResponse":{"type":"object","properties":{"res":{"type":"string","readOnly":true,"example":"pong"}},"required":["res"]},"ServerVersionReponseDto":{"type":"object","properties":{"major":{"type":"integer"},"minor":{"type":"integer"},"patch":{"type":"integer"},"build":{"type":"integer"}},"required":["major","minor","patch","build"]},"AlbumCountResponseDto":{"type":"object","properties":{"owned":{"type":"integer"},"shared":{"type":"integer"},"sharing":{"type":"integer"}},"required":["owned","shared","sharing"]},"CreateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"sharedWithUserIds":{"type":"array","items":{"type":"string"}},"assetIds":{"type":"array","items":{"type":"string"}}},"required":["albumName"]},"AlbumResponseDto":{"type":"object","properties":{"assetCount":{"type":"integer"},"id":{"type":"string"},"ownerId":{"type":"string"},"albumName":{"type":"string"},"createdAt":{"type":"string"},"albumThumbnailAssetId":{"type":"string","nullable":true},"shared":{"type":"boolean"},"sharedUsers":{"type":"array","items":{"$ref":"#/components/schemas/UserResponseDto"}},"assets":{"type":"array","items":{"$ref":"#/components/schemas/AssetResponseDto"}}},"required":["assetCount","id","ownerId","albumName","createdAt","albumThumbnailAssetId","shared","sharedUsers","assets"]},"AddUsersDto":{"type":"object","properties":{"sharedUserIds":{"type":"array","items":{"type":"string"}}},"required":["sharedUserIds"]},"AddAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"RemoveAssetsDto":{"type":"object","properties":{"assetIds":{"type":"array","items":{"type":"string"}}},"required":["assetIds"]},"UpdateAlbumDto":{"type":"object","properties":{"albumName":{"type":"string"},"albumThumbnailAssetId":{"type":"string"}}},"JobCounts":{"type":"object","properties":{"active":{"type":"integer"},"completed":{"type":"integer"},"failed":{"type":"integer"},"delayed":{"type":"integer"},"waiting":{"type":"integer"}},"required":["active","completed","failed","delayed","waiting"]},"AllJobStatusResponseDto":{"type":"object","properties":{"thumbnailGenerationQueueCount":{"$ref":"#/components/schemas/JobCounts"},"metadataExtractionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"videoConversionQueueCount":{"$ref":"#/components/schemas/JobCounts"},"machineLearningQueueCount":{"$ref":"#/components/schemas/JobCounts"},"isThumbnailGenerationActive":{"type":"boolean"},"isMetadataExtractionActive":{"type":"boolean"},"isVideoConversionActive":{"type":"boolean"},"isMachineLearningActive":{"type":"boolean"}},"required":["thumbnailGenerationQueueCount","metadataExtractionQueueCount","videoConversionQueueCount","machineLearningQueueCount","isThumbnailGenerationActive","isMetadataExtractionActive","isVideoConversionActive","isMachineLearningActive"]},"JobId":{"type":"string","enum":["thumbnail-generation","metadata-extraction","video-conversion","machine-learning"]},"JobStatusResponseDto":{"type":"object","properties":{"isActive":{"type":"boolean"},"queueCount":{"type":"object"}},"required":["isActive","queueCount"]},"JobCommand":{"type":"string","enum":["start","stop"]},"JobCommandDto":{"type":"object","properties":{"command":{"$ref":"#/components/schemas/JobCommand"}},"required":["command"]}}}} \ No newline at end of file From dd0f40559dd39a7893559736ec4aa98faf916090 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 14:59:54 -0500 Subject: [PATCH 17/53] added github action file' --- .github/workflows/openapi-generator.yml | 31 +++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 .github/workflows/openapi-generator.yml diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml new file mode 100644 index 0000000000..db429a4129 --- /dev/null +++ b/.github/workflows/openapi-generator.yml @@ -0,0 +1,31 @@ +name: Generate OpenAPI SDK + +on: + workflow_dispatch: + push: + branches: [main] + +jobs: + generate-typescript-axios: + runs-on: ubuntu-latest + name: Example + steps: + # Checkout your code + - name: Checkout + uses: actions/checkout@v2 + + # Generate your OpenAPI document (if you don't write it manually) + + # Use the action to generate a client package + # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace. + - name: Generate Typescript Axios Client + uses: openapi-generators/openapitools-generator-action@v1 + with: + generator: typescript-axios + generator-tag: v6.2.0 + openapi-file: server/immich-openapi-specs.json + + # Do something with the generated client (likely publishing it somewhere) + - name: Do something with the client + run: | + cd typescript-axios-client && ls From fd06aa2135c27d3bdaf25a279d4f035f99ab8425 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:04:24 -0500 Subject: [PATCH 18/53] Add workflow to PR to test --- .github/workflows/openapi-generator.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index db429a4129..20a2e3116b 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -2,6 +2,7 @@ name: Generate OpenAPI SDK on: workflow_dispatch: + pull_request: push: branches: [main] From a94b443f131ae08b06abd84209af1b233a11847c Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:11:09 -0500 Subject: [PATCH 19/53] Push to typescript sdk repo --- .github/workflows/openapi-generator.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 20a2e3116b..0bb34c5266 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -9,7 +9,7 @@ on: jobs: generate-typescript-axios: runs-on: ubuntu-latest - name: Example + name: OpenAPI Generator steps: # Checkout your code - name: Checkout @@ -29,4 +29,4 @@ jobs: # Do something with the generated client (likely publishing it somewhere) - name: Do something with the client run: | - cd typescript-axios-client && ls + cd typescript-axios-client && /bin/sh git_push.sh immich-app typescript-axios-sdk "update" "github.com" From f9af61a5caf74047157fc9438333ed9856df6b31 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:21:17 -0500 Subject: [PATCH 20/53] Manually push to repo --- .github/workflows/openapi-generator.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 0bb34c5266..eb6cdd2220 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -14,8 +14,10 @@ jobs: # Checkout your code - name: Checkout uses: actions/checkout@v2 - - # Generate your OpenAPI document (if you don't write it manually) + run: | + git config --global init.defaultBranch main + git config --global user.email "immichbot@immich.app" + git config --global user.name "Immich Bot" # Use the action to generate a client package # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace. @@ -29,4 +31,10 @@ jobs: # Do something with the generated client (likely publishing it somewhere) - name: Do something with the client run: | - cd typescript-axios-client && /bin/sh git_push.sh immich-app typescript-axios-sdk "update" "github.com" + cd typescript-axios-client + git init + git add . + git commit -m "Update SDK" + git remote add origin git@github.com:immich-app/typescript-axios-sdk.git + git pull origin main + git push origin main From 6804e3dc736d6632cd093628e0855eb5ce379ef0 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:27:31 -0500 Subject: [PATCH 21/53] Fixed --- .github/workflows/openapi-generator.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index eb6cdd2220..8794105d59 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -14,10 +14,6 @@ jobs: # Checkout your code - name: Checkout uses: actions/checkout@v2 - run: | - git config --global init.defaultBranch main - git config --global user.email "immichbot@immich.app" - git config --global user.name "Immich Bot" # Use the action to generate a client package # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace. @@ -31,6 +27,9 @@ jobs: # Do something with the generated client (likely publishing it somewhere) - name: Do something with the client run: | + git config --global init.defaultBranch main + git config --global user.email "immichbot@immich.app" + git config --global user.name "Immich Bot" cd typescript-axios-client git init git add . From 3228882fc0f152592d07c1c59d568f614d03b271 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:32:19 -0500 Subject: [PATCH 22/53] Authenticate --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 8794105d59..8845650cd1 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -34,6 +34,6 @@ jobs: git init git add . git commit -m "Update SDK" - git remote add origin git@github.com:immich-app/typescript-axios-sdk.git + git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@$github.com/immich-app/typescript-axios-sdk.git git pull origin main git push origin main From 2cb7517f6493682ed289fab8517ec4c6ce68c2ce Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:33:07 -0500 Subject: [PATCH 23/53] Fix url --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 8845650cd1..17de7d4265 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -34,6 +34,6 @@ jobs: git init git add . git commit -m "Update SDK" - git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@$github.com/immich-app/typescript-axios-sdk.git + git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git git pull origin main git push origin main From 9a6d29d6e740d2be24400e911849d68b733fa268 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:34:42 -0500 Subject: [PATCH 24/53] Add global config for git --- .github/workflows/openapi-generator.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 17de7d4265..8d361256a4 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -28,6 +28,7 @@ jobs: - name: Do something with the client run: | git config --global init.defaultBranch main + git config --global pull.rebase false git config --global user.email "immichbot@immich.app" git config --global user.name "Immich Bot" cd typescript-axios-client From 77312ce2e087e4b9e2968f8132d82434d10fa725 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:37:03 -0500 Subject: [PATCH 25/53] Force push --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 8d361256a4..625739dc5a 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -37,4 +37,4 @@ jobs: git commit -m "Update SDK" git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git git pull origin main - git push origin main + git push origin main --force From 9e54e30011930e0cbd841aae025e7e7c51e1ac31 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:38:38 -0500 Subject: [PATCH 26/53] git push force --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 625739dc5a..08d9d73826 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -37,4 +37,4 @@ jobs: git commit -m "Update SDK" git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git git pull origin main - git push origin main --force + git push origin main 2>&1 | grep -v 'To https' From fc255b558de0586c69f5a559ab408c9a190e7a3f Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:40:01 -0500 Subject: [PATCH 27/53] fix --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 08d9d73826..d1d3dc3334 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -36,5 +36,5 @@ jobs: git add . git commit -m "Update SDK" git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git - git pull origin main + git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' From 0e509ceafa839a7997a51f9f01e287226850c655 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:45:30 -0500 Subject: [PATCH 28/53] Added permissionf or github bot --- .github/workflows/openapi-generator.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index d1d3dc3334..1673e2ca29 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -6,6 +6,9 @@ on: push: branches: [main] +permissions: + repository-projects: write + jobs: generate-typescript-axios: runs-on: ubuntu-latest From fdac5af5eeb54c375ab1cc073dbc1869f9193c02 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:47:47 -0500 Subject: [PATCH 29/53] Added github token --- .github/workflows/openapi-generator.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 1673e2ca29..761f7f3cf1 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -6,9 +6,6 @@ on: push: branches: [main] -permissions: - repository-projects: write - jobs: generate-typescript-axios: runs-on: ubuntu-latest @@ -17,6 +14,7 @@ jobs: # Checkout your code - name: Checkout uses: actions/checkout@v2 + token: ${{ secrets.GH_TOKEN }} # Use the action to generate a client package # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace. From 0e3fb41e73dc98cb1172af8bf192ef6c88c6cdd0 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 15:48:38 -0500 Subject: [PATCH 30/53] fixed --- .github/workflows/openapi-generator.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 761f7f3cf1..cf49da829f 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -13,8 +13,9 @@ jobs: steps: # Checkout your code - name: Checkout - uses: actions/checkout@v2 - token: ${{ secrets.GH_TOKEN }} + uses: actions/checkout@v3 + with: + token: ${{ secrets.GH_TOKEN }} # Use the action to generate a client package # This uses the default path for the openapi document and thus assumes there is an openapi.json in the current workspace. From 6355a07dc4b7d085139277b0950d5c07b350d783 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 16:09:15 -0500 Subject: [PATCH 31/53] Added github token custom --- .github/workflows/openapi-generator.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index cf49da829f..5bb98967be 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -37,6 +37,6 @@ jobs: git init git add . git commit -m "Update SDK" - git remote add origin https://immich-app:"${{ secrets.GITHUB_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git + git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' From c4e32ce159fb55b083850eb9373fe8986c6833ae Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 16:15:36 -0500 Subject: [PATCH 32/53] Rename repo --- .github/workflows/openapi-generator.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 5bb98967be..0f2899dab1 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -31,12 +31,12 @@ jobs: run: | git config --global init.defaultBranch main git config --global pull.rebase false - git config --global user.email "immichbot@immich.app" - git config --global user.name "Immich Bot" + git config --global user.email "alex.tran1502@gmail.com" + git config --global user.name "Alex Tran" cd typescript-axios-client git init git add . git commit -m "Update SDK" - git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/typescript-axios-sdk.git + git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-typescript-axios.git git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' From c3d7dda61ff6e621f78e02c67839df8d6f959cda Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Thu, 6 Oct 2022 17:23:05 -0500 Subject: [PATCH 33/53] Added generation for dart --- .github/workflows/openapi-generator.yml | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 0f2899dab1..6526726758 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -27,7 +27,7 @@ jobs: openapi-file: server/immich-openapi-specs.json # Do something with the generated client (likely publishing it somewhere) - - name: Do something with the client + - name: Push to typescript repo run: | git config --global init.defaultBranch main git config --global pull.rebase false @@ -40,3 +40,18 @@ jobs: git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-typescript-axios.git git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' + + - name: Generate Dart Client + uses: openapi-generators/openapitools-generator-action@v1 + with: + generator: dart + generator-tag: v6.2.0 + openapi-file: server/immich-openapi-specs.json + + - name: Push to Dart repo + run: | + git config --global init.defaultBranch main + git config --global pull.rebase false + git config --global user.email "alex.tran1502@gmail.com" + git config --global user.name "Alex Tran" + cd dart-client && ls From 38767cad0f37dbb28d087a9cced3fe4e3d0b75ba Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Fri, 7 Oct 2022 12:14:27 +0200 Subject: [PATCH 34/53] Update local-reverse-geocoder to 0.12.5 This version includes a fix to the error handling in that library, which was causing our code to silently fail and loop. See https://github.com/tomayac/local-reverse-geocoder/issues/58 for more detail. --- server/package-lock.json | 16 +++++++--------- server/package.json | 2 +- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 62ebb7fd08..9cb57e7d52 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -36,7 +36,7 @@ "geo-tz": "^7.0.2", "i18n-iso-countries": "^7.5.0", "joi": "^17.5.0", - "local-reverse-geocoder": "^0.12.2", + "local-reverse-geocoder": "^0.12.5", "lodash": "^4.17.21", "luxon": "^3.0.3", "passport": "^0.6.0", @@ -7661,12 +7661,11 @@ } }, "node_modules/local-reverse-geocoder": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.12.2.tgz", - "integrity": "sha512-kTSvDxGTuJoqx619jmHFoGCqFpBi0PPwyd7PDOLZCyo8mMEwJSMx713+ksOCihGpzUfO3hcclE7z/T43sY/IaA==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.12.5.tgz", + "integrity": "sha512-FaH8+T29K9PQRiiqYlt+M9Qvq9GlSnWEnX0FTDXgPrNzQ9SWWYGEvO5uODwAD6sep9z19u/K/+Z3cw4AGVW97Q==", "dependencies": { "async": "^3.2.4", - "cors": "^2.8.5", "csv-parse": "^5.3.0", "debug": "^4.3.4", "kdt": "^0.1.0", @@ -17109,12 +17108,11 @@ "dev": true }, "local-reverse-geocoder": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.12.2.tgz", - "integrity": "sha512-kTSvDxGTuJoqx619jmHFoGCqFpBi0PPwyd7PDOLZCyo8mMEwJSMx713+ksOCihGpzUfO3hcclE7z/T43sY/IaA==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/local-reverse-geocoder/-/local-reverse-geocoder-0.12.5.tgz", + "integrity": "sha512-FaH8+T29K9PQRiiqYlt+M9Qvq9GlSnWEnX0FTDXgPrNzQ9SWWYGEvO5uODwAD6sep9z19u/K/+Z3cw4AGVW97Q==", "requires": { "async": "^3.2.4", - "cors": "^2.8.5", "csv-parse": "^5.3.0", "debug": "^4.3.4", "kdt": "^0.1.0", diff --git a/server/package.json b/server/package.json index 62c113b7db..7d7f5ea2b2 100644 --- a/server/package.json +++ b/server/package.json @@ -55,7 +55,7 @@ "geo-tz": "^7.0.2", "i18n-iso-countries": "^7.5.0", "joi": "^17.5.0", - "local-reverse-geocoder": "^0.12.2", + "local-reverse-geocoder": "^0.12.5", "lodash": "^4.17.21", "luxon": "^3.0.3", "passport": "^0.6.0", From cdddcad784c89c09d6e5012caccfeb4383f23c3d Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 08:47:13 -0500 Subject: [PATCH 35/53] fix(server): Delete encoded video when deleting file --- .../modules/background-task/background-task.processor.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/server/apps/immich/src/modules/background-task/background-task.processor.ts b/server/apps/immich/src/modules/background-task/background-task.processor.ts index a5fa1c5e06..d852f734c2 100644 --- a/server/apps/immich/src/modules/background-task/background-task.processor.ts +++ b/server/apps/immich/src/modules/background-task/background-task.processor.ts @@ -46,6 +46,14 @@ export class BackgroundTaskProcessor { } }); } + + if (asset.encodedVideoPath) { + fs.unlink(asset.encodedVideoPath, (err) => { + if (err) { + console.log('error deleting ', asset.encodedVideoPath); + } + }); + } } } } From 4f7e764fa0169a87a3ede75392ee6b3590397c56 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 09:15:05 -0500 Subject: [PATCH 36/53] Fix typing --- .../metadata-extraction.processor.ts | 36 ++++++++++++------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index 27f2688d74..465b707304 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -20,7 +20,7 @@ import ffmpeg from 'fluent-ffmpeg'; import path from 'path'; import sharp from 'sharp'; import { Repository } from 'typeorm/repository/Repository'; -import geocoder, { InitOptions } from 'local-reverse-geocoder'; +import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder'; import { getName } from 'i18n-iso-countries'; import { find } from 'geo-tz'; import * as luxon from 'luxon'; @@ -39,20 +39,20 @@ function geocoderLookup(points: { latitude: number; longitude: number }[]) { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore geocoder.lookUp(points, 1, (err, addresses) => { - resolve(addresses[0][0]); + resolve(addresses[0][0] as GeoData); }); }); } const geocodingPrecisionLevels = ['cities15000', 'cities5000', 'cities1000', 'cities500']; -export interface AdminCode { +export type AdminCode = { name: string; asciiName: string; geoNameId: string; -} +}; -export interface GeoData { +export type GeoData = { geoNameId: string; name: string; asciiName: string; @@ -63,8 +63,8 @@ export interface GeoData { featureCode: string; countryCode: string; cc2?: any; - admin1Code?: AdminCode; - admin2Code?: AdminCode; + admin1Code?: AdminCode | string; + admin2Code?: AdminCode | string; admin3Code?: any; admin4Code?: any; population: string; @@ -73,7 +73,7 @@ export interface GeoData { timezone: string; modificationDate: string; distance: number; -} +}; @Processor(QueueNameEnum.METADATA_EXTRACTION) export class MetadataExtractionProcessor { @@ -123,10 +123,22 @@ export class MetadataExtractionProcessor { const city = geoCodeInfo.name; let state = ''; - if (geoCodeInfo.admin2Code?.name) state += geoCodeInfo.admin2Code.name; - if (geoCodeInfo.admin1Code?.name) { - if (geoCodeInfo.admin2Code?.name) state += ', '; - state += geoCodeInfo.admin1Code.name; + + if (geoCodeInfo.admin2Code) { + const adminCode2 = geoCodeInfo.admin2Code as AdminCode; + state += adminCode2.name; + } + + if (geoCodeInfo.admin1Code) { + const adminCode1 = geoCodeInfo.admin1Code as AdminCode; + + if (geoCodeInfo.admin2Code) { + const adminCode2 = geoCodeInfo.admin2Code as AdminCode; + if (adminCode2.name) { + state += ', '; + } + } + state += adminCode1.name; } return { country, state, city }; From 82b8313da0b9d7a323e2cbedc0026d40e6f2c2c8 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 09:16:45 -0500 Subject: [PATCH 37/53] Fix test --- .../src/processors/metadata-extraction.processor.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/apps/microservices/src/processors/metadata-extraction.processor.ts b/server/apps/microservices/src/processors/metadata-extraction.processor.ts index 465b707304..b247215d43 100644 --- a/server/apps/microservices/src/processors/metadata-extraction.processor.ts +++ b/server/apps/microservices/src/processors/metadata-extraction.processor.ts @@ -20,7 +20,7 @@ import ffmpeg from 'fluent-ffmpeg'; import path from 'path'; import sharp from 'sharp'; import { Repository } from 'typeorm/repository/Repository'; -import geocoder, { AddressObject, InitOptions } from 'local-reverse-geocoder'; +import geocoder, { InitOptions } from 'local-reverse-geocoder'; import { getName } from 'i18n-iso-countries'; import { find } from 'geo-tz'; import * as luxon from 'luxon'; From 17085dd8a014154bcab34e80765f9ff7559e710e Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 09:39:22 -0500 Subject: [PATCH 38/53] Added SDK to Rust --- .github/workflows/openapi-generator.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 6526726758..d773a20dc1 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -54,4 +54,10 @@ jobs: git config --global pull.rebase false git config --global user.email "alex.tran1502@gmail.com" git config --global user.name "Alex Tran" - cd dart-client && ls + cd dart-client + git init + git add . + git commit -m "Update SDK" + git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-rust.git + git pull origin main --allow-unrelated-histories + git push origin main 2>&1 | grep -v 'To https' From 14dc6793325e555e997d6d59ae2b4df23d7cb10b Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 09:46:10 -0500 Subject: [PATCH 39/53] Added SDK to Rust --- .github/workflows/openapi-generator.yml | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index d773a20dc1..7b59ac87be 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -41,7 +41,7 @@ jobs: git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' - - name: Generate Dart Client + - name: Generate Dart SDK uses: openapi-generators/openapitools-generator-action@v1 with: generator: dart @@ -58,6 +58,27 @@ jobs: git init git add . git commit -m "Update SDK" + git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-dart.git + git pull origin main --allow-unrelated-histories + git push origin main 2>&1 | grep -v 'To https' + + - name: Generate Rust SDK + uses: openapi-generators/openapitools-generator-action@v1 + with: + generator: rust + generator-tag: v6.2.0 + openapi-file: server/immich-openapi-specs.json + + - name: Push to Rust repo + run: | + git config --global init.defaultBranch main + git config --global pull.rebase false + git config --global user.email "alex.tran1502@gmail.com" + git config --global user.name "Alex Tran" + cd rust-client + git init + git add . + git commit -m "Update SDK" git remote add origin https://immich-app:"${{ secrets.GH_TOKEN }}"@github.com/immich-app/immich-sdk-rust.git git pull origin main --allow-unrelated-histories git push origin main 2>&1 | grep -v 'To https' From c28863966b658c4c0ed5a1acc7235d0c394045f5 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 09:50:04 -0500 Subject: [PATCH 40/53] Remove build on PR --- .github/workflows/openapi-generator.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/openapi-generator.yml b/.github/workflows/openapi-generator.yml index 7b59ac87be..91731a0256 100644 --- a/.github/workflows/openapi-generator.yml +++ b/.github/workflows/openapi-generator.yml @@ -2,7 +2,6 @@ name: Generate OpenAPI SDK on: workflow_dispatch: - pull_request: push: branches: [main] From a2882a490856b55fd17db08e77cd4140cfca4d22 Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 14:26:16 -0500 Subject: [PATCH 41/53] Added additional type to enum of openapi --- .../apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts | 1 + server/apps/immich/src/api-v1/job/dto/get-job.dto.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts index a0df22d238..547281b221 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts @@ -9,6 +9,7 @@ export enum GetAssetThumbnailFormatEnum { export class GetAssetThumbnailDto { @IsOptional() @ApiProperty({ + type: String, enum: GetAssetThumbnailFormatEnum, default: GetAssetThumbnailFormatEnum.WEBP, required: false, diff --git a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts index 38289ed134..95d037e670 100644 --- a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts +++ b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts @@ -14,6 +14,7 @@ export class GetJobDto { message: `params must be one of ${Object.values(JobId).join()}`, }) @ApiProperty({ + type: String, enum: JobId, enumName: 'JobId', }) From 9bfacaa39ae21089c0388724db74cd91c8a743cb Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Fri, 7 Oct 2022 14:30:15 -0500 Subject: [PATCH 42/53] Specific specific type for enum value for openapi generator to work correctly --- .../apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts | 2 +- server/apps/immich/src/api-v1/job/dto/get-job.dto.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts index 547281b221..5a8dc06872 100644 --- a/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts +++ b/server/apps/immich/src/api-v1/asset/dto/get-asset-thumbnail.dto.ts @@ -15,5 +15,5 @@ export class GetAssetThumbnailDto { required: false, enumName: 'ThumbnailFormat', }) - format = GetAssetThumbnailFormatEnum.WEBP; + format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP; } diff --git a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts index 95d037e670..8280c66940 100644 --- a/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts +++ b/server/apps/immich/src/api-v1/job/dto/get-job.dto.ts @@ -18,5 +18,5 @@ export class GetJobDto { enum: JobId, enumName: 'JobId', }) - jobId!: string; + jobId!: JobId; } From 342c3254cb1a2fea8bb513c235835991c975ae82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?= <18481195+AnTheMaker@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:54:21 +0200 Subject: [PATCH 43/53] add z-index to #account-info-panel --- web/src/lib/components/shared-components/navigation-bar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar.svelte index 60f3c637e2..bdae32478d 100644 --- a/web/src/lib/components/shared-components/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar.svelte @@ -119,7 +119,7 @@ in:fade={{ duration: 100 }} out:fade={{ duration: 100 }} id="account-info-panel" - class="absolute right-[25px] top-[75px] bg-white shadow-lg rounded-2xl w-[360px] text-center" + class="absolute right-[25px] top-[75px] bg-white shadow-lg rounded-2xl w-[360px] text-center z-[100]" use:clickOutside on:out-click={() => (shouldShowAccountInfoPanel = false)} > From 435548558194a3f16e6e66c0dcea037082a479e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?An=20=7C=20Anton=20R=C3=B6hm?= <18481195+AnTheMaker@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:55:10 +0200 Subject: [PATCH 44/53] lower z-index of #immich-scrubbable-scrollbar --- .../lib/components/shared-components/scrollbar/scrollbar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte index 6ba5621b1e..4cba23d8ca 100644 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte @@ -94,7 +94,7 @@
(isHover = true)} From ec74feea5afad9a0a289457700120cbc018db4c9 Mon Sep 17 00:00:00 2001 From: Werner Date: Mon, 10 Oct 2022 07:46:23 +0200 Subject: [PATCH 45/53] Typo/minor cosmetics --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 305f32648d..dd6015870a 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM # Features -> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development, there will be continuous functions, features and api changes. +> ⚠️ WARNING: **NOT READY FOR PRODUCTION! DO NOT USE TO STORE YOUR ASSETS**. This project is under heavy development. There will be continuous functions, features and api changes. | Features | Mobile | Web | | - | - | - | @@ -118,11 +118,11 @@ There are several services that compose Immich: NOTE: When using a reverse proxy in front of Immich (such as NGINX), the reverse proxy might require extra configuration to allow large files to be uploaded (such as client_max_body_size in the case of NGINX). -## Testing One-step installation (not recommended for production) +## Testing one-step installation (not recommended for production) -> ⚠️ *This installation method is for evaluating Immich before futher customization to meet the users' needs.* +> ⚠️ *This installation method is for evaluating Immich before further customization to meet the users' needs.* -*Applicable system: Ubuntu, Debian, MacOS* +*Applicable operating systems: Ubuntu, Debian, MacOS* - In the shell, from the directory of your choice, run the following command: @@ -204,7 +204,7 @@ docker-compose pull && docker-compose up -d | - | - | - | | Get it on F-Droid |

|

| -> *The Play/App Store version might be lagging behind the latest release due to the review process.* +> *The Play/App Store version might be lagging behind the latest release due to their review process.* # App Beta release channel From 0c4968dc302194d9aac46e4d3d404f667424801b Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Tue, 11 Oct 2022 21:13:37 +0200 Subject: [PATCH 46/53] Fix: Remove default JWT_SECRET value in .env --- docker/.env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index 33b4925514..dc09e9da91 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -38,7 +38,10 @@ LOG_LEVEL=simple # JWT SECRET ################################################################################### -JWT_SECRET=randomstringthatissolongandpowerfulthatnoonecanguess +# This JWT_SECRET is used to sign the authentication keys for user login +# You should set it to a long randomly generated value +# You can use this command to generate one: openssl rand -base64 128 +#JWT_SECRET= ################################################################################### # Reverse Geocoding From 00549eed790c7c99b01ddea3bd7e065739f34a98 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 12 Oct 2022 09:18:43 +0200 Subject: [PATCH 47/53] Uncomment JWT_SECRET in default .env Co-authored-by: Alex --- docker/.env.example | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/.env.example b/docker/.env.example index dc09e9da91..7b398969da 100644 --- a/docker/.env.example +++ b/docker/.env.example @@ -41,7 +41,7 @@ LOG_LEVEL=simple # This JWT_SECRET is used to sign the authentication keys for user login # You should set it to a long randomly generated value # You can use this command to generate one: openssl rand -base64 128 -#JWT_SECRET= +JWT_SECRET= ################################################################################### # Reverse Geocoding From 9869b92c2b3269d3eaa6c145a0ed30d61d2cdba2 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Wed, 12 Oct 2022 09:34:10 +0200 Subject: [PATCH 48/53] Generate random JWT_SECRET value in install.sh --- install.sh | 33 +++++++++++++++++++-------------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index 1701d33cc2..dbe0d8db6a 100755 --- a/install.sh +++ b/install.sh @@ -18,33 +18,37 @@ get_release_version() { create_immich_directory() { echo "Creating Immich directory..." mkdir -p ./immich-app/immich-data + cd ./immich-app } download_docker_compose_file() { echo "Downloading docker-compose.yml..." - curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./immich-app/docker-compose.yml >/dev/null 2>&1 + curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1 } download_dot_env_file() { echo "Downloading .env file..." - curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./immich-app/.env >/dev/null 2>&1 + curl -L https://raw.githubusercontent.com/immich-app/immich/$release_version/docker/.env.example -o ./.env >/dev/null 2>&1 +} + +replace_env_value() { + if [[ "$OSTYPE" == "darwin"* ]]; then + sed -i '' "s|$1=.*|$1=$2|" ./.env + else + sed -i "s|$1=.*|$1=$2|" ./.env + fi } populate_upload_location() { echo "Populating default UPLOAD_LOCATION value..." + upload_location=$(pwd)/immich-data + replace_env_value "UPLOAD_LOCATION" $upload_location +} - cd ./immich-app/immich-data - - upload_location=$(pwd) - - # Replace value of UPLOAD_LOCATION in .env with upload_location path - if [[ "$OSTYPE" == "darwin"* ]]; then - sed -i '' "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env - else - sed -i "s|UPLOAD_LOCATION=.*|UPLOAD_LOCATION=$upload_location|" ../.env - fi - - cd .. +generate_jwt_secret() { + echo "Generating JWT_SECRET value..." + jwt_secret=$(openssl rand -base64 128) + replace_env_value "JWT_SECRET" $jwt_secret } start_docker_compose() { @@ -88,4 +92,5 @@ create_immich_directory download_docker_compose_file download_dot_env_file populate_upload_location +generate_jwt_secret start_docker_compose From c6cbee6563d77f73710509e08f5d389d3016db52 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 10:47:05 +0000 Subject: [PATCH 49/53] chore(deps): bump docker/setup-buildx-action from 2.0.0 to 2.1.0 Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v2.0.0...v2.1.0) --- updated-dependencies: - dependency-name: docker/setup-buildx-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build_push_docker_latest.yml | 8 ++++---- .github/workflows/build_push_docker_staging.yml | 8 ++++---- .github/workflows/build_push_server_release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_push_docker_latest.yml b/.github/workflows/build_push_docker_latest.yml index 2fb4792366..a253e5cde4 100644 --- a/.github/workflows/build_push_docker_latest.yml +++ b/.github/workflows/build_push_docker_latest.yml @@ -20,7 +20,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -48,7 +48,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -75,7 +75,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -103,7 +103,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 with: diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml index 2e4f884a76..c81c4263e2 100644 --- a/.github/workflows/build_push_docker_staging.yml +++ b/.github/workflows/build_push_docker_staging.yml @@ -20,7 +20,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub if: ${{ github.repository == 'immich-app/immich' }} uses: docker/login-action@v2 @@ -50,7 +50,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub if: ${{ github.repository == 'immich-app/immich' }} uses: docker/login-action@v2 @@ -79,7 +79,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub if: ${{ github.repository == 'immich-app/immich' }} uses: docker/login-action@v2 @@ -109,7 +109,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub if: ${{ github.repository == 'immich-app/immich' }} uses: docker/login-action@v2 diff --git a/.github/workflows/build_push_server_release.yml b/.github/workflows/build_push_server_release.yml index 0db75d6b70..6b5ca879a0 100644 --- a/.github/workflows/build_push_server_release.yml +++ b/.github/workflows/build_push_server_release.yml @@ -26,7 +26,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 @@ -61,7 +61,7 @@ jobs: uses: docker/setup-qemu-action@v2.0.0 - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 with: @@ -98,7 +98,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 @@ -138,7 +138,7 @@ jobs: - name: Set up Docker Buildx id: buildx - uses: docker/setup-buildx-action@v2.0.0 + uses: docker/setup-buildx-action@v2.1.0 - name: Login to Docker Hub uses: docker/login-action@v2 From 64b1d4ca3b0b4d738ec99181d08e10e73681f252 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 12 Oct 2022 10:47:08 +0000 Subject: [PATCH 50/53] chore(deps): bump docker/build-push-action from 3.1.1 to 3.2.0 Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 3.1.1 to 3.2.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v3.1.1...v3.2.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build_push_docker_latest.yml | 8 ++++---- .github/workflows/build_push_docker_staging.yml | 8 ++++---- .github/workflows/build_push_server_release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_push_docker_latest.yml b/.github/workflows/build_push_docker_latest.yml index 2fb4792366..46aa5b1f93 100644 --- a/.github/workflows/build_push_docker_latest.yml +++ b/.github/workflows/build_push_docker_latest.yml @@ -27,7 +27,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Immich Mono Repo - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./server file: ./server/Dockerfile @@ -55,7 +55,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Machine Learning - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./machine-learning file: ./machine-learning/Dockerfile @@ -82,7 +82,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Web - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./web file: ./web/Dockerfile @@ -110,7 +110,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Proxy - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./nginx file: ./nginx/Dockerfile diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml index 2e4f884a76..fea4afb94e 100644 --- a/.github/workflows/build_push_docker_staging.yml +++ b/.github/workflows/build_push_docker_staging.yml @@ -28,7 +28,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push Immich Mono Repo - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./server file: ./server/Dockerfile @@ -58,7 +58,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Machine Learning - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./machine-learning file: ./machine-learning/Dockerfile @@ -87,7 +87,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Web - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./web file: ./web/Dockerfile @@ -117,7 +117,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Proxy - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./nginx file: ./nginx/Dockerfile diff --git a/.github/workflows/build_push_server_release.yml b/.github/workflows/build_push_server_release.yml index 0db75d6b70..f4af1ef8f2 100644 --- a/.github/workflows/build_push_server_release.yml +++ b/.github/workflows/build_push_server_release.yml @@ -35,7 +35,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push immich-server release - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./server file: ./server/Dockerfile @@ -68,7 +68,7 @@ jobs: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and Push Machine Learning - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./machine-learning file: ./machine-learning/Dockerfile @@ -107,7 +107,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push immich-web release - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./web file: ./web/Dockerfile @@ -147,7 +147,7 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build and push immich-proxy release - uses: docker/build-push-action@v3.1.1 + uses: docker/build-push-action@v3.2.0 with: context: ./nginx file: ./nginx/Dockerfile From fee652dfd750a28ed85d9b3548372981a8e7150d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 13 Oct 2022 10:55:56 +0000 Subject: [PATCH 51/53] chore(deps): bump docker/setup-qemu-action from 2.0.0 to 2.1.0 Bumps [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action) from 2.0.0 to 2.1.0. - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v2.0.0...v2.1.0) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] --- .github/workflows/build_push_docker_latest.yml | 8 ++++---- .github/workflows/build_push_docker_staging.yml | 8 ++++---- .github/workflows/build_push_server_release.yml | 8 ++++---- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build_push_docker_latest.yml b/.github/workflows/build_push_docker_latest.yml index fca5692f03..b69d5916f2 100644 --- a/.github/workflows/build_push_docker_latest.yml +++ b/.github/workflows/build_push_docker_latest.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -45,7 +45,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -72,7 +72,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -100,7 +100,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 diff --git a/.github/workflows/build_push_docker_staging.yml b/.github/workflows/build_push_docker_staging.yml index 18fbca94bd..36f2f1f568 100644 --- a/.github/workflows/build_push_docker_staging.yml +++ b/.github/workflows/build_push_docker_staging.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -47,7 +47,7 @@ jobs: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -76,7 +76,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -106,7 +106,7 @@ jobs: with: fetch-depth: 0 - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 diff --git a/.github/workflows/build_push_server_release.yml b/.github/workflows/build_push_server_release.yml index 8a072e42b8..3f9a7a74fd 100644 --- a/.github/workflows/build_push_server_release.yml +++ b/.github/workflows/build_push_server_release.yml @@ -22,7 +22,7 @@ jobs: fallback: latest - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx @@ -58,7 +58,7 @@ jobs: with: fallback: latest - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx uses: docker/setup-buildx-action@v2.1.0 @@ -94,7 +94,7 @@ jobs: fallback: latest - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx @@ -134,7 +134,7 @@ jobs: fallback: latest - name: Set up QEMU - uses: docker/setup-qemu-action@v2.0.0 + uses: docker/setup-qemu-action@v2.1.0 - name: Set up Docker Buildx id: buildx From c03f860f8e30e96b77e01618fd6c0a409d9cee5c Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 13 Oct 2022 21:54:29 +0200 Subject: [PATCH 52/53] Log a warning if JWT_SECRET key does not have enough bits --- server/libs/common/src/config/app.config.ts | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/server/libs/common/src/config/app.config.ts b/server/libs/common/src/config/app.config.ts index 1e657403e0..be3a006b75 100644 --- a/server/libs/common/src/config/app.config.ts +++ b/server/libs/common/src/config/app.config.ts @@ -1,5 +1,20 @@ +import { Logger } from '@nestjs/common'; import { ConfigModuleOptions } from '@nestjs/config'; import Joi from 'joi'; +import { createSecretKey, generateKeySync } from 'node:crypto' + +const jwtSecretValidator: Joi.CustomValidator = (value, _) => { + const key = createSecretKey(value, "base64") + const keySizeBits = (key.symmetricKeySize ?? 0) * 8 + + if (keySizeBits < 128) { + const newKey = generateKeySync('hmac', { length: 256 }).export().toString('base64') + Logger.warn("The current JWT_SECRET key is insecure. It should be at least 128 bits long!") + Logger.warn(`Here is a new, securely generated key that you can use instead: ${newKey}`) + } + + return value; +} export const immichAppConfig: ConfigModuleOptions = { envFilePath: '.env', @@ -9,7 +24,7 @@ export const immichAppConfig: ConfigModuleOptions = { DB_USERNAME: Joi.string().required(), DB_PASSWORD: Joi.string().required(), DB_DATABASE_NAME: Joi.string().required(), - JWT_SECRET: Joi.string().required(), + JWT_SECRET: Joi.string().required().custom(jwtSecretValidator), DISABLE_REVERSE_GEOCODING: Joi.boolean().optional().valid(true, false).default(false), REVERSE_GEOCODING_PRECISION: Joi.number().optional().valid(0,1,2,3).default(3), LOG_LEVEL: Joi.string().optional().valid('simple', 'verbose').default('simple'), From 87ba99755b52692a98bcebfb7fca3cb0bcef1957 Mon Sep 17 00:00:00 2001 From: bo0tzz Date: Thu, 13 Oct 2022 22:17:31 +0200 Subject: [PATCH 53/53] Remove unused variable --- server/libs/common/src/config/app.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/libs/common/src/config/app.config.ts b/server/libs/common/src/config/app.config.ts index be3a006b75..04aae2733e 100644 --- a/server/libs/common/src/config/app.config.ts +++ b/server/libs/common/src/config/app.config.ts @@ -3,7 +3,7 @@ import { ConfigModuleOptions } from '@nestjs/config'; import Joi from 'joi'; import { createSecretKey, generateKeySync } from 'node:crypto' -const jwtSecretValidator: Joi.CustomValidator = (value, _) => { +const jwtSecretValidator: Joi.CustomValidator = (value, ) => { const key = createSecretKey(value, "base64") const keySizeBits = (key.symmetricKeySize ?? 0) * 8