From e407a4fa134640f0113d6db83d3693b4d1e86398 Mon Sep 17 00:00:00 2001
From: Alex <alex.tran1502@gmail.com>
Date: Tue, 22 Mar 2022 01:22:04 -0500
Subject: [PATCH] Get thumbnail from app (#68)

* Renamed multipart filed name 'files' to 'assetData'.
* Added an additional field name of 'thumbnailData' to multipart form.
* Implemented upload mechanism for thumbnail directly from the mobile client.
* Removed dead code
* Implemented a version checking mechanism.
---
 .DS_Store                                     | Bin 6148 -> 0 bytes
 .gitignore                                    |   1 +
 PR_CHECKLIST.md                               |  13 ++
 README.md                                     |   5 +-
 docker/docker-compose.gpu.yml                 |   2 +-
 docker/docker-compose.yml                     |   2 +-
 mobile/ios/Podfile.lock                       |   6 +
 mobile/ios/fastlane/Fastfile                  |   3 +
 mobile/lib/main.dart                          |   6 +
 .../modules/home/ui/immich_sliver_appbar.dart |  50 +++++++-
 .../lib/modules/home/ui/profile_drawer.dart   |  75 +++++++++--
 mobile/lib/modules/home/views/home_page.dart  |   2 +
 .../lib/routing/tab_navigation_observer.dart  |   3 +
 .../models/server_info_state.model.dart       |  78 ++++++++++++
 .../shared/models/server_version.model.dart   |  72 +++++++++++
 .../providers/server_info.provider.dart       | 108 +++++++++-------
 .../lib/shared/services/backup.service.dart   |  54 ++++++--
 .../shared/services/server_info.service.dart  |   7 ++
 mobile/pubspec.yaml                           |   2 +-
 server/Dockerfile                             |   4 +-
 server/src/api-v1/asset/asset.controller.ts   |  31 +++--
 server/src/api-v1/asset/asset.module.ts       |   3 +
 server/src/api-v1/asset/asset.service.ts      |   6 +
 .../server-info/server-info.controller.ts     |   6 +
 server/src/config/multer-option.config.ts     |  28 ++++-
 .../src/constants/server_version.constant.ts  |   9 ++
 .../background-task.processor.ts              |   1 -
 .../image-optimize.processor.ts               | 118 ------------------
 .../image-optimize/image-optimize.service.ts  |  29 -----
 29 files changed, 480 insertions(+), 244 deletions(-)
 delete mode 100644 .DS_Store
 create mode 100644 .gitignore
 create mode 100644 PR_CHECKLIST.md
 create mode 100644 mobile/lib/shared/models/server_info_state.model.dart
 create mode 100644 mobile/lib/shared/models/server_version.model.dart
 create mode 100644 server/src/constants/server_version.constant.ts

diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 7c5d996ae92cafef4a40cc3338fab00ced5433e1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 6148
zcmeHK%}T>S5T0#o(@=yS6g)0?Em)-%#7n690!H+pQX3O%FlMD`?V%KM))(?gd>&_Z
zH$rH=YLOY3{dVVPf4-F64giS$G~5Sh0YC$lu;OB~LdZ|LBo%9^BMLpo2-0Ady#$F0
zmZI6PjttPZt3wD8WDrC7`+Wp2Nt{hOoljAz);8*n<2p^})_+tpe=?j*v;J^=O`{8?
zlA!1hgUdLY4O&|#DxD1DG#cxKIE*ml<|<A@HS4Qs8fH4zHxAcrxP#WteBM3kwPo+H
zvuMlt(LtvzyS?sW;WoB+_m0m8Pswwt-VBife{)(kEf(;Oozj`Y>^=!_j}%aHJfLT#
z<JBLhiAqP9K&#Z@859J_Py$gg+mfT0t=JbW$HELS1I)lWGGOniMspoE$9rN1n1MfK
zfbIv0O6Xb44eF}{8(jh**3hg4`&3I%j<o1m%nhOkMVM4XlPYY9Axt{jrH%6}<_1kV
z2wQvzyRxtqiqNa${8ER5@C<Ux3@`&_2C8P*qWAyo=lZ{##3N>a8TeNWh}x-t+Q%c=
zw{_`o^wvt$J5&;i%ME^~U`Id27)wv_4yqQkOX?tc7ITAWLE%3Fng(u|fnR0d3qv?#
AUjP6A

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000000..496ee2ca6a
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.DS_Store
\ No newline at end of file
diff --git a/PR_CHECKLIST.md b/PR_CHECKLIST.md
new file mode 100644
index 0000000000..e7d8b9a6f8
--- /dev/null
+++ b/PR_CHECKLIST.md
@@ -0,0 +1,13 @@
+# Deployment checklist for iOS/Android/Server
+
+[] Up version in [mobile/pubspec.yml](/mobile/pubspec.yaml)
+
+[] Up version in [docker/docker-compose.yml](/docker/docker-compose.yml) for `immich_server` service
+
+[] Up version in [docker/docker-compose.gpu.yml](/docker/docker-compose.gpu.yml) for `immich_server` service
+
+[] Up version in [server/src/constants/server_version.constant.ts](/server/src/constants/server_version.constant.ts)
+
+[] Up version in iOS Fastlane [/mobile/ios/fastlane/Fastfile](/mobile/ios/fastlane/Fastfile)
+
+All of the version should be the same.
\ No newline at end of file
diff --git a/README.md b/README.md
index 9c6c9f6d1f..01b0c3d540 100644
--- a/README.md
+++ b/README.md
@@ -47,8 +47,8 @@ This project is under heavy development, there will be continous functions, feat
 
 # Features
 
-- Upload assets(videos/images).
-- View assets.
+- Upload and view assets(videos/images).
+- Multi-user supported.
 - Quick navigation with drag scroll bar.
 - Auto Backup.
 - Support HEIC/HEIF Backup.
@@ -59,6 +59,7 @@ This project is under heavy development, there will be continous functions, feat
 - Upload assets from your local computer/server using [immich cli tools](https://www.npmjs.com/package/immich)
 - [Optional] Reserve geocoding using Mapbox (Generous free-tier of 100,000 search/month)
 - Show asset's location information on map (OpenStreetMap).
+- Show curated places on the search page
 
 # Development
 
diff --git a/docker/docker-compose.gpu.yml b/docker/docker-compose.gpu.yml
index 3b64483db2..ff7ad1d4ee 100644
--- a/docker/docker-compose.gpu.yml
+++ b/docker/docker-compose.gpu.yml
@@ -2,7 +2,7 @@ version: "3.8"
 
 services:
   immich_server:
-    image: immich-server-dev:1.0.0
+    image: immich-server-dev:1.3.0
     build:
       context: ../server
       target: development
diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 8298eac721..f72e0bc722 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -2,7 +2,7 @@ version: "3.8"
 
 services:
   immich_server:
-    image: immich-server-dev:1.0.0
+    image: immich-server-dev:1.3.0
     build:
       context: ../server
       target: development
diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock
index dd0793fe49..d9cabe49a7 100644
--- a/mobile/ios/Podfile.lock
+++ b/mobile/ios/Podfile.lock
@@ -9,6 +9,8 @@ PODS:
   - FMDB (2.7.5):
     - FMDB/standard (= 2.7.5)
   - FMDB/standard (2.7.5)
+  - package_info_plus (0.4.5):
+    - Flutter
   - path_provider_ios (0.0.1):
     - Flutter
   - photo_manager (1.0.0):
@@ -28,6 +30,7 @@ DEPENDENCIES:
   - Flutter (from `Flutter`)
   - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`)
   - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
+  - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
   - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
   - photo_manager (from `.symlinks/plugins/photo_manager/ios`)
   - sqflite (from `.symlinks/plugins/sqflite/ios`)
@@ -47,6 +50,8 @@ EXTERNAL SOURCES:
     :path: ".symlinks/plugins/flutter_udid/ios"
   fluttertoast:
     :path: ".symlinks/plugins/fluttertoast/ios"
+  package_info_plus:
+    :path: ".symlinks/plugins/package_info_plus/ios"
   path_provider_ios:
     :path: ".symlinks/plugins/path_provider_ios/ios"
   photo_manager:
@@ -63,6 +68,7 @@ SPEC CHECKSUMS:
   flutter_udid: 0848809dbed4c055175747ae6a45a8b4f6771e1c
   fluttertoast: 6122fa75143e992b1d3470f61000f591a798cc58
   FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a
+  package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e
   path_provider_ios: 7d7ce634493af4477d156294792024ec3485acd5
   photo_manager: 84fa94fbeb82e607333ea9a13c43b58e0903a463
   SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 3f8509d639..ea193c1f31 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -18,6 +18,9 @@ default_platform(:ios)
 platform :ios do
   desc "iOS deployment"
   lane :beta do
+    increment_version_number(
+      version_number: "1.3.0" # Set a specific version number
+    )
     increment_build_number({
       build_number: latest_testflight_build_number + 1
     })
diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart
index f57849f2ea..194cd42a96 100644
--- a/mobile/lib/main.dart
+++ b/mobile/lib/main.dart
@@ -7,6 +7,7 @@ import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/routing/tab_navigation_observer.dart';
 import 'package:immich_mobile/shared/providers/app_state.provider.dart';
 import 'package:immich_mobile/shared/providers/backup.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 import 'constants/hive_box.dart';
 import 'package:google_fonts/google_fonts.dart';
@@ -43,7 +44,10 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
         ref.watch(backupProvider.notifier).resumeBackup();
         ref.watch(websocketProvider.notifier).connect();
         ref.watch(assetProvider.notifier).getAllAsset();
+        ref.watch(serverInfoProvider.notifier).getServerVersion();
+
         break;
+
       case AppLifecycleState.inactive:
         debugPrint("[APP STATE] inactive");
         ref.watch(appStateProvider.notifier).state = AppStateEnum.inactive;
@@ -51,10 +55,12 @@ class _ImmichAppState extends ConsumerState<ImmichApp> with WidgetsBindingObserv
         ref.watch(backupProvider.notifier).cancelBackup();
 
         break;
+
       case AppLifecycleState.paused:
         debugPrint("[APP STATE] paused");
         ref.watch(appStateProvider.notifier).state = AppStateEnum.paused;
         break;
+
       case AppLifecycleState.detached:
         debugPrint("[APP STATE] detached");
         ref.watch(appStateProvider.notifier).state = AppStateEnum.detached;
diff --git a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart
index 59e898f358..72d95b854c 100644
--- a/mobile/lib/modules/home/ui/immich_sliver_appbar.dart
+++ b/mobile/lib/modules/home/ui/immich_sliver_appbar.dart
@@ -7,7 +7,9 @@ import 'package:immich_mobile/modules/login/providers/authentication.provider.da
 
 import 'package:immich_mobile/routing/router.dart';
 import 'package:immich_mobile/shared/models/backup_state.model.dart';
+import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 import 'package:immich_mobile/shared/providers/backup.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 
 class ImmichSliverAppBar extends ConsumerWidget {
   const ImmichSliverAppBar({
@@ -21,6 +23,8 @@ class ImmichSliverAppBar extends ConsumerWidget {
   Widget build(BuildContext context, WidgetRef ref) {
     final BackUpState _backupState = ref.watch(backupProvider);
     bool _isEnableAutoBackup = ref.watch(authenticationProvider).deviceInfo.isAutoBackup;
+    final ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
+
     return SliverAppBar(
       centerTitle: true,
       floating: true,
@@ -30,12 +34,46 @@ class ImmichSliverAppBar extends ConsumerWidget {
       shape: const RoundedRectangleBorder(borderRadius: BorderRadius.all(Radius.circular(5))),
       leading: Builder(
         builder: (BuildContext context) {
-          return IconButton(
-            icon: const Icon(Icons.account_circle_rounded),
-            onPressed: () {
-              Scaffold.of(context).openDrawer();
-            },
-            tooltip: MaterialLocalizations.of(context).openAppDrawerTooltip,
+          return Stack(
+            children: [
+              Positioned(
+                top: 5,
+                child: IconButton(
+                  splashRadius: 25,
+                  icon: const Icon(
+                    Icons.account_circle_rounded,
+                    size: 30,
+                  ),
+                  onPressed: () {
+                    Scaffold.of(context).openDrawer();
+                  },
+                ),
+              ),
+              _serverInfoState.isVersionMismatch
+                  ? Positioned(
+                      bottom: 12,
+                      right: 12,
+                      child: GestureDetector(
+                        onTap: () => Scaffold.of(context).openDrawer(),
+                        child: Material(
+                          color: Colors.grey[200],
+                          elevation: 1,
+                          shape: RoundedRectangleBorder(
+                            borderRadius: BorderRadius.circular(50.0),
+                          ),
+                          child: const Padding(
+                            padding: EdgeInsets.all(2.0),
+                            child: Icon(
+                              Icons.info,
+                              color: Color.fromARGB(255, 243, 188, 106),
+                              size: 15,
+                            ),
+                          ),
+                        ),
+                      ),
+                    )
+                  : Container(),
+            ],
           );
         },
       ),
diff --git a/mobile/lib/modules/home/ui/profile_drawer.dart b/mobile/lib/modules/home/ui/profile_drawer.dart
index 6e6e406280..c9a586fc17 100644
--- a/mobile/lib/modules/home/ui/profile_drawer.dart
+++ b/mobile/lib/modules/home/ui/profile_drawer.dart
@@ -5,7 +5,9 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
 import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
 import 'package:immich_mobile/modules/login/models/authentication_state.model.dart';
 import 'package:immich_mobile/modules/login/providers/authentication.provider.dart';
+import 'package:immich_mobile/shared/models/server_info_state.model.dart';
 import 'package:immich_mobile/shared/providers/backup.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 import 'package:package_info_plus/package_info_plus.dart';
 
@@ -15,6 +17,8 @@ class ProfileDrawer extends HookConsumerWidget {
   @override
   Widget build(BuildContext context, WidgetRef ref) {
     AuthenticationState _authState = ref.watch(authenticationProvider);
+    ServerInfoState _serverInfoState = ref.watch(serverInfoProvider);
+
     final appInfo = useState({});
 
     _getPackageInfo() async {
@@ -92,13 +96,70 @@ class ProfileDrawer extends HookConsumerWidget {
           ),
           Padding(
             padding: const EdgeInsets.all(8.0),
-            child: Text(
-              "Version V${appInfo.value["version"]}+${appInfo.value["buildNumber"]}",
-              style: TextStyle(
-                fontSize: 12,
-                color: Colors.grey[400],
-                fontWeight: FontWeight.bold,
-                fontStyle: FontStyle.italic,
+            child: Card(
+              color: Colors.grey[100],
+              child: Padding(
+                padding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 8),
+                child: Column(
+                  crossAxisAlignment: CrossAxisAlignment.center,
+                  children: [
+                    Padding(
+                      padding: const EdgeInsets.all(8.0),
+                      child: Text(
+                        _serverInfoState.isVersionMismatch
+                            ? _serverInfoState.versionMismatchErrorMessage
+                            : "Client and Server are up-to-date",
+                        textAlign: TextAlign.center,
+                        style:
+                            TextStyle(fontSize: 11, color: Theme.of(context).primaryColor, fontWeight: FontWeight.w600),
+                      ),
+                    ),
+                    const Divider(),
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Text(
+                          "App Version",
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: Colors.grey[500],
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                        Text(
+                          "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}",
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: Colors.grey[500],
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      ],
+                    ),
+                    const Divider(),
+                    Row(
+                      mainAxisAlignment: MainAxisAlignment.spaceBetween,
+                      children: [
+                        Text(
+                          "Server Version",
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: Colors.grey[500],
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                        Text(
+                          "${_serverInfoState.serverVersion.major}.${_serverInfoState.serverVersion.minor}.${_serverInfoState.serverVersion.patch}",
+                          style: TextStyle(
+                            fontSize: 11,
+                            color: Colors.grey[500],
+                            fontWeight: FontWeight.bold,
+                          ),
+                        ),
+                      ],
+                    ),
+                  ],
+                ),
               ),
             ),
           )
diff --git a/mobile/lib/modules/home/views/home_page.dart b/mobile/lib/modules/home/views/home_page.dart
index 7a64013313..792b286572 100644
--- a/mobile/lib/modules/home/views/home_page.dart
+++ b/mobile/lib/modules/home/views/home_page.dart
@@ -11,6 +11,7 @@ import 'package:immich_mobile/modules/home/ui/immich_sliver_appbar.dart';
 import 'package:immich_mobile/modules/home/ui/monthly_title_text.dart';
 import 'package:immich_mobile/modules/home/ui/profile_drawer.dart';
 import 'package:immich_mobile/modules/home/providers/asset.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 import 'package:immich_mobile/shared/providers/websocket.provider.dart';
 import 'package:sliver_tools/sliver_tools.dart';
 
@@ -28,6 +29,7 @@ class HomePage extends HookConsumerWidget {
     useEffect(() {
       ref.read(websocketProvider.notifier).connect();
       ref.read(assetProvider.notifier).getAllAsset();
+      ref.watch(serverInfoProvider.notifier).getServerVersion();
       return null;
     }, []);
 
diff --git a/mobile/lib/routing/tab_navigation_observer.dart b/mobile/lib/routing/tab_navigation_observer.dart
index 70f6304156..1d2de0f0fb 100644
--- a/mobile/lib/routing/tab_navigation_observer.dart
+++ b/mobile/lib/routing/tab_navigation_observer.dart
@@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart';
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 
 import 'package:immich_mobile/modules/search/providers/search_page_state.provider.dart';
+import 'package:immich_mobile/shared/providers/server_info.provider.dart';
 
 class TabNavigationObserver extends AutoRouterObserver {
   /// Riverpod Instance
@@ -26,5 +27,7 @@ class TabNavigationObserver extends AutoRouterObserver {
       // Refresh Location State
       ref.refresh(getCuratedLocationProvider);
     }
+
+    ref.watch(serverInfoProvider.notifier).getServerVersion();
   }
 }
diff --git a/mobile/lib/shared/models/server_info_state.model.dart b/mobile/lib/shared/models/server_info_state.model.dart
new file mode 100644
index 0000000000..5c6074ee74
--- /dev/null
+++ b/mobile/lib/shared/models/server_info_state.model.dart
@@ -0,0 +1,78 @@
+import 'dart:convert';
+
+import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
+import 'package:immich_mobile/shared/models/server_version.model.dart';
+
+class ServerInfoState {
+  final MapboxInfo mapboxInfo;
+  final ServerVersion serverVersion;
+  final bool isVersionMismatch;
+  final String versionMismatchErrorMessage;
+
+  ServerInfoState({
+    required this.mapboxInfo,
+    required this.serverVersion,
+    required this.isVersionMismatch,
+    required this.versionMismatchErrorMessage,
+  });
+
+  ServerInfoState copyWith({
+    MapboxInfo? mapboxInfo,
+    ServerVersion? serverVersion,
+    bool? isVersionMismatch,
+    String? versionMismatchErrorMessage,
+  }) {
+    return ServerInfoState(
+      mapboxInfo: mapboxInfo ?? this.mapboxInfo,
+      serverVersion: serverVersion ?? this.serverVersion,
+      isVersionMismatch: isVersionMismatch ?? this.isVersionMismatch,
+      versionMismatchErrorMessage: versionMismatchErrorMessage ?? this.versionMismatchErrorMessage,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'mapboxInfo': mapboxInfo.toMap(),
+      'serverVersion': serverVersion.toMap(),
+      'isVersionMismatch': isVersionMismatch,
+      'versionMismatchErrorMessage': versionMismatchErrorMessage,
+    };
+  }
+
+  factory ServerInfoState.fromMap(Map<String, dynamic> map) {
+    return ServerInfoState(
+      mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
+      serverVersion: ServerVersion.fromMap(map['serverVersion']),
+      isVersionMismatch: map['isVersionMismatch'] ?? false,
+      versionMismatchErrorMessage: map['versionMismatchErrorMessage'] ?? '',
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'ServerInfoState(mapboxInfo: $mapboxInfo, serverVersion: $serverVersion, isVersionMismatch: $isVersionMismatch, versionMismatchErrorMessage: $versionMismatchErrorMessage)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is ServerInfoState &&
+        other.mapboxInfo == mapboxInfo &&
+        other.serverVersion == serverVersion &&
+        other.isVersionMismatch == isVersionMismatch &&
+        other.versionMismatchErrorMessage == versionMismatchErrorMessage;
+  }
+
+  @override
+  int get hashCode {
+    return mapboxInfo.hashCode ^
+        serverVersion.hashCode ^
+        isVersionMismatch.hashCode ^
+        versionMismatchErrorMessage.hashCode;
+  }
+}
diff --git a/mobile/lib/shared/models/server_version.model.dart b/mobile/lib/shared/models/server_version.model.dart
new file mode 100644
index 0000000000..176e939808
--- /dev/null
+++ b/mobile/lib/shared/models/server_version.model.dart
@@ -0,0 +1,72 @@
+import 'dart:convert';
+
+class ServerVersion {
+  final int major;
+  final int minor;
+  final int patch;
+  final int build;
+
+  ServerVersion({
+    required this.major,
+    required this.minor,
+    required this.patch,
+    required this.build,
+  });
+
+  ServerVersion copyWith({
+    int? major,
+    int? minor,
+    int? patch,
+    int? build,
+  }) {
+    return ServerVersion(
+      major: major ?? this.major,
+      minor: minor ?? this.minor,
+      patch: patch ?? this.patch,
+      build: build ?? this.build,
+    );
+  }
+
+  Map<String, dynamic> toMap() {
+    return {
+      'major': major,
+      'minor': minor,
+      'patch': patch,
+      'build': build,
+    };
+  }
+
+  factory ServerVersion.fromMap(Map<String, dynamic> map) {
+    return ServerVersion(
+      major: map['major']?.toInt() ?? 0,
+      minor: map['minor']?.toInt() ?? 0,
+      patch: map['patch']?.toInt() ?? 0,
+      build: map['build']?.toInt() ?? 0,
+    );
+  }
+
+  String toJson() => json.encode(toMap());
+
+  factory ServerVersion.fromJson(String source) => ServerVersion.fromMap(json.decode(source));
+
+  @override
+  String toString() {
+    return 'ServerVersion(major: $major, minor: $minor, patch: $patch, build: $build)';
+  }
+
+  @override
+  bool operator ==(Object other) {
+    if (identical(this, other)) return true;
+
+    return other is ServerVersion &&
+        other.major == major &&
+        other.minor == minor &&
+        other.patch == patch &&
+        other.build == build;
+  }
+
+  @override
+  int get hashCode {
+    return major.hashCode ^ minor.hashCode ^ patch.hashCode ^ build.hashCode;
+  }
+}
diff --git a/mobile/lib/shared/providers/server_info.provider.dart b/mobile/lib/shared/providers/server_info.provider.dart
index 06b11c2fa0..edba9cdc27 100644
--- a/mobile/lib/shared/providers/server_info.provider.dart
+++ b/mobile/lib/shared/providers/server_info.provider.dart
@@ -1,59 +1,19 @@
-import 'dart:convert';
-
 import 'package:hooks_riverpod/hooks_riverpod.dart';
 
 import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
+import 'package:immich_mobile/shared/models/server_info_state.model.dart';
+import 'package:immich_mobile/shared/models/server_version.model.dart';
 import 'package:immich_mobile/shared/services/server_info.service.dart';
-
-class ServerInfoState {
-  final MapboxInfo mapboxInfo;
-  ServerInfoState({
-    required this.mapboxInfo,
-  });
-
-  ServerInfoState copyWith({
-    MapboxInfo? mapboxInfo,
-  }) {
-    return ServerInfoState(
-      mapboxInfo: mapboxInfo ?? this.mapboxInfo,
-    );
-  }
-
-  Map<String, dynamic> toMap() {
-    return {
-      'mapboxInfo': mapboxInfo.toMap(),
-    };
-  }
-
-  factory ServerInfoState.fromMap(Map<String, dynamic> map) {
-    return ServerInfoState(
-      mapboxInfo: MapboxInfo.fromMap(map['mapboxInfo']),
-    );
-  }
-
-  String toJson() => json.encode(toMap());
-
-  factory ServerInfoState.fromJson(String source) => ServerInfoState.fromMap(json.decode(source));
-
-  @override
-  String toString() => 'ServerInfoState(mapboxInfo: $mapboxInfo)';
-
-  @override
-  bool operator ==(Object other) {
-    if (identical(this, other)) return true;
-
-    return other is ServerInfoState && other.mapboxInfo == mapboxInfo;
-  }
-
-  @override
-  int get hashCode => mapboxInfo.hashCode;
-}
+import 'package:package_info_plus/package_info_plus.dart';
 
 class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
   ServerInfoNotifier()
       : super(
           ServerInfoState(
             mapboxInfo: MapboxInfo(isEnable: false, mapboxSecret: ""),
+            serverVersion: ServerVersion(major: 0, patch: 0, minor: 0, build: 0),
+            isVersionMismatch: false,
+            versionMismatchErrorMessage: "",
           ),
         );
 
@@ -61,9 +21,63 @@ class ServerInfoNotifier extends StateNotifier<ServerInfoState> {
 
   getMapboxInfo() async {
     MapboxInfo mapboxInfoRes = await _serverInfoService.getMapboxInfo();
-    print(mapboxInfoRes);
     state = state.copyWith(mapboxInfo: mapboxInfoRes);
   }
+
+  getServerVersion() async {
+    ServerVersion? serverVersion = await _serverInfoService.getServerVersion();
+
+    if (serverVersion == null) {
+      state = state.copyWith(
+        isVersionMismatch: true,
+        versionMismatchErrorMessage:
+            "Server is out of date. Some functionalities might not working correctly. Download and rebuild server",
+      );
+      return;
+    }
+
+    state = state.copyWith(serverVersion: serverVersion);
+
+    PackageInfo packageInfo = await PackageInfo.fromPlatform();
+
+    Map<String, int> appVersion = _getDetailVersion(packageInfo.version);
+
+    if (appVersion["major"]! > serverVersion.major) {
+      state = state.copyWith(
+        isVersionMismatch: true,
+        versionMismatchErrorMessage:
+            "Server is out of date in major version. Some functionalities might not work correctly. Download and rebuild server",
+      );
+
+      return;
+    }
+
+    if (appVersion["minor"]! > serverVersion.minor) {
+      state = state.copyWith(
+        isVersionMismatch: true,
+        versionMismatchErrorMessage:
+            "Server is out of date in minor version. Some functionalities might not work correctly. Consider download and rebuild server",
+      );
+
+      return;
+    }
+
+    state = state.copyWith(isVersionMismatch: false, versionMismatchErrorMessage: "");
+  }
+
+  Map<String, int> _getDetailVersion(String version) {
+    List<String> detail = version.split(".");
+
+    var major = detail[0];
+    var minor = detail[1];
+    var patch = detail[2];
+
+    return {
+      "major": int.parse(major),
+      "minor": int.parse(minor),
+      "patch": int.parse(patch),
+    };
+  }
 }
 
 final serverInfoProvider = StateNotifierProvider<ServerInfoNotifier, ServerInfoState>((ref) {
diff --git a/mobile/lib/shared/services/backup.service.dart b/mobile/lib/shared/services/backup.service.dart
index 584eb2d1d4..c2e0a3552e 100644
--- a/mobile/lib/shared/services/backup.service.dart
+++ b/mobile/lib/shared/services/backup.service.dart
@@ -30,10 +30,14 @@ class BackupService {
       Function(int, int) uploadProgress) async {
     var dio = Dio();
     dio.interceptors.add(AuthenticatedRequestInterceptor());
+
     String deviceId = Hive.box(userInfoBox).get(deviceIdKey);
     String savedEndpoint = Hive.box(userInfoBox).get(serverEndpointKey);
     File? file;
 
+    MultipartFile assetRawUploadData;
+    MultipartFile thumbnailUploadData;
+
     for (var entity in assetList) {
       try {
         if (entity.type == AssetType.video) {
@@ -43,12 +47,20 @@ class BackupService {
         }
 
         if (file != null) {
+          FormData formData;
           String originalFileName = await entity.titleAsync;
           String fileNameWithoutPath = originalFileName.toString().split(".")[0];
           var fileExtension = p.extension(file.path);
           var mimeType = FileHelper.getMimeType(file.path);
-
-          var formData = FormData.fromMap({
+          assetRawUploadData = await MultipartFile.fromFile(
+            file.path,
+            filename: fileNameWithoutPath,
+            contentType: MediaType(
+              mimeType["type"],
+              mimeType["subType"],
+            ),
+          );
+          formData = FormData.fromMap({
             'deviceAssetId': entity.id,
             'deviceId': deviceId,
             'assetType': _getAssetType(entity.type),
@@ -57,18 +69,36 @@ class BackupService {
             'isFavorite': entity.isFavorite,
             'fileExtension': fileExtension,
             'duration': entity.videoDuration,
-            'files': [
-              await MultipartFile.fromFile(
-                file.path,
-                filename: fileNameWithoutPath,
-                contentType: MediaType(
-                  mimeType["type"],
-                  mimeType["subType"],
-                ),
-              ),
-            ]
+            'assetData': [assetRawUploadData]
           });
 
+          // Build thumbnail multipart data
+          var thumbnailData = await entity.thumbDataWithSize(1280, 720);
+          if (thumbnailData != null) {
+            thumbnailUploadData = MultipartFile.fromBytes(
+              List.from(thumbnailData),
+              filename: fileNameWithoutPath,
+              contentType: MediaType(
+                "image",
+                "jpeg",
+              ),
+            );
+
+            // Send thumbnail data if it is exist
+            formData = FormData.fromMap({
+              'deviceAssetId': entity.id,
+              'deviceId': deviceId,
+              'assetType': _getAssetType(entity.type),
+              'createdAt': entity.createDateTime.toIso8601String(),
+              'modifiedAt': entity.modifiedDateTime.toIso8601String(),
+              'isFavorite': entity.isFavorite,
+              'fileExtension': fileExtension,
+              'duration': entity.videoDuration,
+              'thumbnailData': [thumbnailUploadData],
+              'assetData': [assetRawUploadData]
+            });
+          }
+
           Response res = await dio.post(
             '$savedEndpoint/asset/upload',
             data: formData,
diff --git a/mobile/lib/shared/services/server_info.service.dart b/mobile/lib/shared/services/server_info.service.dart
index 4dd11ead33..31d127795b 100644
--- a/mobile/lib/shared/services/server_info.service.dart
+++ b/mobile/lib/shared/services/server_info.service.dart
@@ -1,5 +1,6 @@
 import 'package:dio/dio.dart';
 import 'package:immich_mobile/shared/models/mapbox_info.model.dart';
+import 'package:immich_mobile/shared/models/server_version.model.dart';
 import 'package:immich_mobile/shared/services/network.service.dart';
 import 'package:immich_mobile/shared/models/server_info.model.dart';
 
@@ -17,4 +18,10 @@ class ServerInfoService {
 
     return MapboxInfo.fromJson(response.toString());
   }
+
+  Future<ServerVersion?> getServerVersion() async {
+    Response response = await _networkService.getRequest(url: 'server-info/version');
+
+    return ServerVersion.fromJson(response.toString());
+  }
 }
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 173065add7..50c59aa756 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -2,7 +2,7 @@ name: immich_mobile
 description: A new Flutter project.
 
 publish_to: "none"
-version: 1.1.0+1
+version: 1.3.0+0
 
 environment:
   sdk: ">=2.15.1 <3.0.0"
diff --git a/server/Dockerfile b/server/Dockerfile
index 5e8914b815..9ba76d4f4c 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -9,7 +9,7 @@ WORKDIR /usr/src/app
 
 COPY package.json package-lock.json ./
 
-RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
+# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
 
 RUN npm install
 
@@ -30,7 +30,7 @@ WORKDIR /usr/src/app
 
 COPY package.json package-lock.json ./
 
-RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
+# RUN apk add --update-cache build-base python3 libheif vips-dev vips ffmpeg
 
 RUN npm install --only=production
 
diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts
index 444bac7622..2158ade31c 100644
--- a/server/src/api-v1/asset/asset.controller.ts
+++ b/server/src/api-v1/asset/asset.controller.ts
@@ -16,7 +16,7 @@ import {
 } from '@nestjs/common';
 import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
 import { AssetService } from './asset.service';
-import { FilesInterceptor } from '@nestjs/platform-express';
+import { FileFieldsInterceptor, FilesInterceptor } from '@nestjs/platform-express';
 import { multerOption } from '../../config/multer-option.config';
 import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
 import { CreateAssetDto } from './dto/create-asset.dto';
@@ -29,34 +29,43 @@ import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto';
 import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
 import { DeleteAssetDto } from './dto/delete-asset.dto';
 import { SearchAssetDto } from './dto/search-asset.dto';
+import { CommunicationGateway } from '../communication/communication.gateway';
 
 @UseGuards(JwtAuthGuard)
 @Controller('asset')
 export class AssetController {
   constructor(
+    private wsCommunicateionGateway: CommunicationGateway,
     private assetService: AssetService,
-    private assetOptimizeService: AssetOptimizeService,
     private backgroundTaskService: BackgroundTaskService,
   ) {}
 
   @Post('upload')
-  @UseInterceptors(FilesInterceptor('files', 30, multerOption))
+  @UseInterceptors(
+    FileFieldsInterceptor(
+      [
+        { name: 'assetData', maxCount: 1 },
+        { name: 'thumbnailData', maxCount: 1 },
+      ],
+      multerOption,
+    ),
+  )
   async uploadFile(
     @GetAuthUser() authUser,
-    @UploadedFiles() files: Express.Multer.File[],
+    @UploadedFiles() uploadFiles: { assetData: Express.Multer.File[]; thumbnailData?: Express.Multer.File[] },
     @Body(ValidationPipe) assetInfo: CreateAssetDto,
   ) {
-    files.forEach(async (file) => {
+    uploadFiles.assetData.forEach(async (file) => {
       const savedAsset = await this.assetService.createUserAsset(authUser, assetInfo, file.path, file.mimetype);
 
-      if (savedAsset && savedAsset.type == AssetType.IMAGE) {
-        await this.assetOptimizeService.resizeImage(savedAsset);
-        await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
+      if (uploadFiles.thumbnailData != null) {
+        await this.assetService.updateThumbnailInfo(savedAsset.id, uploadFiles.thumbnailData[0].path);
+        await this.backgroundTaskService.tagImage(uploadFiles.thumbnailData[0].path, savedAsset);
       }
 
-      if (savedAsset && savedAsset.type == AssetType.VIDEO) {
-        await this.assetOptimizeService.getVideoThumbnail(savedAsset, file.originalname);
-      }
+      await this.backgroundTaskService.extractExif(savedAsset, file.originalname, file.size);
+
+      this.wsCommunicateionGateway.server.to(savedAsset.userId).emit('on_upload_success', JSON.stringify(savedAsset));
     });
 
     return 'ok';
diff --git a/server/src/api-v1/asset/asset.module.ts b/server/src/api-v1/asset/asset.module.ts
index 0d4466b9a5..dda0958fb4 100644
--- a/server/src/api-v1/asset/asset.module.ts
+++ b/server/src/api-v1/asset/asset.module.ts
@@ -8,9 +8,12 @@ import { AssetOptimizeService } from '../../modules/image-optimize/image-optimiz
 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';
 
 @Module({
   imports: [
+    CommunicationModule,
+
     BullModule.registerQueue({
       name: 'optimize',
       defaultJobOptions: {
diff --git a/server/src/api-v1/asset/asset.service.ts b/server/src/api-v1/asset/asset.service.ts
index aff091012a..2bc6299fa6 100644
--- a/server/src/api-v1/asset/asset.service.ts
+++ b/server/src/api-v1/asset/asset.service.ts
@@ -24,6 +24,12 @@ export class AssetService {
     private assetRepository: Repository<AssetEntity>,
   ) {}
 
+  public async updateThumbnailInfo(assetId: string, path: string) {
+    return await this.assetRepository.update(assetId, {
+      resizePath: path,
+    });
+  }
+
   public async createUserAsset(authUser: AuthUserDto, assetInfo: CreateAssetDto, path: string, mimeType: string) {
     const asset = new AssetEntity();
     asset.deviceAssetId = assetInfo.deviceAssetId;
diff --git a/server/src/api-v1/server-info/server-info.controller.ts b/server/src/api-v1/server-info/server-info.controller.ts
index f49b63ae73..211d743763 100644
--- a/server/src/api-v1/server-info/server-info.controller.ts
+++ b/server/src/api-v1/server-info/server-info.controller.ts
@@ -5,6 +5,7 @@ import { JwtAuthGuard } from '../../modules/immich-jwt/guards/jwt-auth.guard';
 import { ServerInfoService } from './server-info.service';
 import mapboxGeocoding, { GeocodeService } from '@mapbox/mapbox-sdk/services/geocoding';
 import { MapiResponse } from '@mapbox/mapbox-sdk/lib/classes/mapi-response';
+import { serverVersion } from '../../constants/server_version.constant';
 
 @Controller('server-info')
 export class ServerInfoController {
@@ -30,4 +31,9 @@ export class ServerInfoController {
       mapboxSecret: this.configService.get('MAPBOX_KEY'),
     };
   }
+
+  @Get('/version')
+  async getServerVersion() {
+    return serverVersion;
+  }
 }
diff --git a/server/src/config/multer-option.config.ts b/server/src/config/multer-option.config.ts
index 791de71575..281c825ee2 100644
--- a/server/src/config/multer-option.config.ts
+++ b/server/src/config/multer-option.config.ts
@@ -23,17 +23,33 @@ export const multerOption: MulterOptions = {
     destination: (req: Request, file: Express.Multer.File, cb: any) => {
       const uploadPath = multerConfig.dest;
 
-      const userPath = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
+      if (file.fieldname == 'assetData') {
+        const originalUploadFolder = `${uploadPath}/${req.user['id']}/original/${req.body['deviceId']}`;
 
-      if (!existsSync(userPath)) {
-        mkdirSync(userPath, { recursive: true });
+        if (!existsSync(originalUploadFolder)) {
+          mkdirSync(originalUploadFolder, { recursive: true });
+        }
+
+        cb(null, originalUploadFolder);
+      } else if (file.fieldname == 'thumbnailData') {
+        const thumbnailUploadFolder = `${uploadPath}/${req.user['id']}/thumb/${req.body['deviceId']}`;
+
+        if (!existsSync(thumbnailUploadFolder)) {
+          mkdirSync(thumbnailUploadFolder, { recursive: true });
+        }
+
+        cb(null, thumbnailUploadFolder);
       }
-
-      cb(null, userPath);
     },
 
     filename: (req: Request, file: Express.Multer.File, cb: any) => {
-      cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
+      // console.log(req, file);
+
+      if (file.fieldname == 'assetData') {
+        cb(null, `${file.originalname.split('.')[0]}${req.body['fileExtension']}`);
+      } else if (file.fieldname == 'thumbnailData') {
+        cb(null, `${file.originalname.split('.')[0]}.jpeg`);
+      }
     },
   }),
 };
diff --git a/server/src/constants/server_version.constant.ts b/server/src/constants/server_version.constant.ts
new file mode 100644
index 0000000000..42364c6b39
--- /dev/null
+++ b/server/src/constants/server_version.constant.ts
@@ -0,0 +1,9 @@
+// major.minor.patch+build
+// check mobile/pubspec.yml for current release version
+
+export const serverVersion = {
+  major: 1,
+  minor: 3,
+  patch: 0,
+  build: 0,
+};
diff --git a/server/src/modules/background-task/background-task.processor.ts b/server/src/modules/background-task/background-task.processor.ts
index 9a006c320c..7946143a49 100644
--- a/server/src/modules/background-task/background-task.processor.ts
+++ b/server/src/modules/background-task/background-task.processor.ts
@@ -41,7 +41,6 @@ export class BackgroundTaskProcessor {
   async extractExif(job: Job) {
     const { savedAsset, fileName, fileSize }: { savedAsset: AssetEntity; fileName: string; fileSize: number } =
       job.data;
-
     const fileBuffer = await readFile(savedAsset.originalPath);
 
     const exifData = await exifr.parse(fileBuffer);
diff --git a/server/src/modules/image-optimize/image-optimize.processor.ts b/server/src/modules/image-optimize/image-optimize.processor.ts
index d646c7b5a2..f547985828 100644
--- a/server/src/modules/image-optimize/image-optimize.processor.ts
+++ b/server/src/modules/image-optimize/image-optimize.processor.ts
@@ -22,122 +22,4 @@ export class ImageOptimizeProcessor {
 
     private backgroundTaskService: BackgroundTaskService,
   ) {}
-
-  @Process('resize-image')
-  async resizeUploadedImage(job: Job) {
-    const { savedAsset }: { savedAsset: AssetEntity } = job.data;
-
-    const basePath = APP_UPLOAD_LOCATION;
-    const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
-
-    // Create folder for thumb image if not exist
-
-    const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
-
-    if (!existsSync(resizeDir)) {
-      mkdirSync(resizeDir, { recursive: true });
-    }
-
-    readFile(savedAsset.originalPath, async (err, data) => {
-      if (err) {
-        console.error('Error Reading File');
-      }
-
-      // Special Assets Type - ios
-      if (
-        savedAsset.mimeType == 'image/heic' ||
-        savedAsset.mimeType == 'image/heif' ||
-        savedAsset.mimeType == 'image/dng'
-      ) {
-        let desitnation = '';
-        if (savedAsset.mimeType == 'image/heic') {
-          desitnation = resizePath.replace('.HEIC', '.jpeg');
-        } else if (savedAsset.mimeType == 'image/heif') {
-          desitnation = resizePath.replace('.HEIF', '.jpeg');
-        } else if (savedAsset.mimeType == 'image/dng') {
-          desitnation = resizePath.replace('.DNG', '.jpeg');
-        }
-
-        sharp(data)
-          .toFormat('jpeg')
-          .resize(512, 512, { fit: 'outside' })
-          .toFile(desitnation, async (err, info) => {
-            if (err) {
-              console.error('Error resizing file ', err);
-              return;
-            }
-
-            const res = await this.assetRepository.update(savedAsset, { resizePath: desitnation });
-
-            if (res.affected) {
-              this.wsCommunicateionGateway.server
-                .to(savedAsset.userId)
-                .emit('on_upload_success', JSON.stringify(savedAsset));
-            }
-
-            // Tag Image
-            this.backgroundTaskService.tagImage(desitnation, savedAsset);
-          });
-      } else {
-        sharp(data)
-          .resize(512, 512, { fit: 'outside' })
-          .toFile(resizePath, async (err, info) => {
-            if (err) {
-              console.error('Error resizing file ', err);
-              return;
-            }
-
-            const res = await this.assetRepository.update(savedAsset, { resizePath: resizePath });
-            if (res.affected) {
-              this.wsCommunicateionGateway.server
-                .to(savedAsset.userId)
-                .emit('on_upload_success', JSON.stringify(savedAsset));
-            }
-
-            // Tag Image
-            this.backgroundTaskService.tagImage(resizePath, savedAsset);
-          });
-      }
-    });
-
-    return 'ok';
-  }
-
-  @Process('get-video-thumbnail')
-  async resizeUploadedVideo(job: Job) {
-    const { savedAsset, filename }: { savedAsset: AssetEntity; filename: String } = job.data;
-
-    const basePath = APP_UPLOAD_LOCATION;
-    // const resizePath = savedAsset.originalPath.replace('/original/', '/thumb/');
-    // Create folder for thumb image if not exist
-    const resizeDir = `${basePath}/${savedAsset.userId}/thumb/${savedAsset.deviceId}`;
-
-    if (!existsSync(resizeDir)) {
-      mkdirSync(resizeDir, { recursive: true });
-    }
-
-    ffmpeg(savedAsset.originalPath)
-      .thumbnail({
-        count: 1,
-        timestamps: [1],
-        folder: resizeDir,
-        filename: `${filename}.png`,
-      })
-      .on('end', async (a) => {
-        const thumbnailPath = `${resizeDir}/${filename}.png`;
-
-        const res = await this.assetRepository.update(savedAsset, { resizePath: `${resizeDir}/${filename}.png` });
-
-        if (res.affected) {
-          this.wsCommunicateionGateway.server
-            .to(savedAsset.userId)
-            .emit('on_upload_success', JSON.stringify(savedAsset));
-        }
-
-        // Tag Image
-        this.backgroundTaskService.tagImage(thumbnailPath, savedAsset);
-      });
-
-    return 'ok';
-  }
 }
diff --git a/server/src/modules/image-optimize/image-optimize.service.ts b/server/src/modules/image-optimize/image-optimize.service.ts
index 31c0a466c0..ae8b711e3d 100644
--- a/server/src/modules/image-optimize/image-optimize.service.ts
+++ b/server/src/modules/image-optimize/image-optimize.service.ts
@@ -7,33 +7,4 @@ import { AssetEntity } from '../../api-v1/asset/entities/asset.entity';
 @Injectable()
 export class AssetOptimizeService {
   constructor(@InjectQueue('optimize') private optimizeQueue: Queue) {}
-
-  public async resizeImage(savedAsset: AssetEntity) {
-    const job = await this.optimizeQueue.add(
-      'resize-image',
-      {
-        savedAsset,
-      },
-      { jobId: randomUUID() },
-    );
-
-    return {
-      jobId: job.id,
-    };
-  }
-
-  public async getVideoThumbnail(savedAsset: AssetEntity, filename: string) {
-    const job = await this.optimizeQueue.add(
-      'get-video-thumbnail',
-      {
-        savedAsset,
-        filename,
-      },
-      { jobId: randomUUID() },
-    );
-
-    return {
-      jobId: job.id,
-    };
-  }
 }