>((ref) async {
+ final user = ref.read(currentUserProvider);
+ if (user == null) return [];
+
return ref
.watch(dbProvider)
.assets
.where()
+ .ownerIdEqualToAnyChecksum(user.isarId)
.sortByFileCreatedAtDesc()
.findAll();
});
diff --git a/mobile/lib/modules/search/ui/curated_people_row.dart b/mobile/lib/modules/search/ui/curated_people_row.dart
index 8af610ad1..a712c6929 100644
--- a/mobile/lib/modules/search/ui/curated_people_row.dart
+++ b/mobile/lib/modules/search/ui/curated_people_row.dart
@@ -24,7 +24,7 @@ class CuratedPeopleRow extends StatelessWidget {
@override
Widget build(BuildContext context) {
- const imageSize = 70.0;
+ const imageSize = 60.0;
// Guard empty [content]
if (content.isEmpty) {
diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock
index 2f35cf591..bf01dfcc8 100644
--- a/mobile/pubspec.lock
+++ b/mobile/pubspec.lock
@@ -5,18 +5,18 @@ packages:
dependency: transitive
description:
name: _fe_analyzer_shared
- sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1"
+ sha256: "0b2f2bd91ba804e53a61d757b986f89f1f9eaed5b11e4b2f5a2468d86d6c9fc7"
url: "https://pub.dev"
source: hosted
- version: "66.0.0"
+ version: "67.0.0"
analyzer:
dependency: "direct overridden"
description:
name: analyzer
- sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737"
+ sha256: "37577842a27e4338429a1cbc32679d508836510b056f1eedf0c8d20e39c1383d"
url: "https://pub.dev"
source: hosted
- version: "6.4.0"
+ version: "6.4.1"
analyzer_plugin:
dependency: "direct overridden"
description:
@@ -101,18 +101,18 @@ packages:
dependency: transitive
description:
name: build_daemon
- sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65"
+ sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1"
url: "https://pub.dev"
source: hosted
- version: "4.0.0"
+ version: "4.0.1"
build_resolvers:
dependency: transitive
description:
name: build_resolvers
- sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20"
+ sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a"
url: "https://pub.dev"
source: hosted
- version: "2.2.1"
+ version: "2.4.2"
build_runner:
dependency: "direct dev"
description:
@@ -125,10 +125,10 @@ packages:
dependency: transitive
description:
name: build_runner_core
- sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41"
+ sha256: "4ae8ffe5ac758da294ecf1802f2aff01558d8b1b00616aa7538ea9a8a5d50799"
url: "https://pub.dev"
source: hosted
- version: "7.2.10"
+ version: "7.3.0"
built_collection:
dependency: transitive
description:
@@ -141,10 +141,10 @@ packages:
dependency: transitive
description:
name: built_value
- sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166"
+ sha256: a3ec2e0f967bc47f69f95009bb93db936288d61d5343b9436e378b28a2f830c6
url: "https://pub.dev"
source: hosted
- version: "8.6.1"
+ version: "8.9.0"
cached_network_image:
dependency: "direct main"
description:
@@ -181,10 +181,10 @@ packages:
dependency: "direct main"
description:
name: cancellation_token_http
- sha256: bb91655e2e47d6274b681261ee6a687b7aa9023f49cfc28f42d095b2f86febc3
+ sha256: "37ad2a20dba02aeb1f0a4d845e7a57eebacdb709e1186e0491e7cd81c559c4ff"
url: "https://pub.dev"
source: hosted
- version: "1.3.0"
+ version: "2.0.0"
characters:
dependency: transitive
description:
@@ -205,10 +205,10 @@ packages:
dependency: "direct main"
description:
name: chewie
- sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e"
+ sha256: "8bc4ac4cf3f316e50a25958c0f5eb9bb12cf7e8308bb1d74a43b230da2cfc144"
url: "https://pub.dev"
source: hosted
- version: "1.7.4"
+ version: "1.7.5"
ci:
dependency: transitive
description:
@@ -221,10 +221,10 @@ packages:
dependency: transitive
description:
name: cli_util
- sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7
+ sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev"
source: hosted
- version: "0.4.0"
+ version: "0.4.1"
clock:
dependency: transitive
description:
@@ -237,10 +237,10 @@ packages:
dependency: transitive
description:
name: code_builder
- sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189"
+ sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37
url: "https://pub.dev"
source: hosted
- version: "4.5.0"
+ version: "4.10.0"
collection:
dependency: "direct main"
description:
@@ -277,10 +277,10 @@ packages:
dependency: transitive
description:
name: cross_file
- sha256: "0b0036e8cccbfbe0555fd83c1d31a6f30b77a96b598b35a5d36dd41f718695e9"
+ sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e
url: "https://pub.dev"
source: hosted
- version: "0.3.3+4"
+ version: "0.3.3+8"
crypto:
dependency: transitive
description:
@@ -301,42 +301,42 @@ packages:
dependency: transitive
description:
name: cupertino_icons
- sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be
+ sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d
url: "https://pub.dev"
source: hosted
- version: "1.0.5"
+ version: "1.0.6"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
- sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d"
+ sha256: f89ff83efdba7c8996e86bb3bad0b759d58f9b19ae4d0e277a386ddd8b481217
url: "https://pub.dev"
source: hosted
- version: "0.5.11"
+ version: "0.6.0"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
- sha256: "7d0b094266b5c357769fffb920826b1a08373290f3c5c44b86253aae6873d5fc"
+ sha256: "3a14687fc71a5e2124a29722106f7b7e67dd5a6d58e33f2859650b46acff1d54"
url: "https://pub.dev"
source: hosted
- version: "0.5.11"
+ version: "0.6.1"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
- sha256: "77dd37e9afe5ed86fc75de22ed3dfae5afb75303111358766550af23e7de53cd"
+ sha256: "1e9128e095ad5e0973469bdaac1ead8bfc86c485954c23cf617299de5e6fa029"
url: "https://pub.dev"
source: hosted
- version: "0.5.11"
+ version: "0.6.1"
dart_style:
dependency: transitive
description:
name: dart_style
- sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55"
+ sha256: "40ae61a5d43feea6d24bd22c0537a6629db858963b99b4bc1c3db80676f32368"
url: "https://pub.dev"
source: hosted
- version: "2.3.2"
+ version: "2.3.4"
dartx:
dependency: transitive
description:
@@ -349,18 +349,18 @@ packages:
dependency: transitive
description:
name: dbus
- sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263"
+ sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac"
url: "https://pub.dev"
source: hosted
- version: "0.7.8"
+ version: "0.7.10"
device_info_plus:
dependency: "direct main"
description:
name: device_info_plus
- sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6"
+ sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110"
url: "https://pub.dev"
source: hosted
- version: "9.1.1"
+ version: "9.1.2"
device_info_plus_platform_interface:
dependency: transitive
description:
@@ -373,18 +373,18 @@ packages:
dependency: "direct main"
description:
name: easy_image_viewer
- sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022"
+ sha256: "750bb85e0a34504557d378a616110540caeec2324490fc040709589219e75834"
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.4.1"
easy_localization:
dependency: "direct main"
description:
name: easy_localization
- sha256: de63e3b422adfc97f256cbb3f8cf12739b6a4993d390f3cadb3f51837afaefe5
+ sha256: "9c86754b22aaa3e74e471635b25b33729f958dd6fb83df0ad6612948a7b231af"
url: "https://pub.dev"
source: hosted
- version: "3.0.3"
+ version: "3.0.4"
easy_logger:
dependency: transitive
description:
@@ -413,42 +413,42 @@ packages:
dependency: transitive
description:
name: file
- sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
+ sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev"
source: hosted
- version: "6.1.4"
+ version: "7.0.0"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
- sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046"
+ sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492"
url: "https://pub.dev"
source: hosted
- version: "0.9.2"
+ version: "0.9.2+1"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
- sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412"
+ sha256: b15c3da8bd4908b9918111fa486903f5808e388b8d1c559949f584725a6594d6
url: "https://pub.dev"
source: hosted
- version: "0.9.3+1"
+ version: "0.9.3+3"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
- sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c"
+ sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
- version: "2.6.0"
+ version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
- sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26"
+ sha256: d3547240c20cabf205c7c7f01a50ecdbc413755814d6677f3cb366f04abcead0
url: "https://pub.dev"
source: hosted
- version: "0.9.3"
+ version: "0.9.3+1"
fixnum:
dependency: transitive
description:
@@ -487,10 +487,10 @@ packages:
dependency: "direct main"
description:
name: flutter_hooks
- sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0"
+ sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70
url: "https://pub.dev"
source: hosted
- version: "0.20.4"
+ version: "0.20.5"
flutter_launcher_icons:
dependency: "direct dev"
description:
@@ -548,18 +548,18 @@ packages:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
- sha256: "950e77c2bbe1692bc0874fc7fb491b96a4dc340457f4ea1641443d0a6c1ea360"
+ sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da
url: "https://pub.dev"
source: hosted
- version: "2.0.15"
+ version: "2.0.17"
flutter_riverpod:
dependency: transitive
description:
name: flutter_riverpod
- sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5
+ sha256: "4bce556b7ecbfea26109638d5237684538d4abc509d253e6c5c4c5733b360098"
url: "https://pub.dev"
source: hosted
- version: "2.4.9"
+ version: "2.4.10"
flutter_svg:
dependency: "direct main"
description:
@@ -635,26 +635,26 @@ packages:
dependency: transitive
description:
name: geolocator_android
- sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349"
+ sha256: "136f1c97e1903366393bda514c5d9e98843418baea52899aa45edae9af8a5cd6"
url: "https://pub.dev"
source: hosted
- version: "4.3.1"
+ version: "4.5.2"
geolocator_apple:
dependency: transitive
description:
name: geolocator_apple
- sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf"
+ sha256: "2f2d4ee16c4df269e93c0e382be075cc01d5db6703c3196e4af20a634fe49ef4"
url: "https://pub.dev"
source: hosted
- version: "2.3.5"
+ version: "2.3.6"
geolocator_platform_interface:
dependency: transitive
description:
name: geolocator_platform_interface
- sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535
+ sha256: "009a21c4bc2761e58dccf07c24f219adaebe0ff707abdfd40b0a763d4003fab9"
url: "https://pub.dev"
source: hosted
- version: "4.1.0"
+ version: "4.2.2"
geolocator_web:
dependency: transitive
description:
@@ -691,18 +691,18 @@ packages:
dependency: "direct main"
description:
name: hooks_riverpod
- sha256: c12a456e03ef9be65b0be66963596650ad7a3220e96c7e7b0a048562ea32d6ae
+ sha256: "758b07eba336e3cbacbd81dba481f2228a14102083fdde07045e8514e8054c49"
url: "https://pub.dev"
source: hosted
- version: "2.4.9"
+ version: "2.4.10"
hotreloader:
dependency: transitive
description:
name: hotreloader
- sha256: "94ee21a60ea2836500799f3af035dc3212b1562027f1e0031c14e087f0231449"
+ sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
url: "https://pub.dev"
source: hosted
- version: "4.1.0"
+ version: "4.2.0"
html:
dependency: transitive
description:
@@ -715,10 +715,10 @@ packages:
dependency: "direct main"
description:
name: http
- sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482"
+ sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2"
url: "https://pub.dev"
source: hosted
- version: "0.13.5"
+ version: "0.13.6"
http_multi_server:
dependency: transitive
description:
@@ -739,10 +739,10 @@ packages:
dependency: transitive
description:
name: image
- sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d"
+ sha256: "4c68bfd5ae83e700b5204c1e74451e7bf3cf750e6843c6e158289cf56bda018e"
url: "https://pub.dev"
source: hosted
- version: "4.1.4"
+ version: "4.1.7"
image_picker:
dependency: "direct main"
description:
@@ -755,58 +755,58 @@ packages:
dependency: transitive
description:
name: image_picker_android
- sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34"
+ sha256: "39f2bfe497e495450c81abcd44b62f56c2a36a37a175da7d137b4454977b51b1"
url: "https://pub.dev"
source: hosted
- version: "0.8.7+4"
+ version: "0.8.9+3"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
- sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
+ sha256: e2423c53a68b579a7c37a1eda967b8ae536c3d98518e5db95ca1fe5719a730a3
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "3.0.2"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
- sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b
+ sha256: fadafce49e8569257a0cad56d24438a6fa1f0cbd7ee0af9b631f7492818a4ca3
url: "https://pub.dev"
source: hosted
- version: "0.8.8"
+ version: "0.8.9+1"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
- sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831"
+ sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
url: "https://pub.dev"
source: hosted
- version: "0.2.1"
+ version: "0.2.1+1"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
- sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4
+ sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev"
source: hosted
- version: "0.2.1"
+ version: "0.2.1+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
- sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32
+ sha256: fa4e815e6fcada50e35718727d83ba1c92f1edf95c0b4436554cec301b56233b
url: "https://pub.dev"
source: hosted
- version: "2.9.0"
+ version: "2.9.3"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
- sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952
+ sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
- version: "0.2.1"
+ version: "0.2.1+1"
integration_test:
dependency: "direct dev"
description: flutter
@@ -868,6 +868,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.8.1"
+ leak_tracker:
+ dependency: transitive
+ description:
+ name: leak_tracker
+ sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa"
+ url: "https://pub.dev"
+ source: hosted
+ version: "10.0.0"
+ leak_tracker_flutter_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_flutter_testing
+ sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
+ leak_tracker_testing:
+ dependency: transitive
+ description:
+ name: leak_tracker_testing
+ sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.0.1"
lints:
dependency: transitive
description:
@@ -898,7 +922,7 @@ packages:
description:
path: maplibre_gl_platform_interface
ref: main
- resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+ resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
source: git
version: "0.18.0"
@@ -907,7 +931,7 @@ packages:
description:
path: maplibre_gl_web
ref: main
- resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5
+ resolved-ref: "3cf0abb051849ca3f14e6aa19d2261ad18f22ce1"
url: "https://github.com/maplibre/flutter-maplibre-gl.git"
source: git
version: "0.18.0"
@@ -915,34 +939,34 @@ packages:
dependency: transitive
description:
name: matcher
- sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e"
+ sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
- version: "0.12.16"
+ version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
- sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41"
+ sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "0.8.0"
meta:
dependency: "direct overridden"
description:
name: meta
- sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04
+ sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136"
url: "https://pub.dev"
source: hosted
- version: "1.11.0"
+ version: "1.12.0"
mime:
dependency: transitive
description:
name: mime
- sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e
+ sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2"
url: "https://pub.dev"
source: hosted
- version: "1.0.4"
+ version: "1.0.5"
mocktail:
dependency: "direct dev"
description:
@@ -1010,10 +1034,10 @@ packages:
dependency: "direct main"
description:
name: path
- sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917"
+ sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
- version: "1.8.3"
+ version: "1.9.0"
path_parsing:
dependency: transitive
description:
@@ -1034,18 +1058,18 @@ packages:
dependency: transitive
description:
name: path_provider_android
- sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8"
+ sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668"
url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.2.2"
path_provider_foundation:
dependency: transitive
description:
name: path_provider_foundation
- sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5"
+ sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f"
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
path_provider_ios:
dependency: "direct main"
description:
@@ -1058,50 +1082,50 @@ packages:
dependency: transitive
description:
name: path_provider_linux
- sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3
+ sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
- sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84
+ sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
- version: "2.1.0"
+ version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
- sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da
+ sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.1"
permission_handler:
dependency: "direct main"
description:
name: permission_handler
- sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6"
+ sha256: "74e962b7fad7ff75959161bb2c0ad8fe7f2568ee82621c9c2660b751146bfe44"
url: "https://pub.dev"
source: hosted
- version: "11.2.0"
+ version: "11.3.0"
permission_handler_android:
dependency: transitive
description:
name: permission_handler_android
- sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb"
+ sha256: "1acac6bae58144b442f11e66621c062aead9c99841093c38f5bcdcc24c1c3474"
url: "https://pub.dev"
source: hosted
- version: "12.0.3"
+ version: "12.0.5"
permission_handler_apple:
dependency: transitive
description:
name: permission_handler_apple
- sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830
+ sha256: bdafc6db74253abb63907f4e357302e6bb786ab41465e8635f362ee71fd8707b
url: "https://pub.dev"
source: hosted
- version: "9.3.0"
+ version: "9.4.0"
permission_handler_html:
dependency: transitive
description:
@@ -1114,10 +1138,10 @@ packages:
dependency: transitive
description:
name: permission_handler_platform_interface
- sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c"
+ sha256: "23dfba8447c076ab5be3dee9ceb66aad345c4a648f0cac292c77b1eb0e800b78"
url: "https://pub.dev"
source: hosted
- version: "4.1.0"
+ version: "4.2.0"
permission_handler_windows:
dependency: transitive
description:
@@ -1154,26 +1178,26 @@ packages:
dependency: transitive
description:
name: platform
- sha256: ae68c7bfcd7383af3629daafb32fb4e8681c7154428da4febcff06200585f102
+ sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec"
url: "https://pub.dev"
source: hosted
- version: "3.1.2"
+ version: "3.1.4"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
- sha256: "43798d895c929056255600343db8f049921cbec94d31ec87f1dc5c16c01935dd"
+ sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
- version: "2.1.5"
+ version: "2.1.8"
pointycastle:
dependency: transitive
description:
name: pointycastle
- sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c"
+ sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29"
url: "https://pub.dev"
source: hosted
- version: "3.7.3"
+ version: "3.7.4"
pool:
dependency: transitive
description:
@@ -1186,18 +1210,18 @@ packages:
dependency: transitive
description:
name: process
- sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
+ sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32"
url: "https://pub.dev"
source: hosted
- version: "4.2.4"
+ version: "5.0.2"
provider:
dependency: transitive
description:
name: provider
- sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f
+ sha256: "9a96a0a19b594dbc5bf0f1f27d2bc67d5f95957359b461cd9feb44ed6ae75096"
url: "https://pub.dev"
source: hosted
- version: "6.0.5"
+ version: "6.1.1"
pub_semver:
dependency: transitive
description:
@@ -1218,42 +1242,42 @@ packages:
dependency: transitive
description:
name: riverpod
- sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11"
+ sha256: "548e2192eb7aeb826eb89387f814edb76594f3363e2c0bb99dd733d795ba3589"
url: "https://pub.dev"
source: hosted
- version: "2.4.9"
+ version: "2.5.0"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
- sha256: d4dabc35358413bf4611fcb6abb46308a67c4ef4cd5e69fd3367b11925c59f57
+ sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
url: "https://pub.dev"
source: hosted
- version: "0.5.0"
+ version: "0.5.1"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
- sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979
+ sha256: "77e5d51afa4fa3e67903fb8746f33d368728d7051a0b6c292bcee60aeba46d95"
url: "https://pub.dev"
source: hosted
- version: "2.3.3"
+ version: "2.3.4"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
- sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0
+ sha256: "359068f04879347ae4edbe66c81cc95f83fa1743806d1a0c86e55dd3c33ebb32"
url: "https://pub.dev"
source: hosted
- version: "2.3.9"
+ version: "2.3.11"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
- sha256: "944929ef82c9bfeaa455ccab97920abcf847a0ffed5c9f6babc520a95db25176"
+ sha256: e9bbd02e9e89e18eecb183bbca556d7b523a0669024da9b8167c08903f442937
url: "https://pub.dev"
source: hosted
- version: "2.3.7"
+ version: "2.3.9"
rxdart:
dependency: transitive
description:
@@ -1274,10 +1298,10 @@ packages:
dependency: "direct main"
description:
name: share_plus
- sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd
+ sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900"
url: "https://pub.dev"
source: hosted
- version: "7.2.1"
+ version: "7.2.2"
share_plus_platform_interface:
dependency: transitive
description:
@@ -1290,26 +1314,26 @@ packages:
dependency: transitive
description:
name: shared_preferences
- sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1"
+ sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.2"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
- sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076
+ sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.1"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
- sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef
+ sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c"
url: "https://pub.dev"
source: hosted
- version: "2.3.3"
+ version: "2.3.5"
shared_preferences_linux:
dependency: transitive
description:
@@ -1322,18 +1346,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_platform_interface
- sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1"
+ sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b"
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
- sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a"
+ sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21"
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.2.2"
shared_preferences_windows:
dependency: transitive
description:
@@ -1383,10 +1407,10 @@ packages:
dependency: transitive
description:
name: source_gen
- sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16
+ sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832"
url: "https://pub.dev"
source: hosted
- version: "1.4.0"
+ version: "1.5.0"
source_span:
dependency: transitive
description:
@@ -1395,22 +1419,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.10.0"
+ sprintf:
+ dependency: transitive
+ description:
+ name: sprintf
+ sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.0"
sqflite:
dependency: transitive
description:
name: sqflite
- sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a"
+ sha256: a9016f495c927cb90557c909ff26a6d92d9bd54fc42ba92e19d4e79d61e798c6
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
sqflite_common:
dependency: transitive
description:
name: sqflite_common
- sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a"
+ sha256: "28d8c66baee4968519fb8bd6cdbedad982d6e53359091f0b74544a9f32ec72d5"
url: "https://pub.dev"
source: hosted
- version: "2.5.0"
+ version: "2.5.3"
stack_trace:
dependency: transitive
description:
@@ -1463,10 +1495,10 @@ packages:
dependency: transitive
description:
name: synchronized
- sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60"
+ sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558"
url: "https://pub.dev"
source: hosted
- version: "3.1.0"
+ version: "3.1.0+1"
term_glyph:
dependency: transitive
description:
@@ -1495,10 +1527,10 @@ packages:
dependency: transitive
description:
name: time
- sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124"
+ sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221
url: "https://pub.dev"
source: hosted
- version: "2.1.3"
+ version: "2.1.4"
timezone:
dependency: "direct main"
description:
@@ -1543,10 +1575,10 @@ packages:
dependency: transitive
description:
name: url_launcher_android
- sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f"
+ sha256: d4ed0711849dd8e33eb2dd69c25db0d0d3fdc37e0a62e629fe32f57a22db2745
url: "https://pub.dev"
source: hosted
- version: "6.2.2"
+ version: "6.3.0"
url_launcher_ios:
dependency: transitive
description:
@@ -1575,10 +1607,10 @@ packages:
dependency: transitive
description:
name: url_launcher_platform_interface
- sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198"
+ sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029"
url: "https://pub.dev"
source: hosted
- version: "2.3.0"
+ version: "2.3.2"
url_launcher_web:
dependency: transitive
description:
@@ -1599,10 +1631,10 @@ packages:
dependency: transitive
description:
name: uuid
- sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313"
+ sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8
url: "https://pub.dev"
source: hosted
- version: "3.0.7"
+ version: "4.3.3"
vector_graphics:
dependency: transitive
description:
@@ -1647,10 +1679,10 @@ packages:
dependency: transitive
description:
name: video_player_android
- sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608
+ sha256: "4dd9b8b86d70d65eecf3dcabfcdfbb9c9115d244d022654aba49a00336d540c2"
url: "https://pub.dev"
source: hosted
- version: "2.4.9"
+ version: "2.4.12"
video_player_avfoundation:
dependency: transitive
description:
@@ -1663,26 +1695,26 @@ packages:
dependency: transitive
description:
name: video_player_platform_interface
- sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988"
+ sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6"
url: "https://pub.dev"
source: hosted
- version: "6.2.0"
+ version: "6.2.2"
video_player_web:
dependency: transitive
description:
name: video_player_web
- sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c"
+ sha256: "34beb3a07d4331a24f7e7b2f75b8e2b103289038e07e65529699a671b6a6e2cb"
url: "https://pub.dev"
source: hosted
- version: "2.0.16"
+ version: "2.1.3"
vm_service:
dependency: transitive
description:
name: vm_service
- sha256: c538be99af830f478718b51630ec1b6bee5e74e52c8a802d328d9e71d35d2583
+ sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957
url: "https://pub.dev"
source: hosted
- version: "11.10.0"
+ version: "13.0.0"
wakelock_plus:
dependency: "direct main"
description:
@@ -1727,34 +1759,34 @@ packages:
dependency: transitive
description:
name: webdriver
- sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49"
+ sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e"
url: "https://pub.dev"
source: hosted
- version: "3.0.2"
+ version: "3.0.3"
win32:
dependency: transitive
description:
name: win32
- sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c"
+ sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8"
url: "https://pub.dev"
source: hosted
- version: "4.1.4"
+ version: "5.2.0"
win32_registry:
dependency: transitive
description:
name: win32_registry
- sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae"
+ sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a"
url: "https://pub.dev"
source: hosted
- version: "1.1.0"
+ version: "1.1.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
- sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247
+ sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
url: "https://pub.dev"
source: hosted
- version: "1.0.2"
+ version: "1.0.4"
xml:
dependency: transitive
description:
diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml
index 785d338da..852a2bb08 100644
--- a/mobile/pubspec.yaml
+++ b/mobile/pubspec.yaml
@@ -3,7 +3,6 @@ description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.99.0+129
-isar_version: &isar_version 3.1.0+1
environment:
sdk: '>=3.0.0 <4.0.0'
@@ -37,10 +36,10 @@ dependencies:
flutter_svg: ^2.0.9
package_info_plus: ^5.0.1
url_launcher: ^6.2.4
- http: 0.13.5
- cancellation_token_http: ^1.1.0
+ http: ^0.13.6
+ cancellation_token_http: ^2.0.0
easy_localization: ^3.0.3
- share_plus: ^7.2.1
+ share_plus: ^7.2.2
flutter_displaymode: ^0.6.0
scrollable_positioned_list: ^0.3.8
path: ^1.8.3
@@ -49,8 +48,8 @@ dependencies:
http_parser: ^4.0.2
flutter_web_auth: ^0.5.0
easy_image_viewer: ^1.4.0
- isar: *isar_version
- isar_flutter_libs: *isar_version # contains Isar Core
+ isar: ^3.1.0+1
+ isar_flutter_libs: ^3.1.0+1
permission_handler: ^11.2.0
device_info_plus: ^9.1.1
connectivity_plus: ^5.0.2
@@ -91,10 +90,10 @@ dev_dependencies:
auto_route_generator: ^7.3.2
flutter_launcher_icons: ^0.13.1
flutter_native_splash: ^2.3.9
- isar_generator: *isar_version
+ isar_generator: ^3.1.0+1
integration_test:
sdk: flutter
- custom_lint: ^0.5.8
+ custom_lint: ^0.6.0
riverpod_lint: ^2.3.7
riverpod_generator: ^2.3.9
mocktail: ^1.0.3
diff --git a/README_ca_ES.md b/readme_i18n/README_ca_ES.md
similarity index 99%
rename from README_ca_ES.md
rename to readme_i18n/README_ca_ES.md
index cf7bcb4f5..d46ae5c26 100644
--- a/README_ca_ES.md
+++ b/readme_i18n/README_ca_ES.md
@@ -18,7 +18,7 @@
- English
+ English
Español
Français
Italiano
diff --git a/README_de_DE.md b/readme_i18n/README_de_DE.md
similarity index 99%
rename from README_de_DE.md
rename to readme_i18n/README_de_DE.md
index 322a58b96..76cdaf5a2 100644
--- a/README_de_DE.md
+++ b/readme_i18n/README_de_DE.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_es_ES.md b/readme_i18n/README_es_ES.md
similarity index 99%
rename from README_es_ES.md
rename to readme_i18n/README_es_ES.md
index 047c47260..1f0dd0666 100644
--- a/README_es_ES.md
+++ b/readme_i18n/README_es_ES.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Français
Italiano
diff --git a/README_fr_FR.md b/readme_i18n/README_fr_FR.md
similarity index 99%
rename from README_fr_FR.md
rename to readme_i18n/README_fr_FR.md
index 3b4c6d428..783189c69 100644
--- a/README_fr_FR.md
+++ b/readme_i18n/README_fr_FR.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Italiano
diff --git a/README_it_IT.md b/readme_i18n/README_it_IT.md
similarity index 99%
rename from README_it_IT.md
rename to readme_i18n/README_it_IT.md
index 7e8863a93..9dea33c99 100644
--- a/README_it_IT.md
+++ b/readme_i18n/README_it_IT.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_ja_JP.md b/readme_i18n/README_ja_JP.md
similarity index 99%
rename from README_ja_JP.md
rename to readme_i18n/README_ja_JP.md
index 90143025a..e002bfb21 100644
--- a/README_ja_JP.md
+++ b/readme_i18n/README_ja_JP.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_ko_KR.md b/readme_i18n/README_ko_KR.md
similarity index 99%
rename from README_ko_KR.md
rename to readme_i18n/README_ko_KR.md
index 3a1599597..05ffc96d9 100644
--- a/README_ko_KR.md
+++ b/readme_i18n/README_ko_KR.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_nl_NL.md b/readme_i18n/README_nl_NL.md
similarity index 99%
rename from README_nl_NL.md
rename to readme_i18n/README_nl_NL.md
index 1eeb41e5a..98de2581c 100644
--- a/README_nl_NL.md
+++ b/readme_i18n/README_nl_NL.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_ru_RU.md b/readme_i18n/README_ru_RU.md
similarity index 100%
rename from README_ru_RU.md
rename to readme_i18n/README_ru_RU.md
diff --git a/README_tr_TR.md b/readme_i18n/README_tr_TR.md
similarity index 99%
rename from README_tr_TR.md
rename to readme_i18n/README_tr_TR.md
index 6e5c43ee6..b07e023f4 100644
--- a/README_tr_TR.md
+++ b/readme_i18n/README_tr_TR.md
@@ -18,7 +18,7 @@
- English
+ English
Català
Español
Français
diff --git a/README_zh_CN.md b/readme_i18n/README_zh_CN.md
similarity index 99%
rename from README_zh_CN.md
rename to readme_i18n/README_zh_CN.md
index 874b18f09..dc573f735 100644
--- a/README_zh_CN.md
+++ b/readme_i18n/README_zh_CN.md
@@ -22,7 +22,7 @@
- English
+ English
Català
Español
Français
diff --git a/server/e2e/client/asset-api.ts b/server/e2e/client/asset-api.ts
index 8f30e1f4a..f32d58611 100644
--- a/server/e2e/client/asset-api.ts
+++ b/server/e2e/client/asset-api.ts
@@ -1,4 +1,4 @@
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import request from 'supertest';
export const assetApi = {
diff --git a/server/e2e/client/auth-api.ts b/server/e2e/client/auth-api.ts
index 46b21fb98..a8cfe4660 100644
--- a/server/e2e/client/auth-api.ts
+++ b/server/e2e/client/auth-api.ts
@@ -1,5 +1,5 @@
-import { LoginResponseDto } from 'src/domain/auth/auth.dto';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
+import { LoginResponseDto } from 'src/dtos/auth.dto';
+import { UserResponseDto } from 'src/dtos/user.dto';
import request from 'supertest';
import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub';
diff --git a/server/e2e/client/library-api.ts b/server/e2e/client/library-api.ts
index 90b1b7451..70c8c4c36 100644
--- a/server/e2e/client/library-api.ts
+++ b/server/e2e/client/library-api.ts
@@ -1,4 +1,4 @@
-import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/domain/library/library.dto';
+import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto';
import request from 'supertest';
export const libraryApi = {
diff --git a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
index 3d43227b7..20a9d3202 100644
--- a/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
+++ b/server/e2e/jobs/specs/library-watcher.e2e-spec.ts
@@ -1,12 +1,12 @@
import { api } from 'e2e/client';
import fs from 'node:fs/promises';
import path from 'node:path';
-import { LoginResponseDto } from 'src/domain/auth/auth.dto';
-import { LibraryResponseDto } from 'src/domain/library/library.dto';
-import { LibraryService } from 'src/domain/library/library.service';
+import { LoginResponseDto } from 'src/dtos/auth.dto';
+import { LibraryResponseDto } from 'src/dtos/library.dto';
import { AssetType } from 'src/entities/asset.entity';
import { LibraryType } from 'src/entities/library.entity';
-import { StorageEventType } from 'src/interfaces/storage.repository';
+import { StorageEventType } from 'src/interfaces/storage.interface';
+import { LibraryService } from 'src/services/library.service';
import {
IMMICH_TEST_ASSET_PATH,
IMMICH_TEST_ASSET_TEMP_PATH,
diff --git a/server/e2e/jobs/specs/library.e2e-spec.ts b/server/e2e/jobs/specs/library.e2e-spec.ts
index 0e2ce08f9..d6885cbf3 100644
--- a/server/e2e/jobs/specs/library.e2e-spec.ts
+++ b/server/e2e/jobs/specs/library.e2e-spec.ts
@@ -1,7 +1,7 @@
import { api } from 'e2e/client';
import fs from 'node:fs';
import { LibraryController } from 'src/controllers/library.controller';
-import { LoginResponseDto } from 'src/domain/auth/auth.dto';
+import { LoginResponseDto } from 'src/dtos/auth.dto';
import { LibraryType } from 'src/entities/library.entity';
import request from 'supertest';
import { errorStub } from 'test/fixtures/error.stub';
diff --git a/server/package-lock.json b/server/package-lock.json
index 079b8db53..5d8bfa17c 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -81,7 +81,6 @@
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.5.7",
- "@types/sharp": "^0.32.0",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -4840,16 +4839,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/sharp": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
- "integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
- "deprecated": "This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.",
- "dev": true,
- "dependencies": {
- "sharp": "*"
- }
- },
"node_modules/@types/shimmer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
@@ -17900,15 +17889,6 @@
"@types/node": "*"
}
},
- "@types/sharp": {
- "version": "0.32.0",
- "resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
- "integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
- "dev": true,
- "requires": {
- "sharp": "*"
- }
- },
"@types/shimmer": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.0.5.tgz",
diff --git a/server/package.json b/server/package.json
index d6f7036e3..3283e42ae 100644
--- a/server/package.json
+++ b/server/package.json
@@ -25,12 +25,12 @@
"e2e:jobs": "jest --config e2e/jobs/jest-e2e.json --runInBand",
"typeorm": "typeorm",
"typeorm:migrations:create": "typeorm migration:create",
- "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/infra/database.config.js",
- "typeorm:migrations:run": "typeorm migration:run -d ./dist/infra/database.config.js",
- "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/infra/database.config.js",
- "typeorm:schema:drop": "typeorm query -d ./dist/infra/database.config.js 'DROP schema public cascade; CREATE schema public;'",
+ "typeorm:migrations:generate": "typeorm migration:generate -d ./dist/database.config.js",
+ "typeorm:migrations:run": "typeorm migration:run -d ./dist/database.config.js",
+ "typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js",
+ "typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'",
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
- "sql:generate": "node ./dist/infra/sql-generator/"
+ "sql:generate": "node ./dist/utils/sql.js"
},
"dependencies": {
"@babel/runtime": "^7.22.11",
@@ -105,7 +105,6 @@
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^20.5.7",
- "@types/sharp": "^0.32.0",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
"@typescript-eslint/eslint-plugin": "^7.0.0",
@@ -145,19 +144,20 @@
"^.+\\.ts$": "ts-jest"
},
"collectCoverageFrom": [
- "/src/**/*.(t|j)s",
- "!/src/infra/**/*",
- "!/src/migrations/**/*",
- "!/src/subscribers/**/*",
- "!/src/immich/controllers/**/*"
+ "/src/cores/*.(t|j)s",
+ "/src/dtos/*.(t|j)s",
+ "/src/interfaces/*.(t|j)s",
+ "/src/services/*.(t|j)s",
+ "/src/utils/*.(t|j)s",
+ "/src/*.t|j)s"
],
"coverageDirectory": "./coverage",
"coverageThreshold": {
- "./src/domain/": {
- "branches": 75,
- "functions": 80,
- "lines": 85,
- "statements": 85
+ "./src/": {
+ "branches": 70,
+ "functions": 75,
+ "lines": 80,
+ "statements": 80
}
},
"testEnvironment": "node",
diff --git a/server/src/app.module.ts b/server/src/app.module.ts
new file mode 100644
index 000000000..c44b18727
--- /dev/null
+++ b/server/src/app.module.ts
@@ -0,0 +1,284 @@
+import { BullModule } from '@nestjs/bullmq';
+import { Module, OnModuleInit, Provider, ValidationPipe } from '@nestjs/common';
+import { ConfigModule } from '@nestjs/config';
+import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
+import { EventEmitterModule } from '@nestjs/event-emitter';
+import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
+import { TypeOrmModule } from '@nestjs/typeorm';
+import { OpenTelemetryModule } from 'nestjs-otel';
+import { ListUsersCommand } from 'src/commands/list-users.command';
+import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
+import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
+import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
+import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
+import { ActivityController } from 'src/controllers/activity.controller';
+import { AlbumController } from 'src/controllers/album.controller';
+import { APIKeyController } from 'src/controllers/api-key.controller';
+import { AppController } from 'src/controllers/app.controller';
+import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
+import { AssetController, AssetsController } from 'src/controllers/asset.controller';
+import { AuditController } from 'src/controllers/audit.controller';
+import { AuthController } from 'src/controllers/auth.controller';
+import { DownloadController } from 'src/controllers/download.controller';
+import { FaceController } from 'src/controllers/face.controller';
+import { JobController } from 'src/controllers/job.controller';
+import { LibraryController } from 'src/controllers/library.controller';
+import { OAuthController } from 'src/controllers/oauth.controller';
+import { PartnerController } from 'src/controllers/partner.controller';
+import { PersonController } from 'src/controllers/person.controller';
+import { SearchController } from 'src/controllers/search.controller';
+import { ServerInfoController } from 'src/controllers/server-info.controller';
+import { SharedLinkController } from 'src/controllers/shared-link.controller';
+import { SystemConfigController } from 'src/controllers/system-config.controller';
+import { TagController } from 'src/controllers/tag.controller';
+import { TrashController } from 'src/controllers/trash.controller';
+import { UserController } from 'src/controllers/user.controller';
+import { databaseConfig } from 'src/database.config';
+import { databaseEntities } from 'src/entities';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
+import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IJobRepository } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { ITagRepository } from 'src/interfaces/tag.interface';
+import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AuthGuard } from 'src/middleware/auth.guard';
+import { ErrorInterceptor } from 'src/middleware/error.interceptor';
+import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
+import { AccessRepository } from 'src/repositories/access.repository';
+import { ActivityRepository } from 'src/repositories/activity.repository';
+import { AlbumRepository } from 'src/repositories/album.repository';
+import { ApiKeyRepository } from 'src/repositories/api-key.repository';
+import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
+import { AssetRepositoryV1 } from 'src/repositories/asset-v1.repository';
+import { AssetRepository } from 'src/repositories/asset.repository';
+import { AuditRepository } from 'src/repositories/audit.repository';
+import { CommunicationRepository } from 'src/repositories/communication.repository';
+import { CryptoRepository } from 'src/repositories/crypto.repository';
+import { DatabaseRepository } from 'src/repositories/database.repository';
+import { FilesystemProvider } from 'src/repositories/filesystem.provider';
+import { JobRepository } from 'src/repositories/job.repository';
+import { LibraryRepository } from 'src/repositories/library.repository';
+import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
+import { MediaRepository } from 'src/repositories/media.repository';
+import { MetadataRepository } from 'src/repositories/metadata.repository';
+import { MoveRepository } from 'src/repositories/move.repository';
+import { PartnerRepository } from 'src/repositories/partner.repository';
+import { PersonRepository } from 'src/repositories/person.repository';
+import { SearchRepository } from 'src/repositories/search.repository';
+import { ServerInfoRepository } from 'src/repositories/server-info.repository';
+import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
+import { SystemConfigRepository } from 'src/repositories/system-config.repository';
+import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
+import { TagRepository } from 'src/repositories/tag.repository';
+import { UserTokenRepository } from 'src/repositories/user-token.repository';
+import { UserRepository } from 'src/repositories/user.repository';
+import { ActivityService } from 'src/services/activity.service';
+import { AlbumService } from 'src/services/album.service';
+import { APIKeyService } from 'src/services/api-key.service';
+import { ApiService } from 'src/services/api.service';
+import { AssetServiceV1 } from 'src/services/asset-v1.service';
+import { AssetService } from 'src/services/asset.service';
+import { AuditService } from 'src/services/audit.service';
+import { AuthService } from 'src/services/auth.service';
+import { DatabaseService } from 'src/services/database.service';
+import { DownloadService } from 'src/services/download.service';
+import { JobService } from 'src/services/job.service';
+import { LibraryService } from 'src/services/library.service';
+import { MediaService } from 'src/services/media.service';
+import { MetadataService } from 'src/services/metadata.service';
+import { MicroservicesService } from 'src/services/microservices.service';
+import { PartnerService } from 'src/services/partner.service';
+import { PersonService } from 'src/services/person.service';
+import { SearchService } from 'src/services/search.service';
+import { ServerInfoService } from 'src/services/server-info.service';
+import { SharedLinkService } from 'src/services/shared-link.service';
+import { SmartInfoService } from 'src/services/smart-info.service';
+import { StorageTemplateService } from 'src/services/storage-template.service';
+import { StorageService } from 'src/services/storage.service';
+import { SystemConfigService } from 'src/services/system-config.service';
+import { TagService } from 'src/services/tag.service';
+import { TrashService } from 'src/services/trash.service';
+import { UserService } from 'src/services/user.service';
+import { otelConfig } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+
+const commands = [
+ ResetAdminPasswordCommand,
+ PromptPasswordQuestions,
+ EnablePasswordLoginCommand,
+ DisablePasswordLoginCommand,
+ EnableOAuthLogin,
+ DisableOAuthLogin,
+ ListUsersCommand,
+];
+
+const controllers = [
+ ActivityController,
+ AssetsController,
+ AssetControllerV1,
+ AssetController,
+ AppController,
+ AlbumController,
+ APIKeyController,
+ AuditController,
+ AuthController,
+ DownloadController,
+ FaceController,
+ JobController,
+ LibraryController,
+ OAuthController,
+ PartnerController,
+ SearchController,
+ ServerInfoController,
+ SharedLinkController,
+ SystemConfigController,
+ TagController,
+ TrashController,
+ UserController,
+ PersonController,
+];
+
+const services: Provider[] = [
+ ApiService,
+ MicroservicesService,
+
+ APIKeyService,
+ ActivityService,
+ AlbumService,
+ AssetService,
+ AssetServiceV1,
+ AuditService,
+ AuthService,
+ DatabaseService,
+ DownloadService,
+ ImmichLogger,
+ JobService,
+ LibraryService,
+ MediaService,
+ MetadataService,
+ PartnerService,
+ PersonService,
+ SearchService,
+ ServerInfoService,
+ SharedLinkService,
+ SmartInfoService,
+ StorageService,
+ StorageTemplateService,
+ SystemConfigService,
+ TagService,
+ TrashService,
+ UserService,
+];
+
+const repositories: Provider[] = [
+ { provide: IActivityRepository, useClass: ActivityRepository },
+ { provide: IAccessRepository, useClass: AccessRepository },
+ { provide: IAlbumRepository, useClass: AlbumRepository },
+ { provide: IAssetRepository, useClass: AssetRepository },
+ { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
+ { provide: IAssetStackRepository, useClass: AssetStackRepository },
+ { provide: IAuditRepository, useClass: AuditRepository },
+ { provide: ICommunicationRepository, useClass: CommunicationRepository },
+ { provide: ICryptoRepository, useClass: CryptoRepository },
+ { provide: IDatabaseRepository, useClass: DatabaseRepository },
+ { provide: IJobRepository, useClass: JobRepository },
+ { provide: ILibraryRepository, useClass: LibraryRepository },
+ { provide: IKeyRepository, useClass: ApiKeyRepository },
+ { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
+ { provide: IMetadataRepository, useClass: MetadataRepository },
+ { provide: IMoveRepository, useClass: MoveRepository },
+ { provide: IPartnerRepository, useClass: PartnerRepository },
+ { provide: IPersonRepository, useClass: PersonRepository },
+ { provide: IServerInfoRepository, useClass: ServerInfoRepository },
+ { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
+ { provide: ISearchRepository, useClass: SearchRepository },
+ { provide: IStorageRepository, useClass: FilesystemProvider },
+ { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
+ { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
+ { provide: ITagRepository, useClass: TagRepository },
+ { provide: IMediaRepository, useClass: MediaRepository },
+ { provide: IUserRepository, useClass: UserRepository },
+ { provide: IUserTokenRepository, useClass: UserTokenRepository },
+];
+
+const middleware = [
+ FileUploadInterceptor,
+ { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
+ { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
+ { provide: APP_GUARD, useClass: AuthGuard },
+];
+
+const imports = [
+ BullModule.forRoot(bullConfig),
+ BullModule.registerQueue(...bullQueues),
+ ConfigModule.forRoot(immichAppConfig),
+ EventEmitterModule.forRoot(),
+ OpenTelemetryModule.forRoot(otelConfig),
+ TypeOrmModule.forRoot(databaseConfig),
+ TypeOrmModule.forFeature(databaseEntities),
+];
+
+@Module({
+ imports: [...imports, ScheduleModule.forRoot()],
+ controllers: [...controllers],
+ providers: [...services, ...repositories, ...middleware],
+})
+export class ApiModule implements OnModuleInit {
+ constructor(private service: ApiService) {}
+
+ async onModuleInit() {
+ await this.service.init();
+ }
+}
+
+@Module({
+ imports: [...imports],
+ providers: [...services, ...repositories, SchedulerRegistry],
+})
+export class MicroservicesModule implements OnModuleInit {
+ constructor(private service: MicroservicesService) {}
+
+ async onModuleInit() {
+ await this.service.init();
+ }
+}
+
+@Module({
+ imports: [...imports],
+ providers: [...services, ...repositories, ...commands, SchedulerRegistry],
+})
+export class ImmichAdminModule {}
+
+@Module({
+ imports: [
+ ConfigModule.forRoot(immichAppConfig),
+ EventEmitterModule.forRoot(),
+ TypeOrmModule.forRoot(databaseConfig),
+ TypeOrmModule.forFeature(databaseEntities),
+ ],
+ controllers: [...controllers],
+ providers: [...services, ...repositories, ...middleware, SchedulerRegistry],
+})
+export class AppTestModule {}
diff --git a/server/src/apps/api.main.ts b/server/src/apps/api.main.ts
deleted file mode 100644
index 9ffdd1d48..000000000
--- a/server/src/apps/api.main.ts
+++ /dev/null
@@ -1,57 +0,0 @@
-import { NestFactory } from '@nestjs/core';
-import { NestExpressApplication } from '@nestjs/platform-express';
-import { json } from 'body-parser';
-import cookieParser from 'cookie-parser';
-import { existsSync } from 'node:fs';
-import sirv from 'sirv';
-import { ApiModule } from 'src/apps/api.module';
-import { ApiService } from 'src/apps/api.service';
-import { excludePaths } from 'src/config';
-import { WEB_ROOT, envName, isDev, serverVersion } from 'src/domain/domain.constant';
-import { useSwagger } from 'src/immich/app.utils';
-import { otelSDK } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
-import { WebSocketAdapter } from 'src/infra/websocket.adapter';
-
-const logger = new ImmichLogger('ImmichServer');
-const port = Number(process.env.SERVER_PORT) || 3001;
-
-export async function bootstrapApi() {
- otelSDK.start();
- const app = await NestFactory.create(ApiModule, { bufferLogs: true });
-
- app.useLogger(app.get(ImmichLogger));
- app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
- app.set('etag', 'strong');
- app.use(cookieParser());
- app.use(json({ limit: '10mb' }));
- if (isDev) {
- app.enableCors();
- }
- app.useWebSocketAdapter(new WebSocketAdapter(app));
- useSwagger(app, isDev);
-
- app.setGlobalPrefix('api', { exclude: excludePaths });
- if (existsSync(WEB_ROOT)) {
- // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
- // provides serving of precompressed assets and caching of immutable assets
- app.use(
- sirv(WEB_ROOT, {
- etag: true,
- gzip: true,
- brotli: true,
- setHeaders: (res, pathname) => {
- if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) {
- res.setHeader('cache-control', 'public,max-age=31536000,immutable');
- }
- },
- }),
- );
- }
- app.use(app.get(ApiService).ssr(excludePaths));
-
- const server = await app.listen(port);
- server.requestTimeout = 30 * 60 * 1000;
-
- logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
-}
diff --git a/server/src/apps/api.module.ts b/server/src/apps/api.module.ts
deleted file mode 100644
index 04c95f199..000000000
--- a/server/src/apps/api.module.ts
+++ /dev/null
@@ -1,87 +0,0 @@
-import { Module, OnModuleInit, ValidationPipe } from '@nestjs/common';
-import { APP_GUARD, APP_INTERCEPTOR, APP_PIPE } from '@nestjs/core';
-import { ScheduleModule } from '@nestjs/schedule';
-import { TypeOrmModule } from '@nestjs/typeorm';
-import { ApiService } from 'src/apps/api.service';
-import { ActivityController } from 'src/controllers/activity.controller';
-import { AlbumController } from 'src/controllers/album.controller';
-import { APIKeyController } from 'src/controllers/api-key.controller';
-import { AppController } from 'src/controllers/app.controller';
-import { AssetController, AssetsController } from 'src/controllers/asset.controller';
-import { AuditController } from 'src/controllers/audit.controller';
-import { AuthController } from 'src/controllers/auth.controller';
-import { DownloadController } from 'src/controllers/download.controller';
-import { FaceController } from 'src/controllers/face.controller';
-import { JobController } from 'src/controllers/job.controller';
-import { LibraryController } from 'src/controllers/library.controller';
-import { OAuthController } from 'src/controllers/oauth.controller';
-import { PartnerController } from 'src/controllers/partner.controller';
-import { PersonController } from 'src/controllers/person.controller';
-import { SearchController } from 'src/controllers/search.controller';
-import { ServerInfoController } from 'src/controllers/server-info.controller';
-import { SharedLinkController } from 'src/controllers/shared-link.controller';
-import { SystemConfigController } from 'src/controllers/system-config.controller';
-import { TagController } from 'src/controllers/tag.controller';
-import { TrashController } from 'src/controllers/trash.controller';
-import { UserController } from 'src/controllers/user.controller';
-import { DomainModule } from 'src/domain/domain.module';
-import { AssetEntity } from 'src/entities/asset.entity';
-import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetRepositoryV1, IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
-import { AssetController as AssetControllerV1 } from 'src/immich/api-v1/asset/asset.controller';
-import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
-import { InfraModule } from 'src/infra/infra.module';
-import { AuthGuard } from 'src/middleware/auth.guard';
-import { ErrorInterceptor } from 'src/middleware/error.interceptor';
-import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
-
-@Module({
- imports: [
- //
- InfraModule,
- DomainModule,
- ScheduleModule.forRoot(),
- TypeOrmModule.forFeature([AssetEntity, ExifEntity]),
- ],
- controllers: [
- ActivityController,
- AssetsController,
- AssetControllerV1,
- AssetController,
- AppController,
- AlbumController,
- APIKeyController,
- AuditController,
- AuthController,
- DownloadController,
- FaceController,
- JobController,
- LibraryController,
- OAuthController,
- PartnerController,
- SearchController,
- ServerInfoController,
- SharedLinkController,
- SystemConfigController,
- TagController,
- TrashController,
- UserController,
- PersonController,
- ],
- providers: [
- { provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
- { provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
- { provide: APP_GUARD, useClass: AuthGuard },
- { provide: IAssetRepositoryV1, useClass: AssetRepositoryV1 },
- ApiService,
- AssetServiceV1,
- FileUploadInterceptor,
- ],
-})
-export class ApiModule implements OnModuleInit {
- constructor(private appService: ApiService) {}
-
- async onModuleInit() {
- await this.appService.init();
- }
-}
diff --git a/server/src/apps/immich-admin.main.ts b/server/src/apps/immich-admin.main.ts
deleted file mode 100755
index 5f528a21b..000000000
--- a/server/src/apps/immich-admin.main.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { CommandFactory } from 'nest-commander';
-import { ImmichAdminModule } from 'src/apps/immich-admin.module';
-import { LogLevel } from 'src/entities/system-config.entity';
-
-export async function bootstrapImmichAdmin() {
- process.env.LOG_LEVEL = LogLevel.WARN;
- await CommandFactory.run(ImmichAdminModule);
-}
diff --git a/server/src/apps/immich-admin.module.ts b/server/src/apps/immich-admin.module.ts
deleted file mode 100644
index d0e5cab4c..000000000
--- a/server/src/apps/immich-admin.module.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { Module } from '@nestjs/common';
-import { ListUsersCommand } from 'src/commands/list-users.command';
-import { DisableOAuthLogin, EnableOAuthLogin } from 'src/commands/oauth-login';
-import { DisablePasswordLoginCommand, EnablePasswordLoginCommand } from 'src/commands/password-login';
-import { PromptPasswordQuestions, ResetAdminPasswordCommand } from 'src/commands/reset-admin-password.command';
-import { DomainModule } from 'src/domain/domain.module';
-import { InfraModule } from 'src/infra/infra.module';
-
-@Module({
- imports: [InfraModule, DomainModule],
- providers: [
- ResetAdminPasswordCommand,
- PromptPasswordQuestions,
- EnablePasswordLoginCommand,
- DisablePasswordLoginCommand,
- EnableOAuthLogin,
- DisableOAuthLogin,
- ListUsersCommand,
- ],
-})
-export class ImmichAdminModule {}
diff --git a/server/src/apps/microservices.main.ts b/server/src/apps/microservices.main.ts
deleted file mode 100644
index 552fb714c..000000000
--- a/server/src/apps/microservices.main.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { NestFactory } from '@nestjs/core';
-import { MicroservicesModule } from 'src/apps/microservices.module';
-import { envName, serverVersion } from 'src/domain/domain.constant';
-import { otelSDK } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
-import { WebSocketAdapter } from 'src/infra/websocket.adapter';
-
-const logger = new ImmichLogger('ImmichMicroservice');
-const port = Number(process.env.MICROSERVICES_PORT) || 3002;
-
-export async function bootstrapMicroservices() {
- otelSDK.start();
- const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
- app.useLogger(app.get(ImmichLogger));
- app.useWebSocketAdapter(new WebSocketAdapter(app));
-
- await app.listen(port);
-
- logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
-}
diff --git a/server/src/apps/microservices.module.ts b/server/src/apps/microservices.module.ts
deleted file mode 100644
index 3ddfb28f3..000000000
--- a/server/src/apps/microservices.module.ts
+++ /dev/null
@@ -1,16 +0,0 @@
-import { Module, OnModuleInit } from '@nestjs/common';
-import { MicroservicesService } from 'src/apps/microservices.service';
-import { DomainModule } from 'src/domain/domain.module';
-import { InfraModule } from 'src/infra/infra.module';
-
-@Module({
- imports: [InfraModule, DomainModule],
- providers: [MicroservicesService],
-})
-export class MicroservicesModule implements OnModuleInit {
- constructor(private appService: MicroservicesService) {}
-
- async onModuleInit() {
- await this.appService.init();
- }
-}
diff --git a/server/src/commands/list-users.command.ts b/server/src/commands/list-users.command.ts
index 222833fad..32bcc35d9 100644
--- a/server/src/commands/list-users.command.ts
+++ b/server/src/commands/list-users.command.ts
@@ -1,6 +1,6 @@
import { Command, CommandRunner } from 'nest-commander';
-import { UserService } from 'src/domain/user/user.service';
import { UserEntity } from 'src/entities/user.entity';
+import { UserService } from 'src/services/user.service';
@Command({
name: 'list-users',
diff --git a/server/src/commands/oauth-login.ts b/server/src/commands/oauth-login.ts
index 12562fae1..c9bb4d5ef 100644
--- a/server/src/commands/oauth-login.ts
+++ b/server/src/commands/oauth-login.ts
@@ -1,5 +1,5 @@
import { Command, CommandRunner } from 'nest-commander';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
+import { SystemConfigService } from 'src/services/system-config.service';
@Command({
name: 'enable-oauth-login',
diff --git a/server/src/commands/password-login.ts b/server/src/commands/password-login.ts
index 953a1293a..3d992f858 100644
--- a/server/src/commands/password-login.ts
+++ b/server/src/commands/password-login.ts
@@ -1,5 +1,5 @@
import { Command, CommandRunner } from 'nest-commander';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
+import { SystemConfigService } from 'src/services/system-config.service';
@Command({
name: 'enable-password-login',
diff --git a/server/src/commands/reset-admin-password.command.ts b/server/src/commands/reset-admin-password.command.ts
index ce0a897a8..a186603a3 100644
--- a/server/src/commands/reset-admin-password.command.ts
+++ b/server/src/commands/reset-admin-password.command.ts
@@ -1,6 +1,6 @@
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
-import { UserService } from 'src/domain/user/user.service';
+import { UserResponseDto } from 'src/dtos/user.dto';
+import { UserService } from 'src/services/user.service';
@Command({
name: 'reset-admin-password',
diff --git a/server/src/config.ts b/server/src/config.ts
index b97f0bb7e..c7d2302c1 100644
--- a/server/src/config.ts
+++ b/server/src/config.ts
@@ -3,8 +3,8 @@ import { ConfigModuleOptions } from '@nestjs/config';
import { QueueOptions } from 'bullmq';
import { RedisOptions } from 'ioredis';
import Joi from 'joi';
-import { QueueName } from 'src/domain/job/job.constants';
import { LogLevel } from 'src/entities/system-config.entity';
+import { QueueName } from 'src/interfaces/job.interface';
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
is: Joi.exist(),
@@ -69,5 +69,3 @@ export const bullConfig: QueueOptions = {
};
export const bullQueues: RegisterQueueOptions[] = Object.values(QueueName).map((name) => ({ name }));
-
-export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
diff --git a/server/src/constants.ts b/server/src/constants.ts
new file mode 100644
index 000000000..f6cef9059
--- /dev/null
+++ b/server/src/constants.ts
@@ -0,0 +1,107 @@
+import { Duration } from 'luxon';
+import { readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import { Version } from 'src/utils/version';
+
+const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
+export const serverVersion = Version.fromString(version);
+
+export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
+export const ONE_HOUR = Duration.fromObject({ hours: 1 });
+
+export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
+export const isDev = process.env.NODE_ENV === 'development';
+export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
+export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
+
+const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
+
+export const citiesFile = 'cities500.txt';
+export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
+export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
+export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
+export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
+
+export const MOBILE_REDIRECT = 'app.immich:/';
+export const LOGIN_URL = '/auth/login?autoLaunch=0';
+export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
+export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated';
+export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
+export const IMMICH_API_KEY_NAME = 'api_key';
+export const IMMICH_API_KEY_HEADER = 'x-api-key';
+export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token';
+export enum AuthType {
+ PASSWORD = 'password',
+ OAUTH = 'oauth',
+}
+
+export const excludePaths = ['/.well-known/immich', '/custom.css', '/favicon.ico'];
+
+export const FACE_THUMBNAIL_SIZE = 250;
+
+export const supportedYearTokens = ['y', 'yy'];
+export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
+export const supportedWeekTokens = ['W', 'WW'];
+export const supportedDayTokens = ['d', 'dd'];
+export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
+export const supportedMinuteTokens = ['m', 'mm'];
+export const supportedSecondTokens = ['s', 'ss', 'SSS'];
+export const supportedPresetTokens = [
+ '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+ '{{y}}/{{MM}}-{{dd}}/{{filename}}',
+ '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
+ '{{y}}/{{MM}}/{{filename}}',
+ '{{y}}/{{MMM}}/{{filename}}',
+ '{{y}}/{{MMMM}}/{{filename}}',
+ '{{y}}/{{MM}}/{{dd}}/{{filename}}',
+ '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
+ '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
+ '{{y}}-{{MM}}-{{dd}}/{{filename}}',
+ '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
+ '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
+ '{{y}}/{{y}}-{{MM}}/{{filename}}',
+ '{{y}}/{{y}}-{{WW}}/{{filename}}',
+ '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
+ '{{y}}/{{y}}-{{MM}}/{{assetId}}',
+ '{{y}}/{{y}}-{{WW}}/{{assetId}}',
+ '{{album}}/{{filename}}',
+];
+
+type ModelInfo = { dimSize: number };
+export const CLIP_MODEL_INFO: Record = {
+ RN50__openai: { dimSize: 1024 },
+ RN50__yfcc15m: { dimSize: 1024 },
+ RN50__cc12m: { dimSize: 1024 },
+ RN101__openai: { dimSize: 512 },
+ RN101__yfcc15m: { dimSize: 512 },
+ RN50x4__openai: { dimSize: 640 },
+ RN50x16__openai: { dimSize: 768 },
+ RN50x64__openai: { dimSize: 1024 },
+ 'ViT-B-32__openai': { dimSize: 512 },
+ 'ViT-B-32__laion2b_e16': { dimSize: 512 },
+ 'ViT-B-32__laion400m_e31': { dimSize: 512 },
+ 'ViT-B-32__laion400m_e32': { dimSize: 512 },
+ 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 },
+ 'ViT-B-16__openai': { dimSize: 512 },
+ 'ViT-B-16__laion400m_e31': { dimSize: 512 },
+ 'ViT-B-16__laion400m_e32': { dimSize: 512 },
+ 'ViT-B-16-plus-240__laion400m_e31': { dimSize: 640 },
+ 'ViT-B-16-plus-240__laion400m_e32': { dimSize: 640 },
+ 'ViT-L-14__openai': { dimSize: 768 },
+ 'ViT-L-14__laion400m_e31': { dimSize: 768 },
+ 'ViT-L-14__laion400m_e32': { dimSize: 768 },
+ 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 },
+ 'ViT-L-14-336__openai': { dimSize: 768 },
+ 'ViT-L-14-quickgelu__dfn2b': { dimSize: 768 },
+ 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 },
+ 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 },
+ 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 },
+ 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 },
+ 'LABSE-Vit-L-14': { dimSize: 768 },
+ 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 },
+ 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 },
+ 'XLM-Roberta-Large-Vit-L-14': { dimSize: 768 },
+ 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 },
+ 'nllb-clip-base-siglip__v1': { dimSize: 768 },
+ 'nllb-clip-large-siglip__v1': { dimSize: 1152 },
+};
diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts
index c405b0c6f..a65b284ca 100644
--- a/server/src/controllers/activity.controller.ts
+++ b/server/src/controllers/activity.controller.ts
@@ -7,10 +7,10 @@ import {
ActivityResponseDto,
ActivitySearchDto,
ActivityStatisticsResponseDto,
-} from 'src/domain/activity/activity.dto';
-import { ActivityService } from 'src/domain/activity/activity.service';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+} from 'src/dtos/activity.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { ActivityService } from 'src/services/activity.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Activity')
diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts
index 18fd549b6..c4b11fbb4 100644
--- a/server/src/controllers/album.controller.ts
+++ b/server/src/controllers/album.controller.ts
@@ -1,15 +1,18 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { AlbumCountResponseDto, AlbumResponseDto } from 'src/domain/album/album-response.dto';
-import { AlbumService } from 'src/domain/album/album.service';
-import { AddUsersDto } from 'src/domain/album/dto/album-add-users.dto';
-import { CreateAlbumDto } from 'src/domain/album/dto/album-create.dto';
-import { UpdateAlbumDto } from 'src/domain/album/dto/album-update.dto';
-import { AlbumInfoDto } from 'src/domain/album/dto/album.dto';
-import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto';
-import { BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import {
+ AddUsersDto,
+ AlbumCountResponseDto,
+ AlbumInfoDto,
+ AlbumResponseDto,
+ CreateAlbumDto,
+ GetAlbumsDto,
+ UpdateAlbumDto,
+} from 'src/dtos/album.dto';
+import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
+import { AlbumService } from 'src/services/album.service';
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
@ApiTags('Album')
diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts
index 2d4f8db15..564b90387 100644
--- a/server/src/controllers/api-key.controller.ts
+++ b/server/src/controllers/api-key.controller.ts
@@ -1,14 +1,9 @@
import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import {
- APIKeyCreateDto,
- APIKeyCreateResponseDto,
- APIKeyResponseDto,
- APIKeyUpdateDto,
-} from 'src/domain/api-key/api-key.dto';
-import { APIKeyService } from 'src/domain/api-key/api-key.service';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { APIKeyService } from 'src/services/api-key.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('API Key')
diff --git a/server/src/controllers/app.controller.ts b/server/src/controllers/app.controller.ts
index d4c7ea5b4..472d0da3f 100644
--- a/server/src/controllers/app.controller.ts
+++ b/server/src/controllers/app.controller.ts
@@ -1,7 +1,7 @@
import { Controller, Get, Header } from '@nestjs/common';
import { ApiExcludeEndpoint } from '@nestjs/swagger';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
import { PublicRoute } from 'src/middleware/auth.guard';
+import { SystemConfigService } from 'src/services/system-config.service';
@Controller()
export class AppController {
diff --git a/server/src/immich/api-v1/asset/asset.controller.ts b/server/src/controllers/asset-v1.controller.ts
similarity index 63%
rename from server/src/immich/api-v1/asset/asset.controller.ts
rename to server/src/controllers/asset-v1.controller.ts
index d29f61fdd..2ba9aa7a0 100644
--- a/server/src/immich/api-v1/asset/asset.controller.ts
+++ b/server/src/controllers/asset-v1.controller.ts
@@ -15,23 +15,27 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { AssetService as AssetServiceV1 } from 'src/immich/api-v1/asset/asset.service';
-import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto';
-import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
-import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
-import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
-import { GetAssetThumbnailDto } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto';
-import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto';
-import { AssetBulkUploadCheckResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
-import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
-import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
-import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
-import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
-import { sendFile } from 'src/immich/app.utils';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import {
+ AssetBulkUploadCheckResponseDto,
+ AssetFileUploadResponseDto,
+ CheckExistingAssetsResponseDto,
+ CuratedLocationsResponseDto,
+ CuratedObjectsResponseDto,
+} from 'src/dtos/asset-v1-response.dto';
+import {
+ AssetBulkUploadCheckDto,
+ AssetSearchDto,
+ CheckExistingAssetsDto,
+ CreateAssetDto,
+ GetAssetThumbnailDto,
+ ServeFileDto,
+} from 'src/dtos/asset-v1.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
+import { AssetServiceV1 } from 'src/services/asset-v1.service';
+import { sendFile } from 'src/utils/file';
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
interface UploadFiles {
@@ -43,8 +47,8 @@ interface UploadFiles {
@ApiTags('Asset')
@Controller(Route.ASSET)
@Authenticated()
-export class AssetController {
- constructor(private serviceV1: AssetServiceV1) {}
+export class AssetControllerV1 {
+ constructor(private service: AssetServiceV1) {}
@SharedLinkRoute()
@Post('upload')
@@ -73,7 +77,7 @@ export class AssetController {
sidecarFile = mapToUploadFile(_sidecarFile);
}
- const responseDto = await this.serviceV1.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
+ const responseDto = await this.service.uploadFile(auth, dto, file, livePhotoFile, sidecarFile);
if (responseDto.duplicate) {
res.status(HttpStatus.OK);
}
@@ -91,7 +95,7 @@ export class AssetController {
@Param() { id }: UUIDParamDto,
@Query() dto: ServeFileDto,
) {
- await sendFile(res, next, () => this.serviceV1.serveFile(auth, id, dto));
+ await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
}
@SharedLinkRoute()
@@ -104,22 +108,22 @@ export class AssetController {
@Param() { id }: UUIDParamDto,
@Query() dto: GetAssetThumbnailDto,
) {
- await sendFile(res, next, () => this.serviceV1.serveThumbnail(auth, id, dto));
+ await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto));
}
@Get('/curated-objects')
getCuratedObjects(@Auth() auth: AuthDto): Promise {
- return this.serviceV1.getCuratedObject(auth);
+ return this.service.getCuratedObject(auth);
}
@Get('/curated-locations')
getCuratedLocations(@Auth() auth: AuthDto): Promise {
- return this.serviceV1.getCuratedLocation(auth);
+ return this.service.getCuratedLocation(auth);
}
@Get('/search-terms')
getAssetSearchTerms(@Auth() auth: AuthDto): Promise {
- return this.serviceV1.getAssetSearchTerm(auth);
+ return this.service.getAssetSearchTerm(auth);
}
/**
@@ -133,7 +137,7 @@ export class AssetController {
schema: { type: 'string' },
})
getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise {
- return this.serviceV1.getAllAssets(auth, dto);
+ return this.service.getAllAssets(auth, dto);
}
/**
@@ -145,7 +149,7 @@ export class AssetController {
@Auth() auth: AuthDto,
@Body() dto: CheckExistingAssetsDto,
): Promise {
- return this.serviceV1.checkExistingAssets(auth, dto);
+ return this.service.checkExistingAssets(auth, dto);
}
/**
@@ -157,6 +161,6 @@ export class AssetController {
@Auth() auth: AuthDto,
@Body() dto: AssetBulkUploadCheckDto,
): Promise {
- return this.serviceV1.bulkUploadCheck(auth, dto);
+ return this.service.bulkUploadCheck(auth, dto);
}
}
diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts
index a6db59815..37e169113 100644
--- a/server/src/controllers/asset.controller.ts
+++ b/server/src/controllers/asset.controller.ts
@@ -1,27 +1,24 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
-import { AssetService } from 'src/domain/asset/asset.service';
-import { AssetJobsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { UpdateStackParentDto } from 'src/domain/asset/dto/asset-stack.dto';
-import { AssetStatsDto, AssetStatsResponseDto } from 'src/domain/asset/dto/asset-statistics.dto';
+import { AssetResponseDto, MemoryLaneResponseDto } from 'src/dtos/asset-response.dto';
import {
AssetBulkDeleteDto,
AssetBulkUpdateDto,
+ AssetJobsDto,
+ AssetStatsDto,
+ AssetStatsResponseDto,
DeviceIdDto,
RandomAssetsDto,
UpdateAssetDto,
-} from 'src/domain/asset/dto/asset.dto';
-import { MapMarkerDto } from 'src/domain/asset/dto/map-marker.dto';
-import { MemoryLaneDto } from 'src/domain/asset/dto/memory-lane.dto';
-import { TimeBucketAssetDto, TimeBucketDto } from 'src/domain/asset/dto/time-bucket.dto';
-import { AssetResponseDto, MemoryLaneResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { MapMarkerResponseDto } from 'src/domain/asset/response-dto/map-marker-response.dto';
-import { TimeBucketResponseDto } from 'src/domain/asset/response-dto/time-bucket-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { MetadataSearchDto } from 'src/domain/search/dto/search.dto';
-import { SearchService } from 'src/domain/search/search.service';
+} from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto, MetadataSearchDto } from 'src/dtos/search.dto';
+import { UpdateStackParentDto } from 'src/dtos/stack.dto';
+import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
import { Route } from 'src/middleware/file-upload.interceptor';
+import { AssetService } from 'src/services/asset.service';
+import { SearchService } from 'src/services/search.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Asset')
diff --git a/server/src/controllers/audit.controller.ts b/server/src/controllers/audit.controller.ts
index bb92f990e..1487e78d4 100644
--- a/server/src/controllers/audit.controller.ts
+++ b/server/src/controllers/audit.controller.ts
@@ -7,10 +7,10 @@ import {
FileChecksumResponseDto,
FileReportDto,
FileReportFixDto,
-} from 'src/domain/audit/audit.dto';
-import { AuditService } from 'src/domain/audit/audit.service';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+} from 'src/dtos/audit.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { AdminRoute, Auth, Authenticated } from 'src/middleware/auth.guard';
+import { AuditService } from 'src/services/audit.service';
@ApiTags('Audit')
@Controller('audit')
diff --git a/server/src/controllers/auth.controller.ts b/server/src/controllers/auth.controller.ts
index 941002fe6..9b4e7a3bc 100644
--- a/server/src/controllers/auth.controller.ts
+++ b/server/src/controllers/auth.controller.ts
@@ -1,7 +1,7 @@
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
-import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/domain/auth/auth.constant';
+import { IMMICH_ACCESS_COOKIE, IMMICH_AUTH_TYPE_COOKIE, IMMICH_IS_AUTHENTICATED } from 'src/constants';
import {
AuthDeviceResponseDto,
AuthDto,
@@ -11,10 +11,10 @@ import {
LogoutResponseDto,
SignUpDto,
ValidateAccessTokenResponseDto,
-} from 'src/domain/auth/auth.dto';
-import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
-import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
+} from 'src/dtos/auth.dto';
+import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
+import { AuthService, LoginDetails } from 'src/services/auth.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Authentication')
diff --git a/server/src/controllers/download.controller.ts b/server/src/controllers/download.controller.ts
index 0fb3520cf..4e4bf09d1 100644
--- a/server/src/controllers/download.controller.ts
+++ b/server/src/controllers/download.controller.ts
@@ -1,12 +1,12 @@
import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto';
-import { DownloadService } from 'src/domain/download/download.service';
-import { asStreamableFile, sendFile } from 'src/immich/app.utils';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { Auth, Authenticated, FileResponse, SharedLinkRoute } from 'src/middleware/auth.guard';
+import { DownloadService } from 'src/services/download.service';
+import { asStreamableFile, sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Download')
diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts
index c7caba016..a3f33fb86 100644
--- a/server/src/controllers/face.controller.ts
+++ b/server/src/controllers/face.controller.ts
@@ -1,9 +1,9 @@
import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/domain/person/person.dto';
-import { PersonService } from 'src/domain/person/person.service';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { PersonService } from 'src/services/person.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Face')
diff --git a/server/src/controllers/job.controller.ts b/server/src/controllers/job.controller.ts
index f0e9ed2be..d6bd45b1e 100644
--- a/server/src/controllers/job.controller.ts
+++ b/server/src/controllers/job.controller.ts
@@ -1,8 +1,8 @@
import { Body, Controller, Get, Param, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/domain/job/job.dto';
-import { JobService } from 'src/domain/job/job.service';
+import { AllJobStatusResponseDto, JobCommandDto, JobIdParamDto, JobStatusDto } from 'src/dtos/job.dto';
import { Authenticated } from 'src/middleware/auth.guard';
+import { JobService } from 'src/services/job.service';
@ApiTags('Job')
@Controller('jobs')
diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts
index 677773f2d..70d357187 100644
--- a/server/src/controllers/library.controller.ts
+++ b/server/src/controllers/library.controller.ts
@@ -9,9 +9,9 @@ import {
UpdateLibraryDto,
ValidateLibraryDto,
ValidateLibraryResponseDto,
-} from 'src/domain/library/library.dto';
-import { LibraryService } from 'src/domain/library/library.service';
+} from 'src/dtos/library.dto';
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
+import { LibraryService } from 'src/services/library.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Library')
diff --git a/server/src/controllers/oauth.controller.ts b/server/src/controllers/oauth.controller.ts
index 48b3eafd5..debbd4e67 100644
--- a/server/src/controllers/oauth.controller.ts
+++ b/server/src/controllers/oauth.controller.ts
@@ -7,10 +7,10 @@ import {
OAuthAuthorizeResponseDto,
OAuthCallbackDto,
OAuthConfigDto,
-} from 'src/domain/auth/auth.dto';
-import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
+} from 'src/dtos/auth.dto';
+import { UserResponseDto } from 'src/dtos/user.dto';
import { Auth, Authenticated, GetLoginDetails, PublicRoute } from 'src/middleware/auth.guard';
+import { AuthService, LoginDetails } from 'src/services/auth.service';
@ApiTags('OAuth')
@Controller('oauth')
diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts
index 8ca55e73c..f654a7263 100644
--- a/server/src/controllers/partner.controller.ts
+++ b/server/src/controllers/partner.controller.ts
@@ -1,10 +1,10 @@
import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/common';
import { ApiQuery, ApiTags } from '@nestjs/swagger';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner.dto';
-import { PartnerService } from 'src/domain/partner/partner.service';
-import { PartnerDirection } from 'src/interfaces/partner.repository';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
+import { PartnerDirection } from 'src/interfaces/partner.interface';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { PartnerService } from 'src/services/partner.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Partner')
diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts
index bc955fd2c..c9128a1f7 100644
--- a/server/src/controllers/person.controller.ts
+++ b/server/src/controllers/person.controller.ts
@@ -1,9 +1,9 @@
import { Body, Controller, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
-import { BulkIdResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import {
AssetFaceUpdateDto,
MergePersonDto,
@@ -14,10 +14,10 @@ import {
PersonSearchDto,
PersonStatisticsResponseDto,
PersonUpdateDto,
-} from 'src/domain/person/person.dto';
-import { PersonService } from 'src/domain/person/person.service';
-import { sendFile } from 'src/immich/app.utils';
+} from 'src/dtos/person.dto';
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
+import { PersonService } from 'src/services/person.service';
+import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Person')
diff --git a/server/src/controllers/search.controller.ts b/server/src/controllers/search.controller.ts
index 76fce1609..eaf45be29 100644
--- a/server/src/controllers/search.controller.ts
+++ b/server/src/controllers/search.controller.ts
@@ -1,21 +1,21 @@
import { Body, Controller, Get, HttpCode, HttpStatus, Post, Query } from '@nestjs/common';
import { ApiOperation, ApiTags } from '@nestjs/swagger';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { PersonResponseDto } from 'src/domain/person/person.dto';
-import { SearchSuggestionRequestDto } from 'src/domain/search/dto/search-suggestion.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
PlacesResponseDto,
SearchDto,
+ SearchExploreResponseDto,
SearchPeopleDto,
SearchPlacesDto,
+ SearchResponseDto,
+ SearchSuggestionRequestDto,
SmartSearchDto,
-} from 'src/domain/search/dto/search.dto';
-import { SearchExploreResponseDto } from 'src/domain/search/response-dto/search-explore.response.dto';
-import { SearchResponseDto } from 'src/domain/search/response-dto/search-response.dto';
-import { SearchService } from 'src/domain/search/search.service';
+} from 'src/dtos/search.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { SearchService } from 'src/services/search.service';
@ApiTags('Search')
@Controller('search')
diff --git a/server/src/controllers/server-info.controller.ts b/server/src/controllers/server-info.controller.ts
index aae617493..e32b0d191 100644
--- a/server/src/controllers/server-info.controller.ts
+++ b/server/src/controllers/server-info.controller.ts
@@ -9,9 +9,9 @@ import {
ServerStatsResponseDto,
ServerThemeDto,
ServerVersionResponseDto,
-} from 'src/domain/server-info/server-info.dto';
-import { ServerInfoService } from 'src/domain/server-info/server-info.service';
+} from 'src/dtos/server-info.dto';
import { AdminRoute, Authenticated, PublicRoute } from 'src/middleware/auth.guard';
+import { ServerInfoService } from 'src/services/server-info.service';
@ApiTags('Server Info')
@Controller('server-info')
diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts
index 1e03a5c42..990f4e322 100644
--- a/server/src/controllers/shared-link.controller.ts
+++ b/server/src/controllers/shared-link.controller.ts
@@ -1,14 +1,18 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query, Req, Res } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
import { Request, Response } from 'express';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/domain/auth/auth.constant';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-response.dto';
-import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto';
-import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
+import { IMMICH_SHARED_LINK_ACCESS_COOKIE } from 'src/constants';
+import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import {
+ SharedLinkCreateDto,
+ SharedLinkEditDto,
+ SharedLinkPasswordDto,
+ SharedLinkResponseDto,
+} from 'src/dtos/shared-link.dto';
import { Auth, Authenticated, SharedLinkRoute } from 'src/middleware/auth.guard';
+import { SharedLinkService } from 'src/services/shared-link.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Shared Link')
diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts
index d10bccee0..1fb9dfbea 100644
--- a/server/src/controllers/system-config.controller.ts
+++ b/server/src/controllers/system-config.controller.ts
@@ -1,10 +1,8 @@
import { Body, Controller, Get, Put, Query } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { SystemConfigDto } from 'src/domain/system-config/dto/system-config.dto';
-import { SystemConfigTemplateStorageOptionDto } from 'src/domain/system-config/response-dto/system-config-template-storage-option.dto';
-import { MapThemeDto } from 'src/domain/system-config/system-config-map-theme.dto';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
+import { MapThemeDto, SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
import { AdminRoute, Authenticated } from 'src/middleware/auth.guard';
+import { SystemConfigService } from 'src/services/system-config.service';
@ApiTags('System Config')
@Controller('system-config')
diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts
index e914e577e..1caed8d52 100644
--- a/server/src/controllers/tag.controller.ts
+++ b/server/src/controllers/tag.controller.ts
@@ -1,13 +1,12 @@
import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { TagResponseDto } from 'src/domain/tag/tag-response.dto';
-import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto';
-import { TagService } from 'src/domain/tag/tag.service';
+import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { TagService } from 'src/services/tag.service';
import { UUIDParamDto } from 'src/validation';
@ApiTags('Tag')
diff --git a/server/src/controllers/trash.controller.ts b/server/src/controllers/trash.controller.ts
index 2c0b0c946..25df3543c 100644
--- a/server/src/controllers/trash.controller.ts
+++ b/server/src/controllers/trash.controller.ts
@@ -1,9 +1,9 @@
import { Body, Controller, HttpCode, HttpStatus, Post } from '@nestjs/common';
import { ApiTags } from '@nestjs/swagger';
-import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { TrashService } from 'src/domain/trash/trash.service';
+import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { Auth, Authenticated } from 'src/middleware/auth.guard';
+import { TrashService } from 'src/services/trash.service';
@ApiTags('Trash')
@Controller('trash')
diff --git a/server/src/controllers/user.controller.ts b/server/src/controllers/user.controller.ts
index e573584ad..c108e8852 100644
--- a/server/src/controllers/user.controller.ts
+++ b/server/src/controllers/user.controller.ts
@@ -16,17 +16,13 @@ import {
} from '@nestjs/common';
import { ApiBody, ApiConsumes, ApiTags } from '@nestjs/swagger';
import { NextFunction, Response } from 'express';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { CreateProfileImageDto } from 'src/domain/user/dto/create-profile-image.dto';
-import { CreateUserDto } from 'src/domain/user/dto/create-user.dto';
-import { DeleteUserDto } from 'src/domain/user/dto/delete-user.dto';
-import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
-import { CreateProfileImageResponseDto } from 'src/domain/user/response-dto/create-profile-image-response.dto';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
-import { UserService } from 'src/domain/user/user.service';
-import { sendFile } from 'src/immich/app.utils';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
+import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
import { AdminRoute, Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
+import { UserService } from 'src/services/user.service';
+import { sendFile } from 'src/utils/file';
import { UUIDParamDto } from 'src/validation';
@ApiTags('User')
diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts
index f9e22bb6b..f836e9762 100644
--- a/server/src/cores/access.core.ts
+++ b/server/src/cores/access.core.ts
@@ -1,8 +1,8 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { setDifference, setIsEqual, setUnion } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { setDifference, setIsEqual, setUnion } from 'src/utils/set';
export enum Permission {
ACTIVITY_CREATE = 'activity.create',
diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts
index 8bf65b248..b9dad8642 100644
--- a/server/src/cores/storage.core.ts
+++ b/server/src/cores/storage.core.ts
@@ -1,16 +1,16 @@
import { dirname, join, resolve } from 'node:path';
+import { APP_MEDIA_LOCATION } from 'src/constants';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { APP_MEDIA_LOCATION } from 'src/domain/domain.constant';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
export enum StorageFolder {
ENCODED_VIDEO = 'encoded-video',
diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts
index 295a0e5ed..01bfacc9b 100644
--- a/server/src/cores/system-config.core.ts
+++ b/server/src/cores/system-config.core.ts
@@ -5,8 +5,7 @@ import { validate } from 'class-validator';
import { load as loadYaml } from 'js-yaml';
import * as _ from 'lodash';
import { Subject } from 'rxjs';
-import { QueueName } from 'src/domain/job/job.constants';
-import { SystemConfigDto } from 'src/domain/system-config/dto/system-config.dto';
+import { SystemConfigDto } from 'src/dtos/system-config.dto';
import {
AudioCodec,
CQMode,
@@ -21,8 +20,9 @@ import {
TranscodePolicy,
VideoCodec,
} from 'src/entities/system-config.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { QueueName } from 'src/interfaces/job.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise;
diff --git a/server/src/cores/user.core.ts b/server/src/cores/user.core.ts
index d91f2bae5..4d7da25de 100644
--- a/server/src/cores/user.core.ts
+++ b/server/src/cores/user.core.ts
@@ -1,11 +1,11 @@
import { BadRequestException, ForbiddenException } from '@nestjs/common';
import sanitize from 'sanitize-filename';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
+import { UserResponseDto } from 'src/dtos/user.dto';
import { LibraryType } from 'src/entities/library.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
const SALT_ROUNDS = 10;
diff --git a/server/src/infra/database.config.ts b/server/src/database.config.ts
similarity index 80%
rename from server/src/infra/database.config.ts
rename to server/src/database.config.ts
index 572288c59..867b7f4cb 100644
--- a/server/src/infra/database.config.ts
+++ b/server/src/database.config.ts
@@ -1,4 +1,4 @@
-import { DatabaseExtension } from 'src/interfaces/database.repository';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
import { DataSource } from 'typeorm';
import { PostgresConnectionOptions } from 'typeorm/driver/postgres/PostgresConnectionOptions.js';
@@ -16,9 +16,9 @@ const urlOrParts = url
/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/
export const databaseConfig: PostgresConnectionOptions = {
type: 'postgres',
- entities: [__dirname + '/../entities/*.entity.{js,ts}'],
- migrations: [__dirname + '/../migrations/*.{js,ts}'],
- subscribers: [__dirname + '/../subscribers/*.{js,ts}'],
+ entities: [__dirname + '/entities/*.entity.{js,ts}'],
+ migrations: [__dirname + '/migrations/*.{js,ts}'],
+ subscribers: [__dirname + '/subscribers/*.{js,ts}'],
migrationsRun: false,
synchronize: false,
connectTimeoutMS: 10_000, // 10 seconds
diff --git a/server/src/decorators.ts b/server/src/decorators.ts
index 06dc0bfdc..b913fe00f 100644
--- a/server/src/decorators.ts
+++ b/server/src/decorators.ts
@@ -1,6 +1,8 @@
import { SetMetadata } from '@nestjs/common';
+import { OnEvent, OnEventType } from '@nestjs/event-emitter';
+import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces';
import _ from 'lodash';
-import { setUnion } from 'src/utils';
+import { setUnion } from 'src/utils/set';
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
@@ -122,3 +124,6 @@ export interface GenerateSqlQueries {
/** Decorator to enable versioning/tracking of generated Sql */
export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GENERATE_SQL_KEY, options);
+
+export const OnEventInternal = (event: OnEventType, options?: OnEventOptions) =>
+ OnEvent(event, { suppressErrors: false, ...options });
diff --git a/server/src/domain/album/dto/album-add-users.dto.ts b/server/src/domain/album/dto/album-add-users.dto.ts
deleted file mode 100644
index 1a6be4823..000000000
--- a/server/src/domain/album/dto/album-add-users.dto.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { ArrayNotEmpty } from 'class-validator';
-import { ValidateUUID } from 'src/validation';
-
-export class AddUsersDto {
- @ValidateUUID({ each: true })
- @ArrayNotEmpty()
- sharedUserIds!: string[];
-}
diff --git a/server/src/domain/album/dto/album-create.dto.ts b/server/src/domain/album/dto/album-create.dto.ts
deleted file mode 100644
index 1b4a75332..000000000
--- a/server/src/domain/album/dto/album-create.dto.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsString } from 'class-validator';
-import { Optional, ValidateUUID } from 'src/validation';
-
-export class CreateAlbumDto {
- @IsString()
- @ApiProperty()
- albumName!: string;
-
- @IsString()
- @Optional()
- description?: string;
-
- @ValidateUUID({ optional: true, each: true })
- sharedWithUserIds?: string[];
-
- @ValidateUUID({ optional: true, each: true })
- assetIds?: string[];
-}
diff --git a/server/src/domain/album/dto/album-update.dto.ts b/server/src/domain/album/dto/album-update.dto.ts
deleted file mode 100644
index cf2d5f8c2..000000000
--- a/server/src/domain/album/dto/album-update.dto.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum, IsString } from 'class-validator';
-import { AssetOrder } from 'src/entities/album.entity';
-import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
-
-export class UpdateAlbumDto {
- @Optional()
- @IsString()
- albumName?: string;
-
- @Optional()
- @IsString()
- description?: string;
-
- @ValidateUUID({ optional: true })
- albumThumbnailAssetId?: string;
-
- @ValidateBoolean({ optional: true })
- isActivityEnabled?: boolean;
-
- @IsEnum(AssetOrder)
- @Optional()
- @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
- order?: AssetOrder;
-}
diff --git a/server/src/domain/album/dto/album.dto.ts b/server/src/domain/album/dto/album.dto.ts
deleted file mode 100644
index fe0eb0d5c..000000000
--- a/server/src/domain/album/dto/album.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ValidateBoolean } from 'src/validation';
-
-export class AlbumInfoDto {
- @ValidateBoolean({ optional: true })
- withoutAssets?: boolean;
-}
diff --git a/server/src/domain/album/dto/get-albums.dto.ts b/server/src/domain/album/dto/get-albums.dto.ts
deleted file mode 100644
index 15e4f1cf2..000000000
--- a/server/src/domain/album/dto/get-albums.dto.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { ValidateBoolean, ValidateUUID } from 'src/validation';
-
-export class GetAlbumsDto {
- @ValidateBoolean({ optional: true })
- /**
- * true: only shared albums
- * false: only non-shared own albums
- * undefined: shared and owned albums
- */
- shared?: boolean;
-
- /**
- * Only returns albums that contain the asset
- * Ignores the shared parameter
- * undefined: get all albums
- */
- @ValidateUUID({ optional: true })
- assetId?: string;
-}
diff --git a/server/src/domain/asset/dto/asset-ids.dto.ts b/server/src/domain/asset/dto/asset-ids.dto.ts
deleted file mode 100644
index ea875e85e..000000000
--- a/server/src/domain/asset/dto/asset-ids.dto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum } from 'class-validator';
-import { ValidateUUID } from 'src/validation';
-
-export class AssetIdsDto {
- @ValidateUUID({ each: true })
- assetIds!: string[];
-}
-
-export enum AssetJobName {
- REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
- REFRESH_METADATA = 'refresh-metadata',
- TRANSCODE_VIDEO = 'transcode-video',
-}
-
-export class AssetJobsDto extends AssetIdsDto {
- @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
- @IsEnum(AssetJobName)
- name!: AssetJobName;
-}
diff --git a/server/src/domain/asset/dto/asset-statistics.dto.ts b/server/src/domain/asset/dto/asset-statistics.dto.ts
deleted file mode 100644
index 93e007029..000000000
--- a/server/src/domain/asset/dto/asset-statistics.dto.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { AssetType } from 'src/entities/asset.entity';
-import { AssetStats } from 'src/interfaces/asset.repository';
-import { ValidateBoolean } from 'src/validation';
-
-export class AssetStatsDto {
- @ValidateBoolean({ optional: true })
- isArchived?: boolean;
-
- @ValidateBoolean({ optional: true })
- isFavorite?: boolean;
-
- @ValidateBoolean({ optional: true })
- isTrashed?: boolean;
-}
-
-export class AssetStatsResponseDto {
- @ApiProperty({ type: 'integer' })
- images!: number;
-
- @ApiProperty({ type: 'integer' })
- videos!: number;
-
- @ApiProperty({ type: 'integer' })
- total!: number;
-}
-
-export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
- return {
- images: stats[AssetType.IMAGE],
- videos: stats[AssetType.VIDEO],
- total: Object.values(stats).reduce((total, value) => total + value, 0),
- };
-};
diff --git a/server/src/domain/asset/dto/asset.dto.ts b/server/src/domain/asset/dto/asset.dto.ts
deleted file mode 100644
index a93a59ae3..000000000
--- a/server/src/domain/asset/dto/asset.dto.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { Type } from 'class-transformer';
-import {
- IsDateString,
- IsInt,
- IsLatitude,
- IsLongitude,
- IsNotEmpty,
- IsPositive,
- IsString,
- ValidateIf,
-} from 'class-validator';
-import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
-
-export class DeviceIdDto {
- @IsNotEmpty()
- @IsString()
- deviceId!: string;
-}
-
-const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
- o.latitude !== undefined || o.longitude !== undefined;
-const ValidateGPS = () => ValidateIf(hasGPS);
-
-export class UpdateAssetBase {
- @ValidateBoolean({ optional: true })
- isFavorite?: boolean;
-
- @ValidateBoolean({ optional: true })
- isArchived?: boolean;
-
- @Optional()
- @IsDateString()
- dateTimeOriginal?: string;
-
- @ValidateGPS()
- @IsLatitude()
- @IsNotEmpty()
- latitude?: number;
-
- @ValidateGPS()
- @IsLongitude()
- @IsNotEmpty()
- longitude?: number;
-}
-
-export class AssetBulkUpdateDto extends UpdateAssetBase {
- @ValidateUUID({ each: true })
- ids!: string[];
-
- @ValidateUUID({ optional: true })
- stackParentId?: string;
-
- @ValidateBoolean({ optional: true })
- removeParent?: boolean;
-}
-
-export class UpdateAssetDto extends UpdateAssetBase {
- @Optional()
- @IsString()
- description?: string;
-}
-
-export class RandomAssetsDto {
- @Optional()
- @IsInt()
- @IsPositive()
- @Type(() => Number)
- count?: number;
-}
-
-export class AssetBulkDeleteDto extends BulkIdsDto {
- @ValidateBoolean({ optional: true })
- force?: boolean;
-}
diff --git a/server/src/domain/asset/dto/map-marker.dto.ts b/server/src/domain/asset/dto/map-marker.dto.ts
deleted file mode 100644
index 158750e51..000000000
--- a/server/src/domain/asset/dto/map-marker.dto.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import { ValidateBoolean, ValidateDate } from 'src/validation';
-
-export class MapMarkerDto {
- @ValidateBoolean({ optional: true })
- isArchived?: boolean;
-
- @ValidateBoolean({ optional: true })
- isFavorite?: boolean;
-
- @ValidateDate({ optional: true })
- fileCreatedAfter?: Date;
-
- @ValidateDate({ optional: true })
- fileCreatedBefore?: Date;
-
- @ValidateBoolean({ optional: true })
- withPartners?: boolean;
-}
diff --git a/server/src/domain/asset/dto/memory-lane.dto.ts b/server/src/domain/asset/dto/memory-lane.dto.ts
deleted file mode 100644
index 43f74aff1..000000000
--- a/server/src/domain/asset/dto/memory-lane.dto.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsInt, Max, Min } from 'class-validator';
-
-export class MemoryLaneDto {
- @IsInt()
- @Type(() => Number)
- @Max(31)
- @Min(1)
- @ApiProperty({ type: 'integer' })
- day!: number;
-
- @IsInt()
- @Type(() => Number)
- @Max(12)
- @Min(1)
- @ApiProperty({ type: 'integer' })
- month!: number;
-}
diff --git a/server/src/domain/asset/response-dto/map-marker-response.dto.ts b/server/src/domain/asset/response-dto/map-marker-response.dto.ts
deleted file mode 100644
index f5148883f..000000000
--- a/server/src/domain/asset/response-dto/map-marker-response.dto.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-
-export class MapMarkerResponseDto {
- @ApiProperty()
- id!: string;
-
- @ApiProperty({ format: 'double' })
- lat!: number;
-
- @ApiProperty({ format: 'double' })
- lon!: number;
-
- @ApiProperty()
- city!: string | null;
-
- @ApiProperty()
- state!: string | null;
-
- @ApiProperty()
- country!: string | null;
-}
diff --git a/server/src/domain/asset/response-dto/smart-info-response.dto.ts b/server/src/domain/asset/response-dto/smart-info-response.dto.ts
deleted file mode 100644
index c55c7ffb3..000000000
--- a/server/src/domain/asset/response-dto/smart-info-response.dto.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-
-export class SmartInfoResponseDto {
- tags?: string[] | null;
- objects?: string[] | null;
-}
-
-export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
- return {
- tags: entity.tags,
- objects: entity.objects,
- };
-}
diff --git a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts b/server/src/domain/asset/response-dto/time-bucket-response.dto.ts
deleted file mode 100644
index e143dde46..000000000
--- a/server/src/domain/asset/response-dto/time-bucket-response.dto.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-
-export class TimeBucketResponseDto {
- @ApiProperty({ type: 'string' })
- timeBucket!: string;
-
- @ApiProperty({ type: 'integer' })
- count!: number;
-}
diff --git a/server/src/domain/auth/auth.constant.ts b/server/src/domain/auth/auth.constant.ts
deleted file mode 100644
index f29fc9274..000000000
--- a/server/src/domain/auth/auth.constant.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export const MOBILE_REDIRECT = 'app.immich:/';
-export const LOGIN_URL = '/auth/login?autoLaunch=0';
-export const IMMICH_ACCESS_COOKIE = 'immich_access_token';
-export const IMMICH_IS_AUTHENTICATED = 'immich_is_authenticated';
-export const IMMICH_AUTH_TYPE_COOKIE = 'immich_auth_type';
-export const IMMICH_API_KEY_NAME = 'api_key';
-export const IMMICH_API_KEY_HEADER = 'x-api-key';
-export const IMMICH_SHARED_LINK_ACCESS_COOKIE = 'immich_shared_link_token';
-export enum AuthType {
- PASSWORD = 'password',
- OAUTH = 'oauth',
-}
diff --git a/server/src/domain/domain.module.ts b/server/src/domain/domain.module.ts
deleted file mode 100644
index 04d7c51f4..000000000
--- a/server/src/domain/domain.module.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import { Global, Module, Provider } from '@nestjs/common';
-import { ActivityService } from 'src/domain/activity/activity.service';
-import { AlbumService } from 'src/domain/album/album.service';
-import { APIKeyService } from 'src/domain/api-key/api-key.service';
-import { AssetService } from 'src/domain/asset/asset.service';
-import { AuditService } from 'src/domain/audit/audit.service';
-import { AuthService } from 'src/domain/auth/auth.service';
-import { DatabaseService } from 'src/domain/database/database.service';
-import { DownloadService } from 'src/domain/download/download.service';
-import { JobService } from 'src/domain/job/job.service';
-import { LibraryService } from 'src/domain/library/library.service';
-import { MediaService } from 'src/domain/media/media.service';
-import { MetadataService } from 'src/domain/metadata/metadata.service';
-import { PartnerService } from 'src/domain/partner/partner.service';
-import { PersonService } from 'src/domain/person/person.service';
-import { SearchService } from 'src/domain/search/search.service';
-import { ServerInfoService } from 'src/domain/server-info/server-info.service';
-import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
-import { SmartInfoService } from 'src/domain/smart-info/smart-info.service';
-import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service';
-import { StorageService } from 'src/domain/storage/storage.service';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
-import { TagService } from 'src/domain/tag/tag.service';
-import { TrashService } from 'src/domain/trash/trash.service';
-import { UserService } from 'src/domain/user/user.service';
-import { ImmichLogger } from 'src/infra/logger';
-
-const providers: Provider[] = [
- APIKeyService,
- ActivityService,
- AlbumService,
- AssetService,
- AuditService,
- AuthService,
- DatabaseService,
- DownloadService,
- ImmichLogger,
- JobService,
- LibraryService,
- MediaService,
- MetadataService,
- PartnerService,
- PersonService,
- SearchService,
- ServerInfoService,
- SharedLinkService,
- SmartInfoService,
- StorageService,
- StorageTemplateService,
- SystemConfigService,
- TagService,
- TrashService,
- UserService,
-];
-
-@Global()
-@Module({
- imports: [],
- providers: [...providers],
- exports: [...providers],
-})
-export class DomainModule {}
diff --git a/server/src/domain/job/job.constants.ts b/server/src/domain/job/job.constants.ts
deleted file mode 100644
index b2dac1551..000000000
--- a/server/src/domain/job/job.constants.ts
+++ /dev/null
@@ -1,157 +0,0 @@
-export enum QueueName {
- THUMBNAIL_GENERATION = 'thumbnailGeneration',
- METADATA_EXTRACTION = 'metadataExtraction',
- VIDEO_CONVERSION = 'videoConversion',
- FACE_DETECTION = 'faceDetection',
- FACIAL_RECOGNITION = 'facialRecognition',
- SMART_SEARCH = 'smartSearch',
- BACKGROUND_TASK = 'backgroundTask',
- STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
- MIGRATION = 'migration',
- SEARCH = 'search',
- SIDECAR = 'sidecar',
- LIBRARY = 'library',
-}
-
-export type ConcurrentQueueName = Exclude<
- QueueName,
- QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION
->;
-
-export enum JobCommand {
- START = 'start',
- PAUSE = 'pause',
- RESUME = 'resume',
- EMPTY = 'empty',
- CLEAR_FAILED = 'clear-failed',
-}
-
-export enum JobName {
- // conversion
- QUEUE_VIDEO_CONVERSION = 'queue-video-conversion',
- VIDEO_CONVERSION = 'video-conversion',
-
- // thumbnails
- QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
- GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
- GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
- GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
- GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
-
- // metadata
- QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
- METADATA_EXTRACTION = 'metadata-extraction',
- LINK_LIVE_PHOTOS = 'link-live-photos',
-
- // user
- USER_DELETION = 'user-deletion',
- USER_DELETE_CHECK = 'user-delete-check',
- USER_SYNC_USAGE = 'user-sync-usage',
-
- // asset
- ASSET_DELETION = 'asset-deletion',
- ASSET_DELETION_CHECK = 'asset-deletion-check',
-
- // storage template
- STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
- STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
-
- // migration
- QUEUE_MIGRATION = 'queue-migration',
- MIGRATE_ASSET = 'migrate-asset',
- MIGRATE_PERSON = 'migrate-person',
-
- // facial recognition
- PERSON_CLEANUP = 'person-cleanup',
- QUEUE_FACE_DETECTION = 'queue-face-detection',
- FACE_DETECTION = 'face-detection',
- QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition',
- FACIAL_RECOGNITION = 'facial-recognition',
-
- // library managment
- LIBRARY_SCAN = 'library-refresh',
- LIBRARY_SCAN_ASSET = 'library-refresh-asset',
- LIBRARY_DELETE = 'library-delete',
- LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh',
- LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
- LIBRARY_CHECK_OFFLINE = 'library-check-if-online',
- LIBRARY_REMOVE_OFFLINE = 'library-remove-offline',
-
- // cleanup
- DELETE_FILES = 'delete-files',
- CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
-
- // smart search
- QUEUE_SMART_SEARCH = 'queue-smart-search',
- SMART_SEARCH = 'smart-search',
-
- // XMP sidecars
- QUEUE_SIDECAR = 'queue-sidecar',
- SIDECAR_DISCOVERY = 'sidecar-discovery',
- SIDECAR_SYNC = 'sidecar-sync',
- SIDECAR_WRITE = 'sidecar-write',
-}
-
-export const JOBS_ASSET_PAGINATION_SIZE = 1000;
-
-export const JOBS_TO_QUEUE: Record = {
- // misc
- [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK,
- [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK,
- [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK,
- [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK,
- [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
- [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
- [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
- [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK,
-
- // conversion
- [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
- [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
-
- // thumbnails
- [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
- [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
- [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
- [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
- [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
-
- // metadata
- [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
- [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
- [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION,
-
- // storage template
- [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION,
- [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
-
- // migration
- [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION,
- [JobName.MIGRATE_ASSET]: QueueName.MIGRATION,
- [JobName.MIGRATE_PERSON]: QueueName.MIGRATION,
-
- // facial recognition
- [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION,
- [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION,
- [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
- [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
-
- // smart search
- [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH,
- [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH,
-
- // XMP sidecars
- [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
- [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
- [JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
- [JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
-
- // Library management
- [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
- [JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
- [JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
- [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
- [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY,
- [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
- [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
-};
diff --git a/server/src/domain/job/job.interface.ts b/server/src/domain/job/job.interface.ts
deleted file mode 100644
index 1ea7c8c8f..000000000
--- a/server/src/domain/job/job.interface.ts
+++ /dev/null
@@ -1,45 +0,0 @@
-export interface IBaseJob {
- force?: boolean;
-}
-
-export interface IEntityJob extends IBaseJob {
- id: string;
- source?: 'upload' | 'sidecar-write';
-}
-
-export interface IAssetDeletionJob extends IEntityJob {
- fromExternal?: boolean;
-}
-
-export interface ILibraryFileJob extends IEntityJob {
- ownerId: string;
- assetPath: string;
-}
-
-export interface ILibraryRefreshJob extends IEntityJob {
- refreshModifiedFiles: boolean;
- refreshAllFiles: boolean;
-}
-
-export interface ILibraryOfflineJob extends IEntityJob {
- importPaths: string[];
-}
-
-export interface IBulkEntityJob extends IBaseJob {
- ids: string[];
-}
-
-export interface IDeleteFilesJob extends IBaseJob {
- files: Array;
-}
-
-export interface ISidecarWriteJob extends IEntityJob {
- description?: string;
- dateTimeOriginal?: string;
- latitude?: number;
- longitude?: number;
-}
-
-export interface IDeferrableJob extends IEntityJob {
- deferred?: boolean;
-}
diff --git a/server/src/domain/media/media.constant.ts b/server/src/domain/media/media.constant.ts
deleted file mode 100644
index 3a8ee414b..000000000
--- a/server/src/domain/media/media.constant.ts
+++ /dev/null
@@ -1 +0,0 @@
-export const FACE_THUMBNAIL_SIZE = 250;
diff --git a/server/src/domain/search/dto/search-suggestion.dto.ts b/server/src/domain/search/dto/search-suggestion.dto.ts
deleted file mode 100644
index f702293d0..000000000
--- a/server/src/domain/search/dto/search-suggestion.dto.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
-import { Optional } from 'src/validation';
-
-export enum SearchSuggestionType {
- COUNTRY = 'country',
- STATE = 'state',
- CITY = 'city',
- CAMERA_MAKE = 'camera-make',
- CAMERA_MODEL = 'camera-model',
-}
-
-export class SearchSuggestionRequestDto {
- @IsEnum(SearchSuggestionType)
- @IsNotEmpty()
- @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
- type!: SearchSuggestionType;
-
- @IsString()
- @Optional()
- country?: string;
-
- @IsString()
- @Optional()
- state?: string;
-
- @IsString()
- @Optional()
- make?: string;
-
- @IsString()
- @Optional()
- model?: string;
-}
diff --git a/server/src/domain/search/response-dto/search-explore.response.dto.ts b/server/src/domain/search/response-dto/search-explore.response.dto.ts
deleted file mode 100644
index 33689b979..000000000
--- a/server/src/domain/search/response-dto/search-explore.response.dto.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-
-class SearchExploreItem {
- value!: string;
- data!: AssetResponseDto;
-}
-
-export class SearchExploreResponseDto {
- fieldName!: string;
- items!: SearchExploreItem[];
-}
diff --git a/server/src/domain/search/response-dto/search-response.dto.ts b/server/src/domain/search/response-dto/search-response.dto.ts
deleted file mode 100644
index 53563a9ac..000000000
--- a/server/src/domain/search/response-dto/search-response.dto.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { AlbumResponseDto } from 'src/domain/album/album-response.dto';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-
-class SearchFacetCountResponseDto {
- @ApiProperty({ type: 'integer' })
- count!: number;
- value!: string;
-}
-
-class SearchFacetResponseDto {
- fieldName!: string;
- counts!: SearchFacetCountResponseDto[];
-}
-
-class SearchAlbumResponseDto {
- @ApiProperty({ type: 'integer' })
- total!: number;
- @ApiProperty({ type: 'integer' })
- count!: number;
- items!: AlbumResponseDto[];
- facets!: SearchFacetResponseDto[];
-}
-
-class SearchAssetResponseDto {
- @ApiProperty({ type: 'integer' })
- total!: number;
- @ApiProperty({ type: 'integer' })
- count!: number;
- items!: AssetResponseDto[];
- facets!: SearchFacetResponseDto[];
- nextPage!: string | null;
-}
-
-export class SearchResponseDto {
- albums!: SearchAlbumResponseDto;
- assets!: SearchAssetResponseDto;
-}
diff --git a/server/src/domain/shared-link/shared-link.dto.ts b/server/src/domain/shared-link/shared-link.dto.ts
deleted file mode 100644
index 73928aac2..000000000
--- a/server/src/domain/shared-link/shared-link.dto.ts
+++ /dev/null
@@ -1,75 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum, IsString } from 'class-validator';
-import { SharedLinkType } from 'src/entities/shared-link.entity';
-import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
-
-export class SharedLinkCreateDto {
- @IsEnum(SharedLinkType)
- @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
- type!: SharedLinkType;
-
- @ValidateUUID({ each: true, optional: true })
- assetIds?: string[];
-
- @ValidateUUID({ optional: true })
- albumId?: string;
-
- @IsString()
- @Optional()
- description?: string;
-
- @IsString()
- @Optional()
- password?: string;
-
- @ValidateDate({ optional: true, nullable: true })
- expiresAt?: Date | null = null;
-
- @ValidateBoolean({ optional: true })
- allowUpload?: boolean;
-
- @ValidateBoolean({ optional: true })
- allowDownload?: boolean = true;
-
- @ValidateBoolean({ optional: true })
- showMetadata?: boolean = true;
-}
-
-export class SharedLinkEditDto {
- @Optional()
- description?: string;
-
- @Optional()
- password?: string;
-
- @Optional({ nullable: true })
- expiresAt?: Date | null;
-
- @Optional()
- allowUpload?: boolean;
-
- @ValidateBoolean({ optional: true })
- allowDownload?: boolean;
-
- @ValidateBoolean({ optional: true })
- showMetadata?: boolean;
-
- /**
- * Few clients cannot send null to set the expiryTime to never.
- * Setting this flag and not sending expiryAt is considered as null instead.
- * Clients that can send null values can ignore this.
- */
- @ValidateBoolean({ optional: true })
- changeExpiryTime?: boolean;
-}
-
-export class SharedLinkPasswordDto {
- @IsString()
- @Optional()
- @ApiProperty({ example: 'password' })
- password?: string;
-
- @IsString()
- @Optional()
- token?: string;
-}
diff --git a/server/src/domain/smart-info/smart-info.constant.ts b/server/src/domain/smart-info/smart-info.constant.ts
deleted file mode 100644
index 66c31b985..000000000
--- a/server/src/domain/smart-info/smart-info.constant.ts
+++ /dev/null
@@ -1,129 +0,0 @@
-export type ModelInfo = {
- dimSize: number;
-};
-
-export const CLIP_MODEL_INFO: Record = {
- RN50__openai: {
- dimSize: 1024,
- },
- RN50__yfcc15m: {
- dimSize: 1024,
- },
- RN50__cc12m: {
- dimSize: 1024,
- },
- RN101__openai: {
- dimSize: 512,
- },
- RN101__yfcc15m: {
- dimSize: 512,
- },
- RN50x4__openai: {
- dimSize: 640,
- },
- RN50x16__openai: {
- dimSize: 768,
- },
- RN50x64__openai: {
- dimSize: 1024,
- },
- 'ViT-B-32__openai': {
- dimSize: 512,
- },
- 'ViT-B-32__laion2b_e16': {
- dimSize: 512,
- },
- 'ViT-B-32__laion400m_e31': {
- dimSize: 512,
- },
- 'ViT-B-32__laion400m_e32': {
- dimSize: 512,
- },
- 'ViT-B-32__laion2b-s34b-b79k': {
- dimSize: 512,
- },
- 'ViT-B-16__openai': {
- dimSize: 512,
- },
- 'ViT-B-16__laion400m_e31': {
- dimSize: 512,
- },
- 'ViT-B-16__laion400m_e32': {
- dimSize: 512,
- },
- 'ViT-B-16-plus-240__laion400m_e31': {
- dimSize: 640,
- },
- 'ViT-B-16-plus-240__laion400m_e32': {
- dimSize: 640,
- },
- 'ViT-L-14__openai': {
- dimSize: 768,
- },
- 'ViT-L-14__laion400m_e31': {
- dimSize: 768,
- },
- 'ViT-L-14__laion400m_e32': {
- dimSize: 768,
- },
- 'ViT-L-14__laion2b-s32b-b82k': {
- dimSize: 768,
- },
- 'ViT-L-14-336__openai': {
- dimSize: 768,
- },
- 'ViT-L-14-quickgelu__dfn2b': {
- dimSize: 768,
- },
- 'ViT-H-14__laion2b-s32b-b79k': {
- dimSize: 1024,
- },
- 'ViT-H-14-quickgelu__dfn5b': {
- dimSize: 1024,
- },
- 'ViT-H-14-378-quickgelu__dfn5b': {
- dimSize: 1024,
- },
- 'ViT-g-14__laion2b-s12b-b42k': {
- dimSize: 1024,
- },
- 'LABSE-Vit-L-14': {
- dimSize: 768,
- },
- 'XLM-Roberta-Large-Vit-B-32': {
- dimSize: 512,
- },
- 'XLM-Roberta-Large-Vit-B-16Plus': {
- dimSize: 640,
- },
- 'XLM-Roberta-Large-Vit-L-14': {
- dimSize: 768,
- },
- 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': {
- dimSize: 1024,
- },
- 'nllb-clip-base-siglip__v1': {
- dimSize: 768,
- },
- 'nllb-clip-large-siglip__v1': {
- dimSize: 1152,
- },
-};
-
-export function cleanModelName(modelName: string): string {
- const token = modelName.split('/').at(-1);
- if (!token) {
- throw new Error(`Invalid model name: ${modelName}`);
- }
-
- return token.replaceAll(':', '_');
-}
-
-export function getCLIPModelInfo(modelName: string): ModelInfo {
- const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)];
- if (!modelInfo) {
- throw new Error(`Unknown CLIP model: ${modelName}`);
- }
-
- return modelInfo;
-}
diff --git a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts b/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts
deleted file mode 100644
index 017f69c2d..000000000
--- a/server/src/domain/system-config/dto/system-config-ffmpeg.dto.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsEnum, IsInt, IsString, Max, Min } from 'class-validator';
-import {
- AudioCodec,
- CQMode,
- ToneMapping,
- TranscodeHWAccel,
- TranscodePolicy,
- VideoCodec,
-} from 'src/entities/system-config.entity';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigFFmpegDto {
- @IsInt()
- @Min(0)
- @Max(51)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- crf!: number;
-
- @IsInt()
- @Min(0)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- threads!: number;
-
- @IsString()
- preset!: string;
-
- @IsEnum(VideoCodec)
- @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
- targetVideoCodec!: VideoCodec;
-
- @IsEnum(VideoCodec, { each: true })
- @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
- acceptedVideoCodecs!: VideoCodec[];
-
- @IsEnum(AudioCodec)
- @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
- targetAudioCodec!: AudioCodec;
-
- @IsEnum(AudioCodec, { each: true })
- @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
- acceptedAudioCodecs!: AudioCodec[];
-
- @IsString()
- targetResolution!: string;
-
- @IsString()
- maxBitrate!: string;
-
- @IsInt()
- @Min(-1)
- @Max(16)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- bframes!: number;
-
- @IsInt()
- @Min(0)
- @Max(6)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- refs!: number;
-
- @IsInt()
- @Min(0)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- gopSize!: number;
-
- @IsInt()
- @Min(0)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- npl!: number;
-
- @ValidateBoolean()
- temporalAQ!: boolean;
-
- @IsEnum(CQMode)
- @ApiProperty({ enumName: 'CQMode', enum: CQMode })
- cqMode!: CQMode;
-
- @ValidateBoolean()
- twoPass!: boolean;
-
- @IsString()
- preferredHwDevice!: string;
-
- @IsEnum(TranscodePolicy)
- @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
- transcode!: TranscodePolicy;
-
- @IsEnum(TranscodeHWAccel)
- @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
- accel!: TranscodeHWAccel;
-
- @IsEnum(ToneMapping)
- @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
- tonemap!: ToneMapping;
-}
diff --git a/server/src/domain/system-config/dto/system-config-job.dto.ts b/server/src/domain/system-config/dto/system-config-job.dto.ts
deleted file mode 100644
index 2769da327..000000000
--- a/server/src/domain/system-config/dto/system-config-job.dto.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator';
-import { ConcurrentQueueName, QueueName } from 'src/domain/job/job.constants';
-
-export class JobSettingsDto {
- @IsInt()
- @IsPositive()
- @ApiProperty({ type: 'integer' })
- concurrency!: number;
-}
-
-export class SystemConfigJobDto implements Record {
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.METADATA_EXTRACTION]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.VIDEO_CONVERSION]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.SMART_SEARCH]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.MIGRATION]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.BACKGROUND_TASK]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.SEARCH]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.FACE_DETECTION]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.SIDECAR]!: JobSettingsDto;
-
- @ApiProperty({ type: JobSettingsDto })
- @ValidateNested()
- @IsObject()
- @Type(() => JobSettingsDto)
- [QueueName.LIBRARY]!: JobSettingsDto;
-}
diff --git a/server/src/domain/system-config/dto/system-config-library.dto.ts b/server/src/domain/system-config/dto/system-config-library.dto.ts
deleted file mode 100644
index 8c7501ae4..000000000
--- a/server/src/domain/system-config/dto/system-config-library.dto.ts
+++ /dev/null
@@ -1,49 +0,0 @@
-import { Type } from 'class-transformer';
-import {
- IsNotEmpty,
- IsObject,
- IsString,
- Validate,
- ValidateIf,
- ValidateNested,
- ValidatorConstraint,
- ValidatorConstraintInterface,
-} from 'class-validator';
-import { ValidateBoolean, validateCronExpression } from 'src/validation';
-
-const isEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
-
-@ValidatorConstraint({ name: 'cronValidator' })
-class CronValidator implements ValidatorConstraintInterface {
- validate(expression: string): boolean {
- return validateCronExpression(expression);
- }
-}
-
-export class SystemConfigLibraryScanDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @ValidateIf(isEnabled)
- @IsNotEmpty()
- @Validate(CronValidator, { message: 'Invalid cron expression' })
- @IsString()
- cronExpression!: string;
-}
-
-export class SystemConfigLibraryWatchDto {
- @ValidateBoolean()
- enabled!: boolean;
-}
-
-export class SystemConfigLibraryDto {
- @Type(() => SystemConfigLibraryScanDto)
- @ValidateNested()
- @IsObject()
- scan!: SystemConfigLibraryScanDto;
-
- @Type(() => SystemConfigLibraryWatchDto)
- @ValidateNested()
- @IsObject()
- watch!: SystemConfigLibraryWatchDto;
-}
diff --git a/server/src/domain/system-config/dto/system-config-logging.dto.ts b/server/src/domain/system-config/dto/system-config-logging.dto.ts
deleted file mode 100644
index 53ef5d2f0..000000000
--- a/server/src/domain/system-config/dto/system-config-logging.dto.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum } from 'class-validator';
-import { LogLevel } from 'src/entities/system-config.entity';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigLoggingDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })
- @IsEnum(LogLevel)
- level!: LogLevel;
-}
diff --git a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts b/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts
deleted file mode 100644
index 058585992..000000000
--- a/server/src/domain/system-config/dto/system-config-machine-learning.dto.ts
+++ /dev/null
@@ -1,23 +0,0 @@
-import { Type } from 'class-transformer';
-import { IsObject, IsUrl, ValidateIf, ValidateNested } from 'class-validator';
-import { CLIPConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigMachineLearningDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @IsUrl({ require_tld: false, allow_underscores: true })
- @ValidateIf((dto) => dto.enabled)
- url!: string;
-
- @Type(() => CLIPConfig)
- @ValidateNested()
- @IsObject()
- clip!: CLIPConfig;
-
- @Type(() => RecognitionConfig)
- @ValidateNested()
- @IsObject()
- facialRecognition!: RecognitionConfig;
-}
diff --git a/server/src/domain/system-config/dto/system-config-map.dto.ts b/server/src/domain/system-config/dto/system-config-map.dto.ts
deleted file mode 100644
index 9ec0abfa4..000000000
--- a/server/src/domain/system-config/dto/system-config-map.dto.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { IsString } from 'class-validator';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigMapDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @IsString()
- lightStyle!: string;
-
- @IsString()
- darkStyle!: string;
-}
diff --git a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts b/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts
deleted file mode 100644
index 7d5c5134f..000000000
--- a/server/src/domain/system-config/dto/system-config-new-version-check.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigNewVersionCheckDto {
- @ValidateBoolean()
- enabled!: boolean;
-}
diff --git a/server/src/domain/system-config/dto/system-config-oauth.dto.ts b/server/src/domain/system-config/dto/system-config-oauth.dto.ts
deleted file mode 100644
index 9c7fc5f40..000000000
--- a/server/src/domain/system-config/dto/system-config-oauth.dto.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { IsNotEmpty, IsNumber, IsString, IsUrl, Min, ValidateIf } from 'class-validator';
-import { ValidateBoolean } from 'src/validation';
-
-const isEnabled = (config: SystemConfigOAuthDto) => config.enabled;
-const isOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
-
-export class SystemConfigOAuthDto {
- @ValidateBoolean()
- autoLaunch!: boolean;
-
- @ValidateBoolean()
- autoRegister!: boolean;
-
- @IsString()
- buttonText!: string;
-
- @ValidateIf(isEnabled)
- @IsNotEmpty()
- @IsString()
- clientId!: string;
-
- @ValidateIf(isEnabled)
- @IsNotEmpty()
- @IsString()
- clientSecret!: string;
-
- @IsNumber()
- @Min(0)
- defaultStorageQuota!: number;
-
- @ValidateBoolean()
- enabled!: boolean;
-
- @ValidateIf(isEnabled)
- @IsNotEmpty()
- @IsString()
- issuerUrl!: string;
-
- @ValidateBoolean()
- mobileOverrideEnabled!: boolean;
-
- @ValidateIf(isOverrideEnabled)
- @IsUrl()
- mobileRedirectUri!: string;
-
- @IsString()
- scope!: string;
-
- @IsString()
- @IsNotEmpty()
- signingAlgorithm!: string;
-
- @IsString()
- storageLabelClaim!: string;
-
- @IsString()
- storageQuotaClaim!: string;
-}
diff --git a/server/src/domain/system-config/dto/system-config-password-login.dto.ts b/server/src/domain/system-config/dto/system-config-password-login.dto.ts
deleted file mode 100644
index 8d49a7002..000000000
--- a/server/src/domain/system-config/dto/system-config-password-login.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigPasswordLoginDto {
- @ValidateBoolean()
- enabled!: boolean;
-}
diff --git a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts b/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
deleted file mode 100644
index 8ff286601..000000000
--- a/server/src/domain/system-config/dto/system-config-reverse-geocoding.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigReverseGeocodingDto {
- @ValidateBoolean()
- enabled!: boolean;
-}
diff --git a/server/src/domain/system-config/dto/system-config-server.dto.ts b/server/src/domain/system-config/dto/system-config-server.dto.ts
deleted file mode 100644
index 83a2b0df9..000000000
--- a/server/src/domain/system-config/dto/system-config-server.dto.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { IsString } from 'class-validator';
-
-export class SystemConfigServerDto {
- @IsString()
- externalDomain!: string;
-
- @IsString()
- loginPageMessage!: string;
-}
diff --git a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts b/server/src/domain/system-config/dto/system-config-storage-template.dto.ts
deleted file mode 100644
index 77204b46e..000000000
--- a/server/src/domain/system-config/dto/system-config-storage-template.dto.ts
+++ /dev/null
@@ -1,14 +0,0 @@
-import { IsNotEmpty, IsString } from 'class-validator';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigStorageTemplateDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @ValidateBoolean()
- hashVerificationEnabled!: boolean;
-
- @IsNotEmpty()
- @IsString()
- template!: string;
-}
diff --git a/server/src/domain/system-config/dto/system-config-theme.dto.ts b/server/src/domain/system-config/dto/system-config-theme.dto.ts
deleted file mode 100644
index f47b51e0e..000000000
--- a/server/src/domain/system-config/dto/system-config-theme.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { IsString } from 'class-validator';
-
-export class SystemConfigThemeDto {
- @IsString()
- customCss!: string;
-}
diff --git a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts b/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts
deleted file mode 100644
index d3240efb1..000000000
--- a/server/src/domain/system-config/dto/system-config-thumbnail.dto.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsEnum, IsInt, Max, Min } from 'class-validator';
-import { Colorspace } from 'src/entities/system-config.entity';
-
-export class SystemConfigThumbnailDto {
- @IsInt()
- @Min(1)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- webpSize!: number;
-
- @IsInt()
- @Min(1)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- jpegSize!: number;
-
- @IsInt()
- @Min(1)
- @Max(100)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- quality!: number;
-
- @IsEnum(Colorspace)
- @ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
- colorspace!: Colorspace;
-}
diff --git a/server/src/domain/system-config/dto/system-config-trash.dto.ts b/server/src/domain/system-config/dto/system-config-trash.dto.ts
deleted file mode 100644
index a9e5483eb..000000000
--- a/server/src/domain/system-config/dto/system-config-trash.dto.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsInt, Min } from 'class-validator';
-import { ValidateBoolean } from 'src/validation';
-
-export class SystemConfigTrashDto {
- @ValidateBoolean()
- enabled!: boolean;
-
- @IsInt()
- @Min(0)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- days!: number;
-}
diff --git a/server/src/domain/system-config/dto/system-config-user.dto.ts b/server/src/domain/system-config/dto/system-config-user.dto.ts
deleted file mode 100644
index 22d6ef5fc..000000000
--- a/server/src/domain/system-config/dto/system-config-user.dto.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsInt, Min } from 'class-validator';
-
-export class SystemConfigUserDto {
- @IsInt()
- @Min(1)
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- deleteDelay!: number;
-}
diff --git a/server/src/domain/system-config/dto/system-config.dto.ts b/server/src/domain/system-config/dto/system-config.dto.ts
deleted file mode 100644
index 9aef92303..000000000
--- a/server/src/domain/system-config/dto/system-config.dto.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { Type } from 'class-transformer';
-import { IsObject, ValidateNested } from 'class-validator';
-import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto';
-import { SystemConfigJobDto } from 'src/domain/system-config/dto/system-config-job.dto';
-import { SystemConfigLibraryDto } from 'src/domain/system-config/dto/system-config-library.dto';
-import { SystemConfigLoggingDto } from 'src/domain/system-config/dto/system-config-logging.dto';
-import { SystemConfigMachineLearningDto } from 'src/domain/system-config/dto/system-config-machine-learning.dto';
-import { SystemConfigMapDto } from 'src/domain/system-config/dto/system-config-map.dto';
-import { SystemConfigNewVersionCheckDto } from 'src/domain/system-config/dto/system-config-new-version-check.dto';
-import { SystemConfigOAuthDto } from 'src/domain/system-config/dto/system-config-oauth.dto';
-import { SystemConfigPasswordLoginDto } from 'src/domain/system-config/dto/system-config-password-login.dto';
-import { SystemConfigReverseGeocodingDto } from 'src/domain/system-config/dto/system-config-reverse-geocoding.dto';
-import { SystemConfigServerDto } from 'src/domain/system-config/dto/system-config-server.dto';
-import { SystemConfigStorageTemplateDto } from 'src/domain/system-config/dto/system-config-storage-template.dto';
-import { SystemConfigThemeDto } from 'src/domain/system-config/dto/system-config-theme.dto';
-import { SystemConfigThumbnailDto } from 'src/domain/system-config/dto/system-config-thumbnail.dto';
-import { SystemConfigTrashDto } from 'src/domain/system-config/dto/system-config-trash.dto';
-import { SystemConfigUserDto } from 'src/domain/system-config/dto/system-config-user.dto';
-import { SystemConfig } from 'src/entities/system-config.entity';
-
-export class SystemConfigDto implements SystemConfig {
- @Type(() => SystemConfigFFmpegDto)
- @ValidateNested()
- @IsObject()
- ffmpeg!: SystemConfigFFmpegDto;
-
- @Type(() => SystemConfigLoggingDto)
- @ValidateNested()
- @IsObject()
- logging!: SystemConfigLoggingDto;
-
- @Type(() => SystemConfigMachineLearningDto)
- @ValidateNested()
- @IsObject()
- machineLearning!: SystemConfigMachineLearningDto;
-
- @Type(() => SystemConfigMapDto)
- @ValidateNested()
- @IsObject()
- map!: SystemConfigMapDto;
-
- @Type(() => SystemConfigNewVersionCheckDto)
- @ValidateNested()
- @IsObject()
- newVersionCheck!: SystemConfigNewVersionCheckDto;
-
- @Type(() => SystemConfigOAuthDto)
- @ValidateNested()
- @IsObject()
- oauth!: SystemConfigOAuthDto;
-
- @Type(() => SystemConfigPasswordLoginDto)
- @ValidateNested()
- @IsObject()
- passwordLogin!: SystemConfigPasswordLoginDto;
-
- @Type(() => SystemConfigReverseGeocodingDto)
- @ValidateNested()
- @IsObject()
- reverseGeocoding!: SystemConfigReverseGeocodingDto;
-
- @Type(() => SystemConfigStorageTemplateDto)
- @ValidateNested()
- @IsObject()
- storageTemplate!: SystemConfigStorageTemplateDto;
-
- @Type(() => SystemConfigJobDto)
- @ValidateNested()
- @IsObject()
- job!: SystemConfigJobDto;
-
- @Type(() => SystemConfigThumbnailDto)
- @ValidateNested()
- @IsObject()
- thumbnail!: SystemConfigThumbnailDto;
-
- @Type(() => SystemConfigTrashDto)
- @ValidateNested()
- @IsObject()
- trash!: SystemConfigTrashDto;
-
- @Type(() => SystemConfigThemeDto)
- @ValidateNested()
- @IsObject()
- theme!: SystemConfigThemeDto;
-
- @Type(() => SystemConfigLibraryDto)
- @ValidateNested()
- @IsObject()
- library!: SystemConfigLibraryDto;
-
- @Type(() => SystemConfigServerDto)
- @ValidateNested()
- @IsObject()
- server!: SystemConfigServerDto;
-
- @Type(() => SystemConfigUserDto)
- @ValidateNested()
- @IsObject()
- user!: SystemConfigUserDto;
-}
-
-export function mapConfig(config: SystemConfig): SystemConfigDto {
- return config;
-}
diff --git a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts b/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts
deleted file mode 100644
index f0c8b9b64..000000000
--- a/server/src/domain/system-config/response-dto/system-config-template-storage-option.dto.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-export class SystemConfigTemplateStorageOptionDto {
- yearOptions!: string[];
- monthOptions!: string[];
- weekOptions!: string[];
- dayOptions!: string[];
- hourOptions!: string[];
- minuteOptions!: string[];
- secondOptions!: string[];
- presetOptions!: string[];
-}
diff --git a/server/src/domain/system-config/system-config-map-theme.dto.ts b/server/src/domain/system-config/system-config-map-theme.dto.ts
deleted file mode 100644
index 9286d8d23..000000000
--- a/server/src/domain/system-config/system-config-map-theme.dto.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum } from 'class-validator';
-
-export enum MapTheme {
- LIGHT = 'light',
- DARK = 'dark',
-}
-
-export class MapThemeDto {
- @IsEnum(MapTheme)
- @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' })
- theme!: MapTheme;
-}
diff --git a/server/src/domain/system-config/system-config.constants.ts b/server/src/domain/system-config/system-config.constants.ts
deleted file mode 100644
index 0290472aa..000000000
--- a/server/src/domain/system-config/system-config.constants.ts
+++ /dev/null
@@ -1,27 +0,0 @@
-export const supportedYearTokens = ['y', 'yy'];
-export const supportedMonthTokens = ['M', 'MM', 'MMM', 'MMMM'];
-export const supportedWeekTokens = ['W', 'WW'];
-export const supportedDayTokens = ['d', 'dd'];
-export const supportedHourTokens = ['h', 'hh', 'H', 'HH'];
-export const supportedMinuteTokens = ['m', 'mm'];
-export const supportedSecondTokens = ['s', 'ss', 'SSS'];
-export const supportedPresetTokens = [
- '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
- '{{y}}/{{MM}}-{{dd}}/{{filename}}',
- '{{y}}/{{MMMM}}-{{dd}}/{{filename}}',
- '{{y}}/{{MM}}/{{filename}}',
- '{{y}}/{{MMM}}/{{filename}}',
- '{{y}}/{{MMMM}}/{{filename}}',
- '{{y}}/{{MM}}/{{dd}}/{{filename}}',
- '{{y}}/{{MMMM}}/{{dd}}/{{filename}}',
- '{{y}}/{{y}}-{{MM}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
- '{{y}}-{{MM}}-{{dd}}/{{filename}}',
- '{{y}}-{{MMM}}-{{dd}}/{{filename}}',
- '{{y}}-{{MMMM}}-{{dd}}/{{filename}}',
- '{{y}}/{{y}}-{{MM}}/{{filename}}',
- '{{y}}/{{y}}-{{WW}}/{{filename}}',
- '{{y}}/{{y}}-{{MM}}-{{dd}}/{{assetId}}',
- '{{y}}/{{y}}-{{MM}}/{{assetId}}',
- '{{y}}/{{y}}-{{WW}}/{{assetId}}',
- '{{album}}/{{filename}}',
-];
diff --git a/server/src/domain/tag/tag.dto.ts b/server/src/domain/tag/tag.dto.ts
deleted file mode 100644
index abf9549d8..000000000
--- a/server/src/domain/tag/tag.dto.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
-import { TagType } from 'src/entities/tag.entity';
-import { Optional } from 'src/validation';
-
-export class CreateTagDto {
- @IsString()
- @IsNotEmpty()
- name!: string;
-
- @IsEnum(TagType)
- @IsNotEmpty()
- @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
- type!: TagType;
-}
-
-export class UpdateTagDto {
- @IsString()
- @Optional()
- name?: string;
-}
diff --git a/server/src/domain/user/dto/create-profile-image.dto.ts b/server/src/domain/user/dto/create-profile-image.dto.ts
deleted file mode 100644
index 37a7d1340..000000000
--- a/server/src/domain/user/dto/create-profile-image.dto.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { UploadFieldName } from 'src/domain/asset/asset.service';
-
-export class CreateProfileImageDto {
- @ApiProperty({ type: 'string', format: 'binary' })
- [UploadFieldName.PROFILE_DATA]!: Express.Multer.File;
-}
diff --git a/server/src/domain/user/dto/create-user.dto.ts b/server/src/domain/user/dto/create-user.dto.ts
deleted file mode 100644
index 7861c58c2..000000000
--- a/server/src/domain/user/dto/create-user.dto.ts
+++ /dev/null
@@ -1,61 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Transform } from 'class-transformer';
-import { IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator';
-import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
-
-export class CreateUserDto {
- @IsEmail({ require_tld: false })
- @Transform(toEmail)
- email!: string;
-
- @IsNotEmpty()
- @IsString()
- password!: string;
-
- @IsNotEmpty()
- @IsString()
- name!: string;
-
- @Optional({ nullable: true })
- @IsString()
- @Transform(toSanitized)
- storageLabel?: string | null;
-
- @ValidateBoolean({ optional: true })
- memoriesEnabled?: boolean;
-
- @Optional({ nullable: true })
- @IsNumber()
- @IsPositive()
- @ApiProperty({ type: 'integer', format: 'int64' })
- quotaSizeInBytes?: number | null;
-
- @ValidateBoolean({ optional: true })
- shouldChangePassword?: boolean;
-}
-
-export class CreateAdminDto {
- @IsNotEmpty()
- isAdmin!: true;
-
- @IsEmail({ require_tld: false })
- @Transform(({ value }) => value?.toLowerCase())
- email!: string;
-
- @IsNotEmpty()
- password!: string;
-
- @IsNotEmpty()
- name!: string;
-}
-
-export class CreateUserOAuthDto {
- @IsEmail({ require_tld: false })
- @Transform(({ value }) => value?.toLowerCase())
- email!: string;
-
- @IsNotEmpty()
- oauthId!: string;
-
- name?: string;
-}
diff --git a/server/src/domain/user/dto/delete-user.dto.ts b/server/src/domain/user/dto/delete-user.dto.ts
deleted file mode 100644
index aa41e18aa..000000000
--- a/server/src/domain/user/dto/delete-user.dto.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { ValidateBoolean } from 'src/validation';
-
-export class DeleteUserDto {
- @ValidateBoolean({ optional: true })
- force?: boolean;
-}
diff --git a/server/src/domain/user/dto/update-user.dto.spec.ts b/server/src/domain/user/dto/update-user.dto.spec.ts
deleted file mode 100644
index 0ad407be3..000000000
--- a/server/src/domain/user/dto/update-user.dto.spec.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { plainToInstance } from 'class-transformer';
-import { validate } from 'class-validator';
-import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
-
-describe('update user DTO', () => {
- it('should allow emails without a tld', async () => {
- const someEmail = 'test@test';
-
- const dto = plainToInstance(UpdateUserDto, {
- email: someEmail,
- id: '3fe388e4-2078-44d7-b36c-39d9dee3a657',
- });
- const errors = await validate(dto);
- expect(errors).toHaveLength(0);
- expect(dto.email).toEqual(someEmail);
- });
-});
diff --git a/server/src/domain/user/dto/update-user.dto.ts b/server/src/domain/user/dto/update-user.dto.ts
deleted file mode 100644
index 059971e6c..000000000
--- a/server/src/domain/user/dto/update-user.dto.ts
+++ /dev/null
@@ -1,52 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Transform } from 'class-transformer';
-import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
-import { UserAvatarColor } from 'src/entities/user.entity';
-import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
-
-export class UpdateUserDto {
- @Optional()
- @IsEmail({ require_tld: false })
- @Transform(toEmail)
- email?: string;
-
- @Optional()
- @IsNotEmpty()
- @IsString()
- password?: string;
-
- @Optional()
- @IsString()
- @IsNotEmpty()
- name?: string;
-
- @Optional()
- @IsString()
- @Transform(toSanitized)
- storageLabel?: string;
-
- @IsNotEmpty()
- @IsUUID('4')
- @ApiProperty({ format: 'uuid' })
- id!: string;
-
- @ValidateBoolean({ optional: true })
- isAdmin?: boolean;
-
- @ValidateBoolean({ optional: true })
- shouldChangePassword?: boolean;
-
- @ValidateBoolean({ optional: true })
- memoriesEnabled?: boolean;
-
- @Optional()
- @IsEnum(UserAvatarColor)
- @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
- avatarColor?: UserAvatarColor;
-
- @Optional({ nullable: true })
- @IsNumber()
- @IsPositive()
- @ApiProperty({ type: 'integer', format: 'int64' })
- quotaSizeInBytes?: number | null;
-}
diff --git a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts b/server/src/domain/user/response-dto/create-profile-image-response.dto.ts
deleted file mode 100644
index 2c7fd17be..000000000
--- a/server/src/domain/user/response-dto/create-profile-image-response.dto.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-export class CreateProfileImageResponseDto {
- userId!: string;
- profileImagePath!: string;
-}
-
-export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
- return {
- userId: userId,
- profileImagePath: profileImagePath,
- };
-}
diff --git a/server/src/domain/user/response-dto/user-response.dto.ts b/server/src/domain/user/response-dto/user-response.dto.ts
deleted file mode 100644
index 12ffca48c..000000000
--- a/server/src/domain/user/response-dto/user-response.dto.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum } from 'class-validator';
-import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
-
-export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
- const values = Object.values(UserAvatarColor);
- const randomIndex = Math.floor(
- [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
- );
- return values[randomIndex] as UserAvatarColor;
-};
-
-export class UserDto {
- id!: string;
- name!: string;
- email!: string;
- profileImagePath!: string;
- @IsEnum(UserAvatarColor)
- @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
- avatarColor!: UserAvatarColor;
-}
-
-export class UserResponseDto extends UserDto {
- storageLabel!: string | null;
- shouldChangePassword!: boolean;
- isAdmin!: boolean;
- createdAt!: Date;
- deletedAt!: Date | null;
- updatedAt!: Date;
- oauthId!: string;
- memoriesEnabled?: boolean;
- @ApiProperty({ type: 'integer', format: 'int64' })
- quotaSizeInBytes!: number | null;
- @ApiProperty({ type: 'integer', format: 'int64' })
- quotaUsageInBytes!: number | null;
- @ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
- status!: string;
-}
-
-export const mapSimpleUser = (entity: UserEntity): UserDto => {
- return {
- id: entity.id,
- email: entity.email,
- name: entity.name,
- profileImagePath: entity.profileImagePath,
- avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
- };
-};
-
-export function mapUser(entity: UserEntity): UserResponseDto {
- return {
- ...mapSimpleUser(entity),
- storageLabel: entity.storageLabel,
- shouldChangePassword: entity.shouldChangePassword,
- isAdmin: entity.isAdmin,
- createdAt: entity.createdAt,
- deletedAt: entity.deletedAt,
- updatedAt: entity.updatedAt,
- oauthId: entity.oauthId,
- memoriesEnabled: entity.memoriesEnabled,
- quotaSizeInBytes: entity.quotaSizeInBytes,
- quotaUsageInBytes: entity.quotaUsageInBytes,
- status: entity.status,
- };
-}
diff --git a/server/src/domain/activity/activity.dto.ts b/server/src/dtos/activity.dto.ts
similarity index 95%
rename from server/src/domain/activity/activity.dto.ts
rename to server/src/dtos/activity.dto.ts
index a9d865778..bd0d40095 100644
--- a/server/src/domain/activity/activity.dto.ts
+++ b/server/src/dtos/activity.dto.ts
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString, ValidateIf } from 'class-validator';
-import { UserDto, mapSimpleUser } from 'src/domain/user/response-dto/user-response.dto';
+import { UserDto, mapSimpleUser } from 'src/dtos/user.dto';
import { ActivityEntity } from 'src/entities/activity.entity';
import { Optional, ValidateUUID } from 'src/validation';
diff --git a/server/src/domain/album/album-response.dto.spec.ts b/server/src/dtos/album-response.dto.spec.ts
similarity index 89%
rename from server/src/domain/album/album-response.dto.spec.ts
rename to server/src/dtos/album-response.dto.spec.ts
index 568b416b4..2a6d59abf 100644
--- a/server/src/domain/album/album-response.dto.spec.ts
+++ b/server/src/dtos/album-response.dto.spec.ts
@@ -1,4 +1,4 @@
-import { mapAlbum } from 'src/domain/album/album-response.dto';
+import { mapAlbum } from 'src/dtos/album.dto';
import { albumStub } from 'test/fixtures/album.stub';
describe('mapAlbum', () => {
diff --git a/server/src/domain/album/album-response.dto.ts b/server/src/dtos/album.dto.ts
similarity index 59%
rename from server/src/domain/album/album-response.dto.ts
rename to server/src/dtos/album.dto.ts
index b016a1200..3f7af0f53 100644
--- a/server/src/domain/album/album-response.dto.ts
+++ b/server/src/dtos/album.dto.ts
@@ -1,9 +1,87 @@
import { ApiProperty } from '@nestjs/swagger';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
+import { ArrayNotEmpty, IsEnum, IsString } from 'class-validator';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AlbumEntity, AssetOrder } from 'src/entities/album.entity';
-import { Optional } from 'src/validation';
+import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
+
+export class AlbumInfoDto {
+ @ValidateBoolean({ optional: true })
+ withoutAssets?: boolean;
+}
+
+export class AddUsersDto {
+ @ValidateUUID({ each: true })
+ @ArrayNotEmpty()
+ sharedUserIds!: string[];
+}
+
+export class CreateAlbumDto {
+ @IsString()
+ @ApiProperty()
+ albumName!: string;
+
+ @IsString()
+ @Optional()
+ description?: string;
+
+ @ValidateUUID({ optional: true, each: true })
+ sharedWithUserIds?: string[];
+
+ @ValidateUUID({ optional: true, each: true })
+ assetIds?: string[];
+}
+
+export class UpdateAlbumDto {
+ @Optional()
+ @IsString()
+ albumName?: string;
+
+ @Optional()
+ @IsString()
+ description?: string;
+
+ @ValidateUUID({ optional: true })
+ albumThumbnailAssetId?: string;
+
+ @ValidateBoolean({ optional: true })
+ isActivityEnabled?: boolean;
+
+ @IsEnum(AssetOrder)
+ @Optional()
+ @ApiProperty({ enum: AssetOrder, enumName: 'AssetOrder' })
+ order?: AssetOrder;
+}
+
+export class GetAlbumsDto {
+ @ValidateBoolean({ optional: true })
+ /**
+ * true: only shared albums
+ * false: only non-shared own albums
+ * undefined: shared and owned albums
+ */
+ shared?: boolean;
+
+ /**
+ * Only returns albums that contain the asset
+ * Ignores the shared parameter
+ * undefined: get all albums
+ */
+ @ValidateUUID({ optional: true })
+ assetId?: string;
+}
+
+export class AlbumCountResponseDto {
+ @ApiProperty({ type: 'integer' })
+ owned!: number;
+
+ @ApiProperty({ type: 'integer' })
+ shared!: number;
+
+ @ApiProperty({ type: 'integer' })
+ notShared!: number;
+}
export class AlbumResponseDto {
id!: string;
@@ -73,14 +151,3 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
export const mapAlbumWithAssets = (entity: AlbumEntity) => mapAlbum(entity, true);
export const mapAlbumWithoutAssets = (entity: AlbumEntity) => mapAlbum(entity, false);
-
-export class AlbumCountResponseDto {
- @ApiProperty({ type: 'integer' })
- owned!: number;
-
- @ApiProperty({ type: 'integer' })
- shared!: number;
-
- @ApiProperty({ type: 'integer' })
- notShared!: number;
-}
diff --git a/server/src/domain/api-key/api-key.dto.ts b/server/src/dtos/api-key.dto.ts
similarity index 100%
rename from server/src/domain/api-key/api-key.dto.ts
rename to server/src/dtos/api-key.dto.ts
diff --git a/server/src/domain/asset/response-dto/asset-ids-response.dto.ts b/server/src/dtos/asset-ids.response.dto.ts
similarity index 100%
rename from server/src/domain/asset/response-dto/asset-ids-response.dto.ts
rename to server/src/dtos/asset-ids.response.dto.ts
diff --git a/server/src/domain/asset/response-dto/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts
similarity index 88%
rename from server/src/domain/asset/response-dto/asset-response.dto.ts
rename to server/src/dtos/asset-response.dto.ts
index dff12d481..04e36645e 100644
--- a/server/src/domain/asset/response-dto/asset-response.dto.ts
+++ b/server/src/dtos/asset-response.dto.ts
@@ -1,12 +1,12 @@
import { ApiProperty } from '@nestjs/swagger';
-import { ExifResponseDto, mapExif } from 'src/domain/asset/response-dto/exif-response.dto';
-import { SmartInfoResponseDto, mapSmartInfo } from 'src/domain/asset/response-dto/smart-info-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/domain/person/person.dto';
-import { TagResponseDto, mapTag } from 'src/domain/tag/tag-response.dto';
-import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { ExifResponseDto, mapExif } from 'src/dtos/exif.dto';
+import { PersonWithFacesResponseDto, mapFacesWithoutPerson, mapPerson } from 'src/dtos/person.dto';
+import { TagResponseDto, mapTag } from 'src/dtos/tag.dto';
+import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
+import { SmartInfoEntity } from 'src/entities/smart-info.entity';
export class SanitizedAssetResponseDto {
id!: string;
@@ -134,3 +134,15 @@ export class MemoryLaneResponseDto {
title!: string;
assets!: AssetResponseDto[];
}
+
+export class SmartInfoResponseDto {
+ tags?: string[] | null;
+ objects?: string[] | null;
+}
+
+export function mapSmartInfo(entity: SmartInfoEntity): SmartInfoResponseDto {
+ return {
+ tags: entity.tags,
+ objects: entity.objects,
+ };
+}
diff --git a/server/src/dtos/asset-v1-response.dto.ts b/server/src/dtos/asset-v1-response.dto.ts
new file mode 100644
index 000000000..4b1e97b47
--- /dev/null
+++ b/server/src/dtos/asset-v1-response.dto.ts
@@ -0,0 +1,45 @@
+export class AssetBulkUploadCheckResult {
+ id!: string;
+ action!: AssetUploadAction;
+ reason?: AssetRejectReason;
+ assetId?: string;
+}
+
+export class AssetBulkUploadCheckResponseDto {
+ results!: AssetBulkUploadCheckResult[];
+}
+
+export enum AssetUploadAction {
+ ACCEPT = 'accept',
+ REJECT = 'reject',
+}
+
+export enum AssetRejectReason {
+ DUPLICATE = 'duplicate',
+ UNSUPPORTED_FORMAT = 'unsupported-format',
+}
+
+export class AssetFileUploadResponseDto {
+ id!: string;
+ duplicate!: boolean;
+}
+
+export class CheckExistingAssetsResponseDto {
+ existingIds!: string[];
+}
+
+export class CuratedLocationsResponseDto {
+ id!: string;
+ city!: string;
+ resizePath!: string;
+ deviceAssetId!: string;
+ deviceId!: string;
+}
+
+export class CuratedObjectsResponseDto {
+ id!: string;
+ object!: string;
+ resizePath!: string;
+ deviceAssetId!: string;
+ deviceId!: string;
+}
diff --git a/server/src/dtos/asset-v1.dto.ts b/server/src/dtos/asset-v1.dto.ts
new file mode 100644
index 000000000..50ff3d18b
--- /dev/null
+++ b/server/src/dtos/asset-v1.dto.ts
@@ -0,0 +1,154 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Type } from 'class-transformer';
+import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
+import { UploadFieldName } from 'src/dtos/asset.dto';
+import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
+
+export class AssetBulkUploadCheckItem {
+ @IsString()
+ @IsNotEmpty()
+ id!: string;
+
+ /** base64 or hex encoded sha1 hash */
+ @IsString()
+ @IsNotEmpty()
+ checksum!: string;
+}
+
+export class AssetBulkUploadCheckDto {
+ @IsArray()
+ @ValidateNested({ each: true })
+ @Type(() => AssetBulkUploadCheckItem)
+ assets!: AssetBulkUploadCheckItem[];
+}
+
+export class AssetSearchDto {
+ @ValidateBoolean({ optional: true })
+ isFavorite?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isArchived?: boolean;
+
+ @Optional()
+ @IsInt()
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ skip?: number;
+
+ @Optional()
+ @IsInt()
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ take?: number;
+
+ @Optional()
+ @IsUUID('4')
+ @ApiProperty({ format: 'uuid' })
+ userId?: string;
+
+ @ValidateDate({ optional: true })
+ updatedAfter?: Date;
+
+ @ValidateDate({ optional: true })
+ updatedBefore?: Date;
+}
+
+export class CheckExistingAssetsDto {
+ @ArrayNotEmpty()
+ @IsString({ each: true })
+ @IsNotEmpty({ each: true })
+ deviceAssetIds!: string[];
+
+ @IsNotEmpty()
+ deviceId!: string;
+}
+
+export class CreateAssetDto {
+ @ValidateUUID({ optional: true })
+ libraryId?: string;
+
+ @IsNotEmpty()
+ @IsString()
+ deviceAssetId!: string;
+
+ @IsNotEmpty()
+ @IsString()
+ deviceId!: string;
+
+ @ValidateDate()
+ fileCreatedAt!: Date;
+
+ @ValidateDate()
+ fileModifiedAt!: Date;
+
+ @Optional()
+ @IsString()
+ duration?: string;
+
+ @ValidateBoolean({ optional: true })
+ isFavorite?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isArchived?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isVisible?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isOffline?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isReadOnly?: boolean;
+
+ // The properties below are added to correctly generate the API docs
+ // and client SDKs. Validation should be handled in the controller.
+ @ApiProperty({ type: 'string', format: 'binary' })
+ [UploadFieldName.ASSET_DATA]!: any;
+
+ @ApiProperty({ type: 'string', format: 'binary', required: false })
+ [UploadFieldName.LIVE_PHOTO_DATA]?: any;
+
+ @ApiProperty({ type: 'string', format: 'binary', required: false })
+ [UploadFieldName.SIDECAR_DATA]?: any;
+}
+
+export enum GetAssetThumbnailFormatEnum {
+ JPEG = 'JPEG',
+ WEBP = 'WEBP',
+}
+
+export class GetAssetThumbnailDto {
+ @Optional()
+ @IsEnum(GetAssetThumbnailFormatEnum)
+ @ApiProperty({
+ type: String,
+ enum: GetAssetThumbnailFormatEnum,
+ default: GetAssetThumbnailFormatEnum.WEBP,
+ required: false,
+ enumName: 'ThumbnailFormat',
+ })
+ format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP;
+}
+
+export class SearchPropertiesDto {
+ tags?: string[];
+ objects?: string[];
+ assetType?: string;
+ orientation?: string;
+ lensModel?: string;
+ make?: string;
+ model?: string;
+ city?: string;
+ state?: string;
+ country?: string;
+}
+
+export class ServeFileDto {
+ @ValidateBoolean({ optional: true })
+ @ApiProperty({ title: 'Is serve thumbnail (resize) file' })
+ isThumb?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ @ApiProperty({ title: 'Is request made from web' })
+ isWeb?: boolean;
+}
diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts
new file mode 100644
index 000000000..72f1b24c1
--- /dev/null
+++ b/server/src/dtos/asset.dto.ts
@@ -0,0 +1,132 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Type } from 'class-transformer';
+import {
+ IsDateString,
+ IsEnum,
+ IsInt,
+ IsLatitude,
+ IsLongitude,
+ IsNotEmpty,
+ IsPositive,
+ IsString,
+ ValidateIf,
+} from 'class-validator';
+import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetType } from 'src/entities/asset.entity';
+import { AssetStats } from 'src/interfaces/asset.interface';
+import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
+
+export class DeviceIdDto {
+ @IsNotEmpty()
+ @IsString()
+ deviceId!: string;
+}
+
+const hasGPS = (o: { latitude: undefined; longitude: undefined }) =>
+ o.latitude !== undefined || o.longitude !== undefined;
+const ValidateGPS = () => ValidateIf(hasGPS);
+
+export class UpdateAssetBase {
+ @ValidateBoolean({ optional: true })
+ isFavorite?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isArchived?: boolean;
+
+ @Optional()
+ @IsDateString()
+ dateTimeOriginal?: string;
+
+ @ValidateGPS()
+ @IsLatitude()
+ @IsNotEmpty()
+ latitude?: number;
+
+ @ValidateGPS()
+ @IsLongitude()
+ @IsNotEmpty()
+ longitude?: number;
+}
+
+export class AssetBulkUpdateDto extends UpdateAssetBase {
+ @ValidateUUID({ each: true })
+ ids!: string[];
+
+ @ValidateUUID({ optional: true })
+ stackParentId?: string;
+
+ @ValidateBoolean({ optional: true })
+ removeParent?: boolean;
+}
+
+export class UpdateAssetDto extends UpdateAssetBase {
+ @Optional()
+ @IsString()
+ description?: string;
+}
+
+export class RandomAssetsDto {
+ @Optional()
+ @IsInt()
+ @IsPositive()
+ @Type(() => Number)
+ count?: number;
+}
+
+export class AssetBulkDeleteDto extends BulkIdsDto {
+ @ValidateBoolean({ optional: true })
+ force?: boolean;
+}
+
+export class AssetIdsDto {
+ @ValidateUUID({ each: true })
+ assetIds!: string[];
+}
+
+export enum AssetJobName {
+ REGENERATE_THUMBNAIL = 'regenerate-thumbnail',
+ REFRESH_METADATA = 'refresh-metadata',
+ TRANSCODE_VIDEO = 'transcode-video',
+}
+
+export class AssetJobsDto extends AssetIdsDto {
+ @ApiProperty({ enumName: 'AssetJobName', enum: AssetJobName })
+ @IsEnum(AssetJobName)
+ name!: AssetJobName;
+}
+
+export class AssetStatsDto {
+ @ValidateBoolean({ optional: true })
+ isArchived?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isFavorite?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isTrashed?: boolean;
+}
+
+export class AssetStatsResponseDto {
+ @ApiProperty({ type: 'integer' })
+ images!: number;
+
+ @ApiProperty({ type: 'integer' })
+ videos!: number;
+
+ @ApiProperty({ type: 'integer' })
+ total!: number;
+}
+
+export const mapStats = (stats: AssetStats): AssetStatsResponseDto => {
+ return {
+ images: stats[AssetType.IMAGE],
+ videos: stats[AssetType.VIDEO],
+ total: Object.values(stats).reduce((total, value) => total + value, 0),
+ };
+};
+export enum UploadFieldName {
+ ASSET_DATA = 'assetData',
+ LIVE_PHOTO_DATA = 'livePhotoData',
+ SIDECAR_DATA = 'sidecarData',
+ PROFILE_DATA = 'file',
+}
diff --git a/server/src/domain/audit/audit.dto.ts b/server/src/dtos/audit.dto.ts
similarity index 100%
rename from server/src/domain/audit/audit.dto.ts
rename to server/src/dtos/audit.dto.ts
diff --git a/server/src/domain/auth/auth.dto.ts b/server/src/dtos/auth.dto.ts
similarity index 100%
rename from server/src/domain/auth/auth.dto.ts
rename to server/src/dtos/auth.dto.ts
diff --git a/server/src/domain/download/download.dto.ts b/server/src/dtos/download.dto.ts
similarity index 100%
rename from server/src/domain/download/download.dto.ts
rename to server/src/dtos/download.dto.ts
diff --git a/server/src/domain/asset/response-dto/exif-response.dto.ts b/server/src/dtos/exif.dto.ts
similarity index 100%
rename from server/src/domain/asset/response-dto/exif-response.dto.ts
rename to server/src/dtos/exif.dto.ts
diff --git a/server/src/domain/job/job.dto.ts b/server/src/dtos/job.dto.ts
similarity index 96%
rename from server/src/domain/job/job.dto.ts
rename to server/src/dtos/job.dto.ts
index fd463a9b0..1173ad8d6 100644
--- a/server/src/domain/job/job.dto.ts
+++ b/server/src/dtos/job.dto.ts
@@ -1,6 +1,6 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty } from 'class-validator';
-import { JobCommand, QueueName } from 'src/domain/job/job.constants';
+import { JobCommand, QueueName } from 'src/interfaces/job.interface';
import { ValidateBoolean } from 'src/validation';
export class JobIdParamDto {
diff --git a/server/src/domain/library/library.dto.ts b/server/src/dtos/library.dto.ts
similarity index 100%
rename from server/src/domain/library/library.dto.ts
rename to server/src/dtos/library.dto.ts
diff --git a/server/src/domain/smart-info/dto/model-config.dto.ts b/server/src/dtos/model-config.dto.ts
similarity index 98%
rename from server/src/domain/smart-info/dto/model-config.dto.ts
rename to server/src/dtos/model-config.dto.ts
index b63ec7414..d1e8bf339 100644
--- a/server/src/domain/smart-info/dto/model-config.dto.ts
+++ b/server/src/dtos/model-config.dto.ts
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validator';
-import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.repository';
+import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.interface';
import { Optional, ValidateBoolean } from 'src/validation';
export class ModelConfig {
diff --git a/server/src/domain/partner/partner.dto.ts b/server/src/dtos/partner.dto.ts
similarity index 71%
rename from server/src/domain/partner/partner.dto.ts
rename to server/src/dtos/partner.dto.ts
index c197d2079..187f8f341 100644
--- a/server/src/domain/partner/partner.dto.ts
+++ b/server/src/dtos/partner.dto.ts
@@ -1,5 +1,5 @@
import { IsNotEmpty } from 'class-validator';
-import { UserResponseDto } from 'src/domain/user/response-dto/user-response.dto';
+import { UserResponseDto } from 'src/dtos/user.dto';
export class UpdatePartnerDto {
@IsNotEmpty()
diff --git a/server/src/domain/person/person.dto.ts b/server/src/dtos/person.dto.ts
similarity index 98%
rename from server/src/domain/person/person.dto.ts
rename to server/src/dtos/person.dto.ts
index 4153ba813..b28f18603 100644
--- a/server/src/domain/person/person.dto.ts
+++ b/server/src/dtos/person.dto.ts
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsArray, IsNotEmpty, IsString, MaxDate, ValidateNested } from 'class-validator';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
diff --git a/server/src/domain/search/dto/search.dto.ts b/server/src/dtos/search.dto.ts
similarity index 66%
rename from server/src/domain/search/dto/search.dto.ts
rename to server/src/dtos/search.dto.ts
index 4f77517b3..799baddee 100644
--- a/server/src/domain/search/dto/search.dto.ts
+++ b/server/src/dtos/search.dto.ts
@@ -1,6 +1,8 @@
import { ApiProperty } from '@nestjs/swagger';
import { Type } from 'class-transformer';
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
+import { AlbumResponseDto } from 'src/dtos/album.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { AssetOrder } from 'src/entities/album.entity';
import { AssetType } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
@@ -264,3 +266,130 @@ export function mapPlaces(place: GeodataPlacesEntity): PlacesResponseDto {
admin2name: place.admin2Name,
};
}
+export enum SearchSuggestionType {
+ COUNTRY = 'country',
+ STATE = 'state',
+ CITY = 'city',
+ CAMERA_MAKE = 'camera-make',
+ CAMERA_MODEL = 'camera-model',
+}
+
+export class SearchSuggestionRequestDto {
+ @IsEnum(SearchSuggestionType)
+ @IsNotEmpty()
+ @ApiProperty({ enumName: 'SearchSuggestionType', enum: SearchSuggestionType })
+ type!: SearchSuggestionType;
+
+ @IsString()
+ @Optional()
+ country?: string;
+
+ @IsString()
+ @Optional()
+ state?: string;
+
+ @IsString()
+ @Optional()
+ make?: string;
+
+ @IsString()
+ @Optional()
+ model?: string;
+}
+
+class SearchFacetCountResponseDto {
+ @ApiProperty({ type: 'integer' })
+ count!: number;
+ value!: string;
+}
+
+class SearchFacetResponseDto {
+ fieldName!: string;
+ counts!: SearchFacetCountResponseDto[];
+}
+
+class SearchAlbumResponseDto {
+ @ApiProperty({ type: 'integer' })
+ total!: number;
+ @ApiProperty({ type: 'integer' })
+ count!: number;
+ items!: AlbumResponseDto[];
+ facets!: SearchFacetResponseDto[];
+}
+
+class SearchAssetResponseDto {
+ @ApiProperty({ type: 'integer' })
+ total!: number;
+ @ApiProperty({ type: 'integer' })
+ count!: number;
+ items!: AssetResponseDto[];
+ facets!: SearchFacetResponseDto[];
+ nextPage!: string | null;
+}
+
+export class SearchResponseDto {
+ albums!: SearchAlbumResponseDto;
+ assets!: SearchAssetResponseDto;
+}
+
+class SearchExploreItem {
+ value!: string;
+ data!: AssetResponseDto;
+}
+
+export class SearchExploreResponseDto {
+ fieldName!: string;
+ items!: SearchExploreItem[];
+}
+
+export class MapMarkerDto {
+ @ValidateBoolean({ optional: true })
+ isArchived?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ isFavorite?: boolean;
+
+ @ValidateDate({ optional: true })
+ fileCreatedAfter?: Date;
+
+ @ValidateDate({ optional: true })
+ fileCreatedBefore?: Date;
+
+ @ValidateBoolean({ optional: true })
+ withPartners?: boolean;
+}
+
+export class MemoryLaneDto {
+ @IsInt()
+ @Type(() => Number)
+ @Max(31)
+ @Min(1)
+ @ApiProperty({ type: 'integer' })
+ day!: number;
+
+ @IsInt()
+ @Type(() => Number)
+ @Max(12)
+ @Min(1)
+ @ApiProperty({ type: 'integer' })
+ month!: number;
+}
+export class MapMarkerResponseDto {
+ @ApiProperty()
+ id!: string;
+
+ @ApiProperty({ format: 'double' })
+ lat!: number;
+
+ @ApiProperty({ format: 'double' })
+ lon!: number;
+
+ @ApiProperty()
+ city!: string | null;
+
+ @ApiProperty()
+ state!: string | null;
+
+ @ApiProperty()
+ country!: string | null;
+}
diff --git a/server/src/domain/server-info/server-info.dto.ts b/server/src/dtos/server-info.dto.ts
similarity index 94%
rename from server/src/domain/server-info/server-info.dto.ts
rename to server/src/dtos/server-info.dto.ts
index 0cbfbe773..497b7ab5e 100644
--- a/server/src/domain/server-info/server-info.dto.ts
+++ b/server/src/dtos/server-info.dto.ts
@@ -1,8 +1,8 @@
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
import type { DateTime } from 'luxon';
import { FeatureFlags } from 'src/cores/system-config.core';
-import { IVersion, VersionType } from 'src/domain/domain.constant';
-import { SystemConfigThemeDto } from 'src/domain/system-config/dto/system-config-theme.dto';
+import { SystemConfigThemeDto } from 'src/dtos/system-config.dto';
+import { IVersion, VersionType } from 'src/utils/version';
export class ServerPingResponse {
@ApiResponseProperty({ type: String, example: 'pong' })
diff --git a/server/src/domain/shared-link/shared-link-response.dto.ts b/server/src/dtos/shared-link.dto.ts
similarity index 57%
rename from server/src/domain/shared-link/shared-link-response.dto.ts
rename to server/src/dtos/shared-link.dto.ts
index 44024506f..9a90901d2 100644
--- a/server/src/domain/shared-link/shared-link-response.dto.ts
+++ b/server/src/dtos/shared-link.dto.ts
@@ -1,9 +1,81 @@
import { ApiProperty } from '@nestjs/swagger';
+import { IsEnum, IsString } from 'class-validator';
import _ from 'lodash';
-import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/domain/album/album-response.dto';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
+import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity';
+import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
+export class SharedLinkCreateDto {
+ @IsEnum(SharedLinkType)
+ @ApiProperty({ enum: SharedLinkType, enumName: 'SharedLinkType' })
+ type!: SharedLinkType;
+
+ @ValidateUUID({ each: true, optional: true })
+ assetIds?: string[];
+
+ @ValidateUUID({ optional: true })
+ albumId?: string;
+
+ @IsString()
+ @Optional()
+ description?: string;
+
+ @IsString()
+ @Optional()
+ password?: string;
+
+ @ValidateDate({ optional: true, nullable: true })
+ expiresAt?: Date | null = null;
+
+ @ValidateBoolean({ optional: true })
+ allowUpload?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ allowDownload?: boolean = true;
+
+ @ValidateBoolean({ optional: true })
+ showMetadata?: boolean = true;
+}
+
+export class SharedLinkEditDto {
+ @Optional()
+ description?: string;
+
+ @Optional()
+ password?: string;
+
+ @Optional({ nullable: true })
+ expiresAt?: Date | null;
+
+ @Optional()
+ allowUpload?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ allowDownload?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ showMetadata?: boolean;
+
+ /**
+ * Few clients cannot send null to set the expiryTime to never.
+ * Setting this flag and not sending expiryAt is considered as null instead.
+ * Clients that can send null values can ignore this.
+ */
+ @ValidateBoolean({ optional: true })
+ changeExpiryTime?: boolean;
+}
+
+export class SharedLinkPasswordDto {
+ @IsString()
+ @Optional()
+ @ApiProperty({ example: 'password' })
+ password?: string;
+
+ @IsString()
+ @Optional()
+ token?: string;
+}
export class SharedLinkResponseDto {
id!: string;
description!: string | null;
diff --git a/server/src/domain/asset/dto/asset-stack.dto.ts b/server/src/dtos/stack.dto.ts
similarity index 100%
rename from server/src/domain/asset/dto/asset-stack.dto.ts
rename to server/src/dtos/stack.dto.ts
diff --git a/server/src/dtos/system-config.dto.ts b/server/src/dtos/system-config.dto.ts
new file mode 100644
index 000000000..740f1672e
--- /dev/null
+++ b/server/src/dtos/system-config.dto.ts
@@ -0,0 +1,516 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Type } from 'class-transformer';
+import {
+ IsEnum,
+ IsInt,
+ IsNotEmpty,
+ IsNumber,
+ IsObject,
+ IsPositive,
+ IsString,
+ IsUrl,
+ Max,
+ Min,
+ Validate,
+ ValidateIf,
+ ValidateNested,
+ ValidatorConstraint,
+ ValidatorConstraintInterface,
+} from 'class-validator';
+import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
+import {
+ AudioCodec,
+ CQMode,
+ Colorspace,
+ LogLevel,
+ SystemConfig,
+ ToneMapping,
+ TranscodeHWAccel,
+ TranscodePolicy,
+ VideoCodec,
+} from 'src/entities/system-config.entity';
+import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
+import { ValidateBoolean, validateCronExpression } from 'src/validation';
+
+@ValidatorConstraint({ name: 'cronValidator' })
+class CronValidator implements ValidatorConstraintInterface {
+ validate(expression: string): boolean {
+ return validateCronExpression(expression);
+ }
+}
+
+const isLibraryScanEnabled = (config: SystemConfigLibraryScanDto) => config.enabled;
+const isOAuthEnabled = (config: SystemConfigOAuthDto) => config.enabled;
+const isOAuthOverrideEnabled = (config: SystemConfigOAuthDto) => config.mobileOverrideEnabled;
+
+export class SystemConfigFFmpegDto {
+ @IsInt()
+ @Min(0)
+ @Max(51)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ crf!: number;
+
+ @IsInt()
+ @Min(0)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ threads!: number;
+
+ @IsString()
+ preset!: string;
+
+ @IsEnum(VideoCodec)
+ @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec })
+ targetVideoCodec!: VideoCodec;
+
+ @IsEnum(VideoCodec, { each: true })
+ @ApiProperty({ enumName: 'VideoCodec', enum: VideoCodec, isArray: true })
+ acceptedVideoCodecs!: VideoCodec[];
+
+ @IsEnum(AudioCodec)
+ @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec })
+ targetAudioCodec!: AudioCodec;
+
+ @IsEnum(AudioCodec, { each: true })
+ @ApiProperty({ enumName: 'AudioCodec', enum: AudioCodec, isArray: true })
+ acceptedAudioCodecs!: AudioCodec[];
+
+ @IsString()
+ targetResolution!: string;
+
+ @IsString()
+ maxBitrate!: string;
+
+ @IsInt()
+ @Min(-1)
+ @Max(16)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ bframes!: number;
+
+ @IsInt()
+ @Min(0)
+ @Max(6)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ refs!: number;
+
+ @IsInt()
+ @Min(0)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ gopSize!: number;
+
+ @IsInt()
+ @Min(0)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ npl!: number;
+
+ @ValidateBoolean()
+ temporalAQ!: boolean;
+
+ @IsEnum(CQMode)
+ @ApiProperty({ enumName: 'CQMode', enum: CQMode })
+ cqMode!: CQMode;
+
+ @ValidateBoolean()
+ twoPass!: boolean;
+
+ @IsString()
+ preferredHwDevice!: string;
+
+ @IsEnum(TranscodePolicy)
+ @ApiProperty({ enumName: 'TranscodePolicy', enum: TranscodePolicy })
+ transcode!: TranscodePolicy;
+
+ @IsEnum(TranscodeHWAccel)
+ @ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
+ accel!: TranscodeHWAccel;
+
+ @IsEnum(ToneMapping)
+ @ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
+ tonemap!: ToneMapping;
+}
+
+class JobSettingsDto {
+ @IsInt()
+ @IsPositive()
+ @ApiProperty({ type: 'integer' })
+ concurrency!: number;
+}
+
+class SystemConfigJobDto implements Record {
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.THUMBNAIL_GENERATION]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.METADATA_EXTRACTION]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.VIDEO_CONVERSION]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.SMART_SEARCH]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.MIGRATION]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.BACKGROUND_TASK]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.SEARCH]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.FACE_DETECTION]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.SIDECAR]!: JobSettingsDto;
+
+ @ApiProperty({ type: JobSettingsDto })
+ @ValidateNested()
+ @IsObject()
+ @Type(() => JobSettingsDto)
+ [QueueName.LIBRARY]!: JobSettingsDto;
+}
+
+class SystemConfigLibraryScanDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @ValidateIf(isLibraryScanEnabled)
+ @IsNotEmpty()
+ @Validate(CronValidator, { message: 'Invalid cron expression' })
+ @IsString()
+ cronExpression!: string;
+}
+
+class SystemConfigLibraryWatchDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+}
+
+class SystemConfigLibraryDto {
+ @Type(() => SystemConfigLibraryScanDto)
+ @ValidateNested()
+ @IsObject()
+ scan!: SystemConfigLibraryScanDto;
+
+ @Type(() => SystemConfigLibraryWatchDto)
+ @ValidateNested()
+ @IsObject()
+ watch!: SystemConfigLibraryWatchDto;
+}
+
+class SystemConfigLoggingDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @ApiProperty({ enum: LogLevel, enumName: 'LogLevel' })
+ @IsEnum(LogLevel)
+ level!: LogLevel;
+}
+
+class SystemConfigMachineLearningDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @IsUrl({ require_tld: false, allow_underscores: true })
+ @ValidateIf((dto) => dto.enabled)
+ url!: string;
+
+ @Type(() => CLIPConfig)
+ @ValidateNested()
+ @IsObject()
+ clip!: CLIPConfig;
+
+ @Type(() => RecognitionConfig)
+ @ValidateNested()
+ @IsObject()
+ facialRecognition!: RecognitionConfig;
+}
+
+enum MapTheme {
+ LIGHT = 'light',
+ DARK = 'dark',
+}
+
+export class MapThemeDto {
+ @IsEnum(MapTheme)
+ @ApiProperty({ enum: MapTheme, enumName: 'MapTheme' })
+ theme!: MapTheme;
+}
+
+class SystemConfigMapDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @IsString()
+ lightStyle!: string;
+
+ @IsString()
+ darkStyle!: string;
+}
+
+class SystemConfigNewVersionCheckDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+}
+
+class SystemConfigOAuthDto {
+ @ValidateBoolean()
+ autoLaunch!: boolean;
+
+ @ValidateBoolean()
+ autoRegister!: boolean;
+
+ @IsString()
+ buttonText!: string;
+
+ @ValidateIf(isOAuthEnabled)
+ @IsNotEmpty()
+ @IsString()
+ clientId!: string;
+
+ @ValidateIf(isOAuthEnabled)
+ @IsNotEmpty()
+ @IsString()
+ clientSecret!: string;
+
+ @IsNumber()
+ @Min(0)
+ defaultStorageQuota!: number;
+
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @ValidateIf(isOAuthEnabled)
+ @IsNotEmpty()
+ @IsString()
+ issuerUrl!: string;
+
+ @ValidateBoolean()
+ mobileOverrideEnabled!: boolean;
+
+ @ValidateIf(isOAuthOverrideEnabled)
+ @IsUrl()
+ mobileRedirectUri!: string;
+
+ @IsString()
+ scope!: string;
+
+ @IsString()
+ @IsNotEmpty()
+ signingAlgorithm!: string;
+
+ @IsString()
+ storageLabelClaim!: string;
+
+ @IsString()
+ storageQuotaClaim!: string;
+}
+
+class SystemConfigPasswordLoginDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+}
+
+class SystemConfigReverseGeocodingDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+}
+
+class SystemConfigServerDto {
+ @IsString()
+ externalDomain!: string;
+
+ @IsString()
+ loginPageMessage!: string;
+}
+
+class SystemConfigStorageTemplateDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @ValidateBoolean()
+ hashVerificationEnabled!: boolean;
+
+ @IsNotEmpty()
+ @IsString()
+ template!: string;
+}
+
+export class SystemConfigTemplateStorageOptionDto {
+ yearOptions!: string[];
+ monthOptions!: string[];
+ weekOptions!: string[];
+ dayOptions!: string[];
+ hourOptions!: string[];
+ minuteOptions!: string[];
+ secondOptions!: string[];
+ presetOptions!: string[];
+}
+
+export class SystemConfigThemeDto {
+ @IsString()
+ customCss!: string;
+}
+
+class SystemConfigThumbnailDto {
+ @IsInt()
+ @Min(1)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ webpSize!: number;
+
+ @IsInt()
+ @Min(1)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ jpegSize!: number;
+
+ @IsInt()
+ @Min(1)
+ @Max(100)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ quality!: number;
+
+ @IsEnum(Colorspace)
+ @ApiProperty({ enumName: 'Colorspace', enum: Colorspace })
+ colorspace!: Colorspace;
+}
+
+class SystemConfigTrashDto {
+ @ValidateBoolean()
+ enabled!: boolean;
+
+ @IsInt()
+ @Min(0)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ days!: number;
+}
+
+class SystemConfigUserDto {
+ @IsInt()
+ @Min(1)
+ @Type(() => Number)
+ @ApiProperty({ type: 'integer' })
+ deleteDelay!: number;
+}
+
+export class SystemConfigDto implements SystemConfig {
+ @Type(() => SystemConfigFFmpegDto)
+ @ValidateNested()
+ @IsObject()
+ ffmpeg!: SystemConfigFFmpegDto;
+
+ @Type(() => SystemConfigLoggingDto)
+ @ValidateNested()
+ @IsObject()
+ logging!: SystemConfigLoggingDto;
+
+ @Type(() => SystemConfigMachineLearningDto)
+ @ValidateNested()
+ @IsObject()
+ machineLearning!: SystemConfigMachineLearningDto;
+
+ @Type(() => SystemConfigMapDto)
+ @ValidateNested()
+ @IsObject()
+ map!: SystemConfigMapDto;
+
+ @Type(() => SystemConfigNewVersionCheckDto)
+ @ValidateNested()
+ @IsObject()
+ newVersionCheck!: SystemConfigNewVersionCheckDto;
+
+ @Type(() => SystemConfigOAuthDto)
+ @ValidateNested()
+ @IsObject()
+ oauth!: SystemConfigOAuthDto;
+
+ @Type(() => SystemConfigPasswordLoginDto)
+ @ValidateNested()
+ @IsObject()
+ passwordLogin!: SystemConfigPasswordLoginDto;
+
+ @Type(() => SystemConfigReverseGeocodingDto)
+ @ValidateNested()
+ @IsObject()
+ reverseGeocoding!: SystemConfigReverseGeocodingDto;
+
+ @Type(() => SystemConfigStorageTemplateDto)
+ @ValidateNested()
+ @IsObject()
+ storageTemplate!: SystemConfigStorageTemplateDto;
+
+ @Type(() => SystemConfigJobDto)
+ @ValidateNested()
+ @IsObject()
+ job!: SystemConfigJobDto;
+
+ @Type(() => SystemConfigThumbnailDto)
+ @ValidateNested()
+ @IsObject()
+ thumbnail!: SystemConfigThumbnailDto;
+
+ @Type(() => SystemConfigTrashDto)
+ @ValidateNested()
+ @IsObject()
+ trash!: SystemConfigTrashDto;
+
+ @Type(() => SystemConfigThemeDto)
+ @ValidateNested()
+ @IsObject()
+ theme!: SystemConfigThemeDto;
+
+ @Type(() => SystemConfigLibraryDto)
+ @ValidateNested()
+ @IsObject()
+ library!: SystemConfigLibraryDto;
+
+ @Type(() => SystemConfigServerDto)
+ @ValidateNested()
+ @IsObject()
+ server!: SystemConfigServerDto;
+
+ @Type(() => SystemConfigUserDto)
+ @ValidateNested()
+ @IsObject()
+ user!: SystemConfigUserDto;
+}
+
+export function mapConfig(config: SystemConfig): SystemConfigDto {
+ return config;
+}
diff --git a/server/src/domain/tag/tag-response.dto.ts b/server/src/dtos/tag.dto.ts
similarity index 54%
rename from server/src/domain/tag/tag-response.dto.ts
rename to server/src/dtos/tag.dto.ts
index 535efcf43..1094d70df 100644
--- a/server/src/domain/tag/tag-response.dto.ts
+++ b/server/src/dtos/tag.dto.ts
@@ -1,5 +1,24 @@
import { ApiProperty } from '@nestjs/swagger';
+import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { TagEntity, TagType } from 'src/entities/tag.entity';
+import { Optional } from 'src/validation';
+
+export class CreateTagDto {
+ @IsString()
+ @IsNotEmpty()
+ name!: string;
+
+ @IsEnum(TagType)
+ @IsNotEmpty()
+ @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType })
+ type!: TagType;
+}
+
+export class UpdateTagDto {
+ @IsString()
+ @Optional()
+ name?: string;
+}
export class TagResponseDto {
id!: string;
diff --git a/server/src/domain/asset/dto/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts
similarity index 83%
rename from server/src/domain/asset/dto/time-bucket.dto.ts
rename to server/src/dtos/time-bucket.dto.ts
index a5ff023d1..a55126013 100644
--- a/server/src/domain/asset/dto/time-bucket.dto.ts
+++ b/server/src/dtos/time-bucket.dto.ts
@@ -1,7 +1,7 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
import { AssetOrder } from 'src/entities/album.entity';
-import { TimeBucketSize } from 'src/interfaces/asset.repository';
+import { TimeBucketSize } from 'src/interfaces/asset.interface';
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
export class TimeBucketDto {
@@ -44,3 +44,11 @@ export class TimeBucketAssetDto extends TimeBucketDto {
@IsString()
timeBucket!: string;
}
+
+export class TimeBucketResponseDto {
+ @ApiProperty({ type: 'string' })
+ timeBucket!: string;
+
+ @ApiProperty({ type: 'integer' })
+ count!: number;
+}
diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts
new file mode 100644
index 000000000..2f3d8cf22
--- /dev/null
+++ b/server/src/dtos/user-profile.dto.ts
@@ -0,0 +1,28 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { UploadFieldName } from 'src/dtos/asset.dto';
+import { UserAvatarColor, UserEntity } from 'src/entities/user.entity';
+
+export class CreateProfileImageDto {
+ @ApiProperty({ type: 'string', format: 'binary' })
+ [UploadFieldName.PROFILE_DATA]!: Express.Multer.File;
+}
+
+export class CreateProfileImageResponseDto {
+ userId!: string;
+ profileImagePath!: string;
+}
+
+export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto {
+ return {
+ userId: userId,
+ profileImagePath: profileImagePath,
+ };
+}
+
+export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
+ const values = Object.values(UserAvatarColor);
+ const randomIndex = Math.floor(
+ [...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
+ );
+ return values[randomIndex] as UserAvatarColor;
+};
diff --git a/server/src/domain/user/dto/create-user.dto.spec.ts b/server/src/dtos/user.dto.spec.ts
similarity index 79%
rename from server/src/domain/user/dto/create-user.dto.spec.ts
rename to server/src/dtos/user.dto.spec.ts
index 28abc44ad..d07399f0e 100644
--- a/server/src/domain/user/dto/create-user.dto.spec.ts
+++ b/server/src/dtos/user.dto.spec.ts
@@ -1,6 +1,20 @@
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
-import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto } from 'src/domain/user/dto/create-user.dto';
+import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
+
+describe('update user DTO', () => {
+ it('should allow emails without a tld', async () => {
+ const someEmail = 'test@test';
+
+ const dto = plainToInstance(UpdateUserDto, {
+ email: someEmail,
+ id: '3fe388e4-2078-44d7-b36c-39d9dee3a657',
+ });
+ const errors = await validate(dto);
+ expect(errors).toHaveLength(0);
+ expect(dto.email).toEqual(someEmail);
+ });
+});
describe('create user DTO', () => {
it('validates the email', async () => {
diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts
new file mode 100644
index 000000000..309006822
--- /dev/null
+++ b/server/src/dtos/user.dto.ts
@@ -0,0 +1,169 @@
+import { ApiProperty } from '@nestjs/swagger';
+import { Transform } from 'class-transformer';
+import { IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
+import { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
+import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
+import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
+
+export class CreateUserDto {
+ @IsEmail({ require_tld: false })
+ @Transform(toEmail)
+ email!: string;
+
+ @IsNotEmpty()
+ @IsString()
+ password!: string;
+
+ @IsNotEmpty()
+ @IsString()
+ name!: string;
+
+ @Optional({ nullable: true })
+ @IsString()
+ @Transform(toSanitized)
+ storageLabel?: string | null;
+
+ @ValidateBoolean({ optional: true })
+ memoriesEnabled?: boolean;
+
+ @Optional({ nullable: true })
+ @IsNumber()
+ @IsPositive()
+ @ApiProperty({ type: 'integer', format: 'int64' })
+ quotaSizeInBytes?: number | null;
+
+ @ValidateBoolean({ optional: true })
+ shouldChangePassword?: boolean;
+}
+
+export class CreateAdminDto {
+ @IsNotEmpty()
+ isAdmin!: true;
+
+ @IsEmail({ require_tld: false })
+ @Transform(({ value }) => value?.toLowerCase())
+ email!: string;
+
+ @IsNotEmpty()
+ password!: string;
+
+ @IsNotEmpty()
+ name!: string;
+}
+
+export class CreateUserOAuthDto {
+ @IsEmail({ require_tld: false })
+ @Transform(({ value }) => value?.toLowerCase())
+ email!: string;
+
+ @IsNotEmpty()
+ oauthId!: string;
+
+ name?: string;
+}
+
+export class DeleteUserDto {
+ @ValidateBoolean({ optional: true })
+ force?: boolean;
+}
+
+export class UpdateUserDto {
+ @Optional()
+ @IsEmail({ require_tld: false })
+ @Transform(toEmail)
+ email?: string;
+
+ @Optional()
+ @IsNotEmpty()
+ @IsString()
+ password?: string;
+
+ @Optional()
+ @IsString()
+ @IsNotEmpty()
+ name?: string;
+
+ @Optional()
+ @IsString()
+ @Transform(toSanitized)
+ storageLabel?: string;
+
+ @IsNotEmpty()
+ @IsUUID('4')
+ @ApiProperty({ format: 'uuid' })
+ id!: string;
+
+ @ValidateBoolean({ optional: true })
+ isAdmin?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ shouldChangePassword?: boolean;
+
+ @ValidateBoolean({ optional: true })
+ memoriesEnabled?: boolean;
+
+ @Optional()
+ @IsEnum(UserAvatarColor)
+ @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
+ avatarColor?: UserAvatarColor;
+
+ @Optional({ nullable: true })
+ @IsNumber()
+ @IsPositive()
+ @ApiProperty({ type: 'integer', format: 'int64' })
+ quotaSizeInBytes?: number | null;
+}
+
+export class UserDto {
+ id!: string;
+ name!: string;
+ email!: string;
+ profileImagePath!: string;
+ @IsEnum(UserAvatarColor)
+ @ApiProperty({ enumName: 'UserAvatarColor', enum: UserAvatarColor })
+ avatarColor!: UserAvatarColor;
+}
+
+export class UserResponseDto extends UserDto {
+ storageLabel!: string | null;
+ shouldChangePassword!: boolean;
+ isAdmin!: boolean;
+ createdAt!: Date;
+ deletedAt!: Date | null;
+ updatedAt!: Date;
+ oauthId!: string;
+ memoriesEnabled?: boolean;
+ @ApiProperty({ type: 'integer', format: 'int64' })
+ quotaSizeInBytes!: number | null;
+ @ApiProperty({ type: 'integer', format: 'int64' })
+ quotaUsageInBytes!: number | null;
+ @ApiProperty({ enumName: 'UserStatus', enum: UserStatus })
+ status!: string;
+}
+
+export const mapSimpleUser = (entity: UserEntity): UserDto => {
+ return {
+ id: entity.id,
+ email: entity.email,
+ name: entity.name,
+ profileImagePath: entity.profileImagePath,
+ avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
+ };
+};
+
+export function mapUser(entity: UserEntity): UserResponseDto {
+ return {
+ ...mapSimpleUser(entity),
+ storageLabel: entity.storageLabel,
+ shouldChangePassword: entity.shouldChangePassword,
+ isAdmin: entity.isAdmin,
+ createdAt: entity.createdAt,
+ deletedAt: entity.deletedAt,
+ updatedAt: entity.updatedAt,
+ oauthId: entity.oauthId,
+ memoriesEnabled: entity.memoriesEnabled,
+ quotaSizeInBytes: entity.quotaSizeInBytes,
+ quotaUsageInBytes: entity.quotaUsageInBytes,
+ status: entity.status,
+ };
+}
diff --git a/server/src/entities/system-config.entity.ts b/server/src/entities/system-config.entity.ts
index cf7474417..98b882a36 100644
--- a/server/src/entities/system-config.entity.ts
+++ b/server/src/entities/system-config.entity.ts
@@ -1,4 +1,4 @@
-import { ConcurrentQueueName } from 'src/domain/job/job.constants';
+import { ConcurrentQueueName } from 'src/interfaces/job.interface';
import { Column, Entity, PrimaryColumn } from 'typeorm';
@Entity('system_config')
diff --git a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts b/server/src/immich/api-v1/asset/dto/asset-check.dto.ts
deleted file mode 100644
index d3474171f..000000000
--- a/server/src/immich/api-v1/asset/dto/asset-check.dto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Type } from 'class-transformer';
-import { IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
-
-export class AssetBulkUploadCheckItem {
- @IsString()
- @IsNotEmpty()
- id!: string;
-
- /** base64 or hex encoded sha1 hash */
- @IsString()
- @IsNotEmpty()
- checksum!: string;
-}
-
-export class AssetBulkUploadCheckDto {
- @IsArray()
- @ValidateNested({ each: true })
- @Type(() => AssetBulkUploadCheckItem)
- assets!: AssetBulkUploadCheckItem[];
-}
diff --git a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts b/server/src/immich/api-v1/asset/dto/asset-search.dto.ts
deleted file mode 100644
index 97d0aa1fa..000000000
--- a/server/src/immich/api-v1/asset/dto/asset-search.dto.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { Type } from 'class-transformer';
-import { IsInt, IsUUID } from 'class-validator';
-import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
-
-export class AssetSearchDto {
- @ValidateBoolean({ optional: true })
- isFavorite?: boolean;
-
- @ValidateBoolean({ optional: true })
- isArchived?: boolean;
-
- @Optional()
- @IsInt()
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- skip?: number;
-
- @Optional()
- @IsInt()
- @Type(() => Number)
- @ApiProperty({ type: 'integer' })
- take?: number;
-
- @Optional()
- @IsUUID('4')
- @ApiProperty({ format: 'uuid' })
- userId?: string;
-
- @ValidateDate({ optional: true })
- updatedAfter?: Date;
-
- @ValidateDate({ optional: true })
- updatedBefore?: Date;
-}
diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts
deleted file mode 100644
index a634ba42e..000000000
--- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.spec.ts
+++ /dev/null
@@ -1,28 +0,0 @@
-import { plainToInstance } from 'class-transformer';
-import { validateSync } from 'class-validator';
-import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
-
-describe('CheckExistingAssetsDto', () => {
- it('should fail with an empty list', () => {
- const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [], deviceId: 'test-device' });
- const errors = validateSync(dto);
- expect(errors).toHaveLength(1);
- expect(errors[0].property).toEqual('deviceAssetIds');
- });
-
- it('should fail with an empty string', () => {
- const dto = plainToInstance(CheckExistingAssetsDto, { deviceAssetIds: [''], deviceId: 'test-device' });
- const errors = validateSync(dto);
- expect(errors).toHaveLength(1);
- expect(errors[0].property).toEqual('deviceAssetIds');
- });
-
- it('should work with valid asset ids', () => {
- const dto = plainToInstance(CheckExistingAssetsDto, {
- deviceAssetIds: ['asset-1', 'asset-2'],
- deviceId: 'test-device',
- });
- const errors = validateSync(dto);
- expect(errors).toHaveLength(0);
- });
-});
diff --git a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts b/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts
deleted file mode 100644
index 65740ab89..000000000
--- a/server/src/immich/api-v1/asset/dto/check-existing-assets.dto.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { ArrayNotEmpty, IsNotEmpty, IsString } from 'class-validator';
-
-export class CheckExistingAssetsDto {
- @ArrayNotEmpty()
- @IsString({ each: true })
- @IsNotEmpty({ each: true })
- deviceAssetIds!: string[];
-
- @IsNotEmpty()
- deviceId!: string;
-}
diff --git a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts b/server/src/immich/api-v1/asset/dto/create-asset.dto.ts
deleted file mode 100644
index d16a9c05c..000000000
--- a/server/src/immich/api-v1/asset/dto/create-asset.dto.ts
+++ /dev/null
@@ -1,53 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsNotEmpty, IsString } from 'class-validator';
-import { UploadFieldName } from 'src/domain/asset/asset.service';
-import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
-
-export class CreateAssetDto {
- @ValidateUUID({ optional: true })
- libraryId?: string;
-
- @IsNotEmpty()
- @IsString()
- deviceAssetId!: string;
-
- @IsNotEmpty()
- @IsString()
- deviceId!: string;
-
- @ValidateDate()
- fileCreatedAt!: Date;
-
- @ValidateDate()
- fileModifiedAt!: Date;
-
- @Optional()
- @IsString()
- duration?: string;
-
- @ValidateBoolean({ optional: true })
- isFavorite?: boolean;
-
- @ValidateBoolean({ optional: true })
- isArchived?: boolean;
-
- @ValidateBoolean({ optional: true })
- isVisible?: boolean;
-
- @ValidateBoolean({ optional: true })
- isOffline?: boolean;
-
- @ValidateBoolean({ optional: true })
- isReadOnly?: boolean;
-
- // The properties below are added to correctly generate the API docs
- // and client SDKs. Validation should be handled in the controller.
- @ApiProperty({ type: 'string', format: 'binary' })
- [UploadFieldName.ASSET_DATA]!: any;
-
- @ApiProperty({ type: 'string', format: 'binary', required: false })
- [UploadFieldName.LIVE_PHOTO_DATA]?: any;
-
- @ApiProperty({ type: 'string', format: 'binary', required: false })
- [UploadFieldName.SIDECAR_DATA]?: any;
-}
diff --git a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts b/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts
deleted file mode 100644
index 6c709eb02..000000000
--- a/server/src/immich/api-v1/asset/dto/get-asset-thumbnail.dto.ts
+++ /dev/null
@@ -1,21 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { IsEnum } from 'class-validator';
-import { Optional } from 'src/validation';
-
-export enum GetAssetThumbnailFormatEnum {
- JPEG = 'JPEG',
- WEBP = 'WEBP',
-}
-
-export class GetAssetThumbnailDto {
- @Optional()
- @IsEnum(GetAssetThumbnailFormatEnum)
- @ApiProperty({
- type: String,
- enum: GetAssetThumbnailFormatEnum,
- default: GetAssetThumbnailFormatEnum.WEBP,
- required: false,
- enumName: 'ThumbnailFormat',
- })
- format: GetAssetThumbnailFormatEnum = GetAssetThumbnailFormatEnum.WEBP;
-}
diff --git a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts b/server/src/immich/api-v1/asset/dto/search-properties.dto.ts
deleted file mode 100644
index 669b29b2e..000000000
--- a/server/src/immich/api-v1/asset/dto/search-properties.dto.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-export class SearchPropertiesDto {
- tags?: string[];
- objects?: string[];
- assetType?: string;
- orientation?: string;
- lensModel?: string;
- make?: string;
- model?: string;
- city?: string;
- state?: string;
- country?: string;
-}
diff --git a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts b/server/src/immich/api-v1/asset/dto/serve-file.dto.ts
deleted file mode 100644
index 8b3147fc2..000000000
--- a/server/src/immich/api-v1/asset/dto/serve-file.dto.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { ApiProperty } from '@nestjs/swagger';
-import { ValidateBoolean } from 'src/validation';
-
-export class ServeFileDto {
- @ValidateBoolean({ optional: true })
- @ApiProperty({ title: 'Is serve thumbnail (resize) file' })
- isThumb?: boolean;
-
- @ValidateBoolean({ optional: true })
- @ApiProperty({ title: 'Is request made from web' })
- isWeb?: boolean;
-}
diff --git a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts
deleted file mode 100644
index 1a51dc53f..000000000
--- a/server/src/immich/api-v1/asset/response-dto/asset-check-response.dto.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-export class AssetBulkUploadCheckResult {
- id!: string;
- action!: AssetUploadAction;
- reason?: AssetRejectReason;
- assetId?: string;
-}
-
-export class AssetBulkUploadCheckResponseDto {
- results!: AssetBulkUploadCheckResult[];
-}
-
-export enum AssetUploadAction {
- ACCEPT = 'accept',
- REJECT = 'reject',
-}
-
-export enum AssetRejectReason {
- DUPLICATE = 'duplicate',
- UNSUPPORTED_FORMAT = 'unsupported-format',
-}
diff --git a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts
deleted file mode 100644
index f628b708d..000000000
--- a/server/src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export class AssetFileUploadResponseDto {
- id!: string;
- duplicate!: boolean;
-}
diff --git a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts
deleted file mode 100644
index c39a79606..000000000
--- a/server/src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export class CheckExistingAssetsResponseDto {
- existingIds!: string[];
-}
diff --git a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts
deleted file mode 100644
index 63b1b0969..000000000
--- a/server/src/immich/api-v1/asset/response-dto/curated-locations-response.dto.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export class CuratedLocationsResponseDto {
- id!: string;
- city!: string;
- resizePath!: string;
- deviceAssetId!: string;
- deviceId!: string;
-}
diff --git a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts b/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts
deleted file mode 100644
index 0d23b3eb7..000000000
--- a/server/src/immich/api-v1/asset/response-dto/curated-objects-response.dto.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export class CuratedObjectsResponseDto {
- id!: string;
- object!: string;
- resizePath!: string;
- deviceAssetId!: string;
- deviceId!: string;
-}
diff --git a/server/src/infra/infra.module.ts b/server/src/infra/infra.module.ts
deleted file mode 100644
index b0387ba96..000000000
--- a/server/src/infra/infra.module.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import { BullModule } from '@nestjs/bullmq';
-import { Global, Module, Provider } from '@nestjs/common';
-import { ConfigModule } from '@nestjs/config';
-import { EventEmitterModule } from '@nestjs/event-emitter';
-import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule';
-import { TypeOrmModule } from '@nestjs/typeorm';
-import { OpenTelemetryModule } from 'nestjs-otel';
-import { bullConfig, bullQueues, immichAppConfig } from 'src/config';
-import { databaseEntities } from 'src/entities';
-import { databaseConfig } from 'src/infra/database.config';
-import { otelConfig } from 'src/infra/instrumentation';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IActivityRepository } from 'src/interfaces/activity.repository';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
-import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IAuditRepository } from 'src/interfaces/audit.repository';
-import { ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { IMetadataRepository } from 'src/interfaces/metadata.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { IServerInfoRepository } from 'src/interfaces/server-info.repository';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
-import { ITagRepository } from 'src/interfaces/tag.repository';
-import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { AccessRepository } from 'src/repositories/access.repository';
-import { ActivityRepository } from 'src/repositories/activity.repository';
-import { AlbumRepository } from 'src/repositories/album.repository';
-import { ApiKeyRepository } from 'src/repositories/api-key.repository';
-import { AssetStackRepository } from 'src/repositories/asset-stack.repository';
-import { AssetRepository } from 'src/repositories/asset.repository';
-import { AuditRepository } from 'src/repositories/audit.repository';
-import { CommunicationRepository } from 'src/repositories/communication.repository';
-import { CryptoRepository } from 'src/repositories/crypto.repository';
-import { DatabaseRepository } from 'src/repositories/database.repository';
-import { FilesystemProvider } from 'src/repositories/filesystem.provider';
-import { JobRepository } from 'src/repositories/job.repository';
-import { LibraryRepository } from 'src/repositories/library.repository';
-import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
-import { MediaRepository } from 'src/repositories/media.repository';
-import { MetadataRepository } from 'src/repositories/metadata.repository';
-import { MoveRepository } from 'src/repositories/move.repository';
-import { PartnerRepository } from 'src/repositories/partner.repository';
-import { PersonRepository } from 'src/repositories/person.repository';
-import { SearchRepository } from 'src/repositories/search.repository';
-import { ServerInfoRepository } from 'src/repositories/server-info.repository';
-import { SharedLinkRepository } from 'src/repositories/shared-link.repository';
-import { SystemConfigRepository } from 'src/repositories/system-config.repository';
-import { SystemMetadataRepository } from 'src/repositories/system-metadata.repository';
-import { TagRepository } from 'src/repositories/tag.repository';
-import { UserTokenRepository } from 'src/repositories/user-token.repository';
-import { UserRepository } from 'src/repositories/user.repository';
-
-const providers: Provider[] = [
- { provide: IActivityRepository, useClass: ActivityRepository },
- { provide: IAccessRepository, useClass: AccessRepository },
- { provide: IAlbumRepository, useClass: AlbumRepository },
- { provide: IAssetRepository, useClass: AssetRepository },
- { provide: IAssetStackRepository, useClass: AssetStackRepository },
- { provide: IAuditRepository, useClass: AuditRepository },
- { provide: ICommunicationRepository, useClass: CommunicationRepository },
- { provide: ICryptoRepository, useClass: CryptoRepository },
- { provide: IDatabaseRepository, useClass: DatabaseRepository },
- { provide: IJobRepository, useClass: JobRepository },
- { provide: ILibraryRepository, useClass: LibraryRepository },
- { provide: IKeyRepository, useClass: ApiKeyRepository },
- { provide: IMachineLearningRepository, useClass: MachineLearningRepository },
- { provide: IMetadataRepository, useClass: MetadataRepository },
- { provide: IMoveRepository, useClass: MoveRepository },
- { provide: IPartnerRepository, useClass: PartnerRepository },
- { provide: IPersonRepository, useClass: PersonRepository },
- { provide: IServerInfoRepository, useClass: ServerInfoRepository },
- { provide: ISharedLinkRepository, useClass: SharedLinkRepository },
- { provide: ISearchRepository, useClass: SearchRepository },
- { provide: IStorageRepository, useClass: FilesystemProvider },
- { provide: ISystemConfigRepository, useClass: SystemConfigRepository },
- { provide: ISystemMetadataRepository, useClass: SystemMetadataRepository },
- { provide: ITagRepository, useClass: TagRepository },
- { provide: IMediaRepository, useClass: MediaRepository },
- { provide: IUserRepository, useClass: UserRepository },
- { provide: IUserTokenRepository, useClass: UserTokenRepository },
- SchedulerRegistry,
-];
-
-@Global()
-@Module({
- imports: [
- ConfigModule.forRoot(immichAppConfig),
- EventEmitterModule.forRoot(),
- TypeOrmModule.forRoot(databaseConfig),
- TypeOrmModule.forFeature(databaseEntities),
- ScheduleModule,
- BullModule.forRoot(bullConfig),
- BullModule.registerQueue(...bullQueues),
- OpenTelemetryModule.forRoot(otelConfig),
- ],
- providers: [...providers],
- exports: [...providers, BullModule],
-})
-export class InfraModule {}
-
-@Global()
-@Module({
- imports: [
- ConfigModule.forRoot(immichAppConfig),
- EventEmitterModule.forRoot(),
- TypeOrmModule.forRoot(databaseConfig),
- TypeOrmModule.forFeature(databaseEntities),
- ScheduleModule,
- ],
- providers: [...providers],
- exports: [...providers],
-})
-export class InfraTestModule {}
diff --git a/server/src/infra/sql-generator/sql.logger.ts b/server/src/infra/sql-generator/sql.logger.ts
deleted file mode 100644
index 6f3c298c0..000000000
--- a/server/src/infra/sql-generator/sql.logger.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { format } from 'sql-formatter';
-import { Logger } from 'typeorm';
-
-export class SqlLogger implements Logger {
- queries: string[] = [];
- errors: Array<{ error: string | Error; query: string }> = [];
-
- clear() {
- this.queries = [];
- this.errors = [];
- }
-
- logQuery(query: string) {
- this.queries.push(format(query, { language: 'postgresql' }));
- }
-
- logQueryError(error: string | Error, query: string) {
- this.errors.push({ error, query });
- }
-
- logQuerySlow() {}
- logSchemaBuild() {}
- logMigration() {}
- log() {}
-}
diff --git a/server/src/interfaces/access.repository.ts b/server/src/interfaces/access.interface.ts
similarity index 100%
rename from server/src/interfaces/access.repository.ts
rename to server/src/interfaces/access.interface.ts
diff --git a/server/src/interfaces/activity.repository.ts b/server/src/interfaces/activity.interface.ts
similarity index 100%
rename from server/src/interfaces/activity.repository.ts
rename to server/src/interfaces/activity.interface.ts
diff --git a/server/src/interfaces/album.repository.ts b/server/src/interfaces/album.interface.ts
similarity index 100%
rename from server/src/interfaces/album.repository.ts
rename to server/src/interfaces/album.interface.ts
diff --git a/server/src/interfaces/api-key.repository.ts b/server/src/interfaces/api-key.interface.ts
similarity index 100%
rename from server/src/interfaces/api-key.repository.ts
rename to server/src/interfaces/api-key.interface.ts
diff --git a/server/src/interfaces/asset-stack.repository.ts b/server/src/interfaces/asset-stack.interface.ts
similarity index 100%
rename from server/src/interfaces/asset-stack.repository.ts
rename to server/src/interfaces/asset-stack.interface.ts
diff --git a/server/src/interfaces/asset-v1.interface.ts b/server/src/interfaces/asset-v1.interface.ts
new file mode 100644
index 000000000..8348bfaee
--- /dev/null
+++ b/server/src/interfaces/asset-v1.interface.ts
@@ -0,0 +1,25 @@
+import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto';
+import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto';
+import { AssetEntity } from 'src/entities/asset.entity';
+
+export interface AssetCheck {
+ id: string;
+ checksum: Buffer;
+}
+
+export interface AssetOwnerCheck extends AssetCheck {
+ ownerId: string;
+}
+
+export interface IAssetRepositoryV1 {
+ get(id: string): Promise;
+ getLocationsByUserId(userId: string): Promise;
+ getDetectedObjectsByUserId(userId: string): Promise;
+ getAllByUserId(userId: string, dto: AssetSearchDto): Promise;
+ getSearchPropertiesByUserId(userId: string): Promise;
+ getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise;
+ getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise;
+ getByOriginalPath(originalPath: string): Promise;
+}
+
+export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
diff --git a/server/src/interfaces/asset.repository.ts b/server/src/interfaces/asset.interface.ts
similarity index 97%
rename from server/src/interfaces/asset.repository.ts
rename to server/src/interfaces/asset.interface.ts
index a04969a4f..0d86bdc05 100644
--- a/server/src/interfaces/asset.repository.ts
+++ b/server/src/interfaces/asset.interface.ts
@@ -2,9 +2,9 @@ import { AssetOrder } from 'src/entities/album.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
-import { ReverseGeocodeResult } from 'src/interfaces/metadata.repository';
-import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.repository';
-import { Paginated, PaginationOptions } from 'src/utils';
+import { ReverseGeocodeResult } from 'src/interfaces/metadata.interface';
+import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
+import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { FindOptionsRelations, FindOptionsSelect } from 'typeorm';
export type AssetStats = Record;
diff --git a/server/src/interfaces/audit.repository.ts b/server/src/interfaces/audit.interface.ts
similarity index 100%
rename from server/src/interfaces/audit.repository.ts
rename to server/src/interfaces/audit.interface.ts
diff --git a/server/src/interfaces/communication.repository.ts b/server/src/interfaces/communication.interface.ts
similarity index 94%
rename from server/src/interfaces/communication.repository.ts
rename to server/src/interfaces/communication.interface.ts
index 870c0d937..4627e5265 100644
--- a/server/src/interfaces/communication.repository.ts
+++ b/server/src/interfaces/communication.interface.ts
@@ -1,5 +1,5 @@
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { ReleaseNotification, ServerVersionResponseDto } from 'src/domain/server-info/server-info.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server-info.dto';
import { SystemConfig } from 'src/entities/system-config.entity';
export const ICommunicationRepository = 'ICommunicationRepository';
diff --git a/server/src/interfaces/crypto.repository.ts b/server/src/interfaces/crypto.interface.ts
similarity index 100%
rename from server/src/interfaces/crypto.repository.ts
rename to server/src/interfaces/crypto.interface.ts
diff --git a/server/src/interfaces/database.repository.ts b/server/src/interfaces/database.interface.ts
similarity index 96%
rename from server/src/interfaces/database.repository.ts
rename to server/src/interfaces/database.interface.ts
index e87775c3e..42342eccc 100644
--- a/server/src/interfaces/database.repository.ts
+++ b/server/src/interfaces/database.interface.ts
@@ -1,4 +1,4 @@
-import { Version } from 'src/domain/domain.constant';
+import { Version } from 'src/utils/version';
export enum DatabaseExtension {
CUBE = 'cube',
diff --git a/server/src/interfaces/job.repository.ts b/server/src/interfaces/job.interface.ts
similarity index 52%
rename from server/src/interfaces/job.repository.ts
rename to server/src/interfaces/job.interface.ts
index 84b39bbad..3257bd721 100644
--- a/server/src/interfaces/job.repository.ts
+++ b/server/src/interfaces/job.interface.ts
@@ -1,14 +1,140 @@
-import { JobName, QueueName } from 'src/domain/job/job.constants';
-import {
- IAssetDeletionJob,
- IBaseJob,
- IDeferrableJob,
- IDeleteFilesJob,
- IEntityJob,
- ILibraryFileJob,
- ILibraryRefreshJob,
- ISidecarWriteJob,
-} from 'src/domain/job/job.interface';
+export enum QueueName {
+ THUMBNAIL_GENERATION = 'thumbnailGeneration',
+ METADATA_EXTRACTION = 'metadataExtraction',
+ VIDEO_CONVERSION = 'videoConversion',
+ FACE_DETECTION = 'faceDetection',
+ FACIAL_RECOGNITION = 'facialRecognition',
+ SMART_SEARCH = 'smartSearch',
+ BACKGROUND_TASK = 'backgroundTask',
+ STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
+ MIGRATION = 'migration',
+ SEARCH = 'search',
+ SIDECAR = 'sidecar',
+ LIBRARY = 'library',
+}
+
+export type ConcurrentQueueName = Exclude<
+ QueueName,
+ QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION
+>;
+
+export enum JobCommand {
+ START = 'start',
+ PAUSE = 'pause',
+ RESUME = 'resume',
+ EMPTY = 'empty',
+ CLEAR_FAILED = 'clear-failed',
+}
+
+export enum JobName {
+ // conversion
+ QUEUE_VIDEO_CONVERSION = 'queue-video-conversion',
+ VIDEO_CONVERSION = 'video-conversion',
+
+ // thumbnails
+ QUEUE_GENERATE_THUMBNAILS = 'queue-generate-thumbnails',
+ GENERATE_JPEG_THUMBNAIL = 'generate-jpeg-thumbnail',
+ GENERATE_WEBP_THUMBNAIL = 'generate-webp-thumbnail',
+ GENERATE_THUMBHASH_THUMBNAIL = 'generate-thumbhash-thumbnail',
+ GENERATE_PERSON_THUMBNAIL = 'generate-person-thumbnail',
+
+ // metadata
+ QUEUE_METADATA_EXTRACTION = 'queue-metadata-extraction',
+ METADATA_EXTRACTION = 'metadata-extraction',
+ LINK_LIVE_PHOTOS = 'link-live-photos',
+
+ // user
+ USER_DELETION = 'user-deletion',
+ USER_DELETE_CHECK = 'user-delete-check',
+ USER_SYNC_USAGE = 'user-sync-usage',
+
+ // asset
+ ASSET_DELETION = 'asset-deletion',
+ ASSET_DELETION_CHECK = 'asset-deletion-check',
+
+ // storage template
+ STORAGE_TEMPLATE_MIGRATION = 'storage-template-migration',
+ STORAGE_TEMPLATE_MIGRATION_SINGLE = 'storage-template-migration-single',
+
+ // migration
+ QUEUE_MIGRATION = 'queue-migration',
+ MIGRATE_ASSET = 'migrate-asset',
+ MIGRATE_PERSON = 'migrate-person',
+
+ // facial recognition
+ PERSON_CLEANUP = 'person-cleanup',
+ QUEUE_FACE_DETECTION = 'queue-face-detection',
+ FACE_DETECTION = 'face-detection',
+ QUEUE_FACIAL_RECOGNITION = 'queue-facial-recognition',
+ FACIAL_RECOGNITION = 'facial-recognition',
+
+ // library management
+ LIBRARY_SCAN = 'library-refresh',
+ LIBRARY_SCAN_ASSET = 'library-refresh-asset',
+ LIBRARY_REMOVE_OFFLINE = 'library-remove-offline',
+ LIBRARY_CHECK_OFFLINE = 'library-check-offline',
+ LIBRARY_DELETE = 'library-delete',
+ LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh',
+ LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup',
+
+ // cleanup
+ DELETE_FILES = 'delete-files',
+ CLEAN_OLD_AUDIT_LOGS = 'clean-old-audit-logs',
+
+ // smart search
+ QUEUE_SMART_SEARCH = 'queue-smart-search',
+ SMART_SEARCH = 'smart-search',
+
+ // XMP sidecars
+ QUEUE_SIDECAR = 'queue-sidecar',
+ SIDECAR_DISCOVERY = 'sidecar-discovery',
+ SIDECAR_SYNC = 'sidecar-sync',
+ SIDECAR_WRITE = 'sidecar-write',
+}
+
+export const JOBS_ASSET_PAGINATION_SIZE = 1000;
+
+export interface IBaseJob {
+ force?: boolean;
+}
+
+export interface IEntityJob extends IBaseJob {
+ id: string;
+ source?: 'upload' | 'sidecar-write';
+}
+
+export interface IAssetDeletionJob extends IEntityJob {
+ fromExternal?: boolean;
+}
+
+export interface ILibraryFileJob extends IEntityJob {
+ ownerId: string;
+ assetPath: string;
+}
+
+export interface ILibraryRefreshJob extends IEntityJob {
+ refreshModifiedFiles: boolean;
+ refreshAllFiles: boolean;
+}
+
+export interface IBulkEntityJob extends IBaseJob {
+ ids: string[];
+}
+
+export interface IDeleteFilesJob extends IBaseJob {
+ files: Array;
+}
+
+export interface ISidecarWriteJob extends IEntityJob {
+ description?: string;
+ dateTimeOriginal?: string;
+ latitude?: number;
+ longitude?: number;
+}
+
+export interface IDeferrableJob extends IEntityJob {
+ deferred?: boolean;
+}
export interface JobCounts {
active: number;
diff --git a/server/src/interfaces/library.repository.ts b/server/src/interfaces/library.interface.ts
similarity index 92%
rename from server/src/interfaces/library.repository.ts
rename to server/src/interfaces/library.interface.ts
index 5638d6024..dbc7fab81 100644
--- a/server/src/interfaces/library.repository.ts
+++ b/server/src/interfaces/library.interface.ts
@@ -1,4 +1,4 @@
-import { LibraryStatsResponseDto } from 'src/domain/library/library.dto';
+import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
export const ILibraryRepository = 'ILibraryRepository';
diff --git a/server/src/interfaces/machine-learning.repository.ts b/server/src/interfaces/machine-learning.interface.ts
similarity index 90%
rename from server/src/interfaces/machine-learning.repository.ts
rename to server/src/interfaces/machine-learning.interface.ts
index d11e2e8f7..0aeed7635 100644
--- a/server/src/interfaces/machine-learning.repository.ts
+++ b/server/src/interfaces/machine-learning.interface.ts
@@ -1,4 +1,4 @@
-import { CLIPConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto';
+import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
export const IMachineLearningRepository = 'IMachineLearningRepository';
diff --git a/server/src/interfaces/media.repository.ts b/server/src/interfaces/media.interface.ts
similarity index 100%
rename from server/src/interfaces/media.repository.ts
rename to server/src/interfaces/media.interface.ts
diff --git a/server/src/interfaces/metadata.repository.ts b/server/src/interfaces/metadata.interface.ts
similarity index 100%
rename from server/src/interfaces/metadata.repository.ts
rename to server/src/interfaces/metadata.interface.ts
diff --git a/server/src/interfaces/move.repository.ts b/server/src/interfaces/move.interface.ts
similarity index 100%
rename from server/src/interfaces/move.repository.ts
rename to server/src/interfaces/move.interface.ts
diff --git a/server/src/interfaces/partner.repository.ts b/server/src/interfaces/partner.interface.ts
similarity index 100%
rename from server/src/interfaces/partner.repository.ts
rename to server/src/interfaces/partner.interface.ts
diff --git a/server/src/interfaces/person.repository.ts b/server/src/interfaces/person.interface.ts
similarity index 97%
rename from server/src/interfaces/person.repository.ts
rename to server/src/interfaces/person.interface.ts
index cba11fbb9..382bbda22 100644
--- a/server/src/interfaces/person.repository.ts
+++ b/server/src/interfaces/person.interface.ts
@@ -1,7 +1,7 @@
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { Paginated, PaginationOptions } from 'src/utils';
+import { Paginated, PaginationOptions } from 'src/utils/pagination';
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect } from 'typeorm';
export const IPersonRepository = 'IPersonRepository';
diff --git a/server/src/interfaces/search.repository.ts b/server/src/interfaces/search.interface.ts
similarity index 98%
rename from server/src/interfaces/search.repository.ts
rename to server/src/interfaces/search.interface.ts
index 29eaa32b7..a4be05549 100644
--- a/server/src/interfaces/search.repository.ts
+++ b/server/src/interfaces/search.interface.ts
@@ -2,7 +2,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-import { Paginated } from 'src/utils';
+import { Paginated } from 'src/utils/pagination';
export const ISearchRepository = 'ISearchRepository';
diff --git a/server/src/interfaces/server-info.repository.ts b/server/src/interfaces/server-info.interface.ts
similarity index 100%
rename from server/src/interfaces/server-info.repository.ts
rename to server/src/interfaces/server-info.interface.ts
diff --git a/server/src/interfaces/shared-link.repository.ts b/server/src/interfaces/shared-link.interface.ts
similarity index 100%
rename from server/src/interfaces/shared-link.repository.ts
rename to server/src/interfaces/shared-link.interface.ts
diff --git a/server/src/interfaces/storage.repository.ts b/server/src/interfaces/storage.interface.ts
similarity index 96%
rename from server/src/interfaces/storage.repository.ts
rename to server/src/interfaces/storage.interface.ts
index 505e535b0..e78bb0195 100644
--- a/server/src/interfaces/storage.repository.ts
+++ b/server/src/interfaces/storage.interface.ts
@@ -2,7 +2,7 @@ import { WatchOptions } from 'chokidar';
import { Stats } from 'node:fs';
import { FileReadOptions } from 'node:fs/promises';
import { Readable } from 'node:stream';
-import { CrawlOptionsDto } from 'src/domain/library/library.dto';
+import { CrawlOptionsDto } from 'src/dtos/library.dto';
export interface ImmichReadStream {
stream: Readable;
diff --git a/server/src/interfaces/system-config.repository.ts b/server/src/interfaces/system-config.interface.ts
similarity index 100%
rename from server/src/interfaces/system-config.repository.ts
rename to server/src/interfaces/system-config.interface.ts
diff --git a/server/src/interfaces/system-metadata.repository.ts b/server/src/interfaces/system-metadata.interface.ts
similarity index 100%
rename from server/src/interfaces/system-metadata.repository.ts
rename to server/src/interfaces/system-metadata.interface.ts
diff --git a/server/src/interfaces/tag.repository.ts b/server/src/interfaces/tag.interface.ts
similarity index 100%
rename from server/src/interfaces/tag.repository.ts
rename to server/src/interfaces/tag.interface.ts
diff --git a/server/src/interfaces/user-token.repository.ts b/server/src/interfaces/user-token.interface.ts
similarity index 100%
rename from server/src/interfaces/user-token.repository.ts
rename to server/src/interfaces/user-token.interface.ts
diff --git a/server/src/interfaces/user.repository.ts b/server/src/interfaces/user.interface.ts
similarity index 100%
rename from server/src/interfaces/user.repository.ts
rename to server/src/interfaces/user.interface.ts
diff --git a/server/src/main.ts b/server/src/main.ts
index ee60c793c..3a9303868 100644
--- a/server/src/main.ts
+++ b/server/src/main.ts
@@ -1,6 +1,75 @@
-import { bootstrapApi } from 'src/apps/api.main';
-import { bootstrapImmichAdmin } from 'src/apps/immich-admin.main';
-import { bootstrapMicroservices } from 'src/apps/microservices.main';
+import { NestFactory } from '@nestjs/core';
+import { NestExpressApplication } from '@nestjs/platform-express';
+import { json } from 'body-parser';
+import cookieParser from 'cookie-parser';
+import { CommandFactory } from 'nest-commander';
+import { existsSync } from 'node:fs';
+import sirv from 'sirv';
+import { ApiModule, ImmichAdminModule, MicroservicesModule } from 'src/app.module';
+import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants';
+import { LogLevel } from 'src/entities/system-config.entity';
+import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
+import { ApiService } from 'src/services/api.service';
+import { otelSDK } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+import { useSwagger } from 'src/utils/misc';
+
+async function bootstrapMicroservices() {
+ const logger = new ImmichLogger('ImmichMicroservice');
+ const port = Number(process.env.MICROSERVICES_PORT) || 3002;
+
+ otelSDK.start();
+ const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
+ app.useLogger(app.get(ImmichLogger));
+ app.useWebSocketAdapter(new WebSocketAdapter(app));
+
+ await app.listen(port);
+
+ logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
+}
+
+async function bootstrapApi() {
+ const logger = new ImmichLogger('ImmichServer');
+ const port = Number(process.env.SERVER_PORT) || 3001;
+
+ otelSDK.start();
+ const app = await NestFactory.create(ApiModule, { bufferLogs: true });
+
+ app.useLogger(app.get(ImmichLogger));
+ app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
+ app.set('etag', 'strong');
+ app.use(cookieParser());
+ app.use(json({ limit: '10mb' }));
+ if (isDev) {
+ app.enableCors();
+ }
+ app.useWebSocketAdapter(new WebSocketAdapter(app));
+ useSwagger(app, isDev);
+
+ app.setGlobalPrefix('api', { exclude: excludePaths });
+ if (existsSync(WEB_ROOT)) {
+ // copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
+ // provides serving of precompressed assets and caching of immutable assets
+ app.use(
+ sirv(WEB_ROOT, {
+ etag: true,
+ gzip: true,
+ brotli: true,
+ setHeaders: (res, pathname) => {
+ if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) {
+ res.setHeader('cache-control', 'public,max-age=31536000,immutable');
+ }
+ },
+ }),
+ );
+ }
+ app.use(app.get(ApiService).ssr(excludePaths));
+
+ const server = await app.listen(port);
+ server.requestTimeout = 30 * 60 * 1000;
+
+ logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
+}
const immichApp = process.argv[2] || process.env.IMMICH_APP;
@@ -8,6 +77,11 @@ if (process.argv[2] === immichApp) {
process.argv.splice(2, 1);
}
+async function bootstrapImmichAdmin() {
+ process.env.LOG_LEVEL = LogLevel.WARN;
+ await CommandFactory.run(ImmichAdminModule);
+}
+
function bootstrap() {
switch (immichApp) {
case 'immich': {
@@ -23,8 +97,9 @@ function bootstrap() {
return bootstrapImmichAdmin();
}
default: {
- throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
+ throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`);
}
}
}
+
void bootstrap();
diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts
index 070bf15e8..eaa47d013 100644
--- a/server/src/middleware/auth.guard.ts
+++ b/server/src/middleware/auth.guard.ts
@@ -9,10 +9,10 @@ import {
import { Reflector } from '@nestjs/core';
import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger';
import { Request } from 'express';
-import { IMMICH_API_KEY_NAME } from 'src/domain/auth/auth.constant';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { AuthService, LoginDetails } from 'src/domain/auth/auth.service';
-import { ImmichLogger } from 'src/infra/logger';
+import { IMMICH_API_KEY_NAME } from 'src/constants';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { AuthService, LoginDetails } from 'src/services/auth.service';
+import { ImmichLogger } from 'src/utils/logger';
import { UAParser } from 'ua-parser-js';
export enum Metadata {
diff --git a/server/src/middleware/error.interceptor.ts b/server/src/middleware/error.interceptor.ts
index 70c129922..9e2273b97 100644
--- a/server/src/middleware/error.interceptor.ts
+++ b/server/src/middleware/error.interceptor.ts
@@ -7,9 +7,8 @@ import {
NestInterceptor,
} from '@nestjs/common';
import { Observable, catchError, throwError } from 'rxjs';
-import { routeToErrorMessage } from 'src/immich/app.utils';
-import { ImmichLogger } from 'src/infra/logger';
-import { isConnectionAborted } from 'src/utils';
+import { ImmichLogger } from 'src/utils/logger';
+import { isConnectionAborted, routeToErrorMessage } from 'src/utils/misc';
@Injectable()
export class ErrorInterceptor implements NestInterceptor {
diff --git a/server/src/middleware/file-upload.interceptor.ts b/server/src/middleware/file-upload.interceptor.ts
index a7598f99d..53acbefa8 100644
--- a/server/src/middleware/file-upload.interceptor.ts
+++ b/server/src/middleware/file-upload.interceptor.ts
@@ -6,9 +6,10 @@ import { NextFunction, RequestHandler } from 'express';
import multer, { StorageEngine, diskStorage } from 'multer';
import { createHash, randomUUID } from 'node:crypto';
import { Observable } from 'rxjs';
-import { AssetService, UploadFieldName, UploadFile } from 'src/domain/asset/asset.service';
-import { ImmichLogger } from 'src/infra/logger';
+import { UploadFieldName } from 'src/dtos/asset.dto';
import { AuthRequest } from 'src/middleware/auth.guard';
+import { AssetService, UploadFile } from 'src/services/asset.service';
+import { ImmichLogger } from 'src/utils/logger';
export enum Route {
ASSET = 'asset',
diff --git a/server/src/infra/websocket.adapter.ts b/server/src/middleware/websocket.adapter.ts
similarity index 100%
rename from server/src/infra/websocket.adapter.ts
rename to server/src/middleware/websocket.adapter.ts
diff --git a/server/src/migrations/1700713871511-UsePgVectors.ts b/server/src/migrations/1700713871511-UsePgVectors.ts
index 46d5320ce..75c85e3e0 100644
--- a/server/src/migrations/1700713871511-UsePgVectors.ts
+++ b/server/src/migrations/1700713871511-UsePgVectors.ts
@@ -1,5 +1,5 @@
-import { getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant';
-import { vectorExt } from 'src/infra/database.config';
+import { vectorExt } from 'src/database.config';
+import { getCLIPModelInfo } from 'src/utils/misc';
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UsePgVectors1700713871511 implements MigrationInterface {
diff --git a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
index b045ba62b..908ebdb8f 100644
--- a/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
+++ b/server/src/migrations/1700713994428-AddCLIPEmbeddingIndex.ts
@@ -1,5 +1,5 @@
-import { vectorExt } from 'src/infra/database.config';
-import { DatabaseExtension } from 'src/interfaces/database.repository';
+import { vectorExt } from 'src/database.config';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
diff --git a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
index e77ce3b0b..75bebfa8e 100644
--- a/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
+++ b/server/src/migrations/1700714033632-AddFaceEmbeddingIndex.ts
@@ -1,5 +1,5 @@
-import { vectorExt } from 'src/infra/database.config';
-import { DatabaseExtension } from 'src/interfaces/database.repository';
+import { vectorExt } from 'src/database.config';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
import { MigrationInterface, QueryRunner } from 'typeorm';
export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
diff --git a/server/src/infra/sql/access.repository.sql b/server/src/queries/access.repository.sql
similarity index 100%
rename from server/src/infra/sql/access.repository.sql
rename to server/src/queries/access.repository.sql
diff --git a/server/src/infra/sql/album.repository.sql b/server/src/queries/album.repository.sql
similarity index 100%
rename from server/src/infra/sql/album.repository.sql
rename to server/src/queries/album.repository.sql
diff --git a/server/src/infra/sql/api.key.repository.sql b/server/src/queries/api.key.repository.sql
similarity index 100%
rename from server/src/infra/sql/api.key.repository.sql
rename to server/src/queries/api.key.repository.sql
diff --git a/server/src/infra/sql/asset.repository.sql b/server/src/queries/asset.repository.sql
similarity index 100%
rename from server/src/infra/sql/asset.repository.sql
rename to server/src/queries/asset.repository.sql
diff --git a/server/src/infra/sql/audit.repository.sql b/server/src/queries/audit.repository.sql
similarity index 100%
rename from server/src/infra/sql/audit.repository.sql
rename to server/src/queries/audit.repository.sql
diff --git a/server/src/infra/sql/library.repository.sql b/server/src/queries/library.repository.sql
similarity index 100%
rename from server/src/infra/sql/library.repository.sql
rename to server/src/queries/library.repository.sql
diff --git a/server/src/infra/sql/move.repository.sql b/server/src/queries/move.repository.sql
similarity index 100%
rename from server/src/infra/sql/move.repository.sql
rename to server/src/queries/move.repository.sql
diff --git a/server/src/infra/sql/partner.repository.sql b/server/src/queries/partner.repository.sql
similarity index 100%
rename from server/src/infra/sql/partner.repository.sql
rename to server/src/queries/partner.repository.sql
diff --git a/server/src/infra/sql/person.repository.sql b/server/src/queries/person.repository.sql
similarity index 100%
rename from server/src/infra/sql/person.repository.sql
rename to server/src/queries/person.repository.sql
diff --git a/server/src/infra/sql/search.repository.sql b/server/src/queries/search.repository.sql
similarity index 100%
rename from server/src/infra/sql/search.repository.sql
rename to server/src/queries/search.repository.sql
diff --git a/server/src/infra/sql/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql
similarity index 100%
rename from server/src/infra/sql/shared.link.repository.sql
rename to server/src/queries/shared.link.repository.sql
diff --git a/server/src/infra/sql/system.config.repository.sql b/server/src/queries/system.config.repository.sql
similarity index 100%
rename from server/src/infra/sql/system.config.repository.sql
rename to server/src/queries/system.config.repository.sql
diff --git a/server/src/infra/sql/system.metadata.repository.sql b/server/src/queries/system.metadata.repository.sql
similarity index 100%
rename from server/src/infra/sql/system.metadata.repository.sql
rename to server/src/queries/system.metadata.repository.sql
diff --git a/server/src/infra/sql/tag.repository.sql b/server/src/queries/tag.repository.sql
similarity index 100%
rename from server/src/infra/sql/tag.repository.sql
rename to server/src/queries/tag.repository.sql
diff --git a/server/src/infra/sql/user.repository.sql b/server/src/queries/user.repository.sql
similarity index 100%
rename from server/src/infra/sql/user.repository.sql
rename to server/src/queries/user.repository.sql
diff --git a/server/src/infra/sql/user.token.repository.sql b/server/src/queries/user.token.repository.sql
similarity index 100%
rename from server/src/infra/sql/user.token.repository.sql
rename to server/src/queries/user.token.repository.sql
diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts
index 418cf542f..37b5be0e8 100644
--- a/server/src/repositories/access.repository.ts
+++ b/server/src/repositories/access.repository.ts
@@ -9,8 +9,8 @@ import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IAccessRepository } from 'src/interfaces/access.repository';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Brackets, In, Repository } from 'typeorm';
type IActivityAccess = IAccessRepository['activity'];
diff --git a/server/src/repositories/activity.repository.ts b/server/src/repositories/activity.repository.ts
index bec4a4f17..475ad3b85 100644
--- a/server/src/repositories/activity.repository.ts
+++ b/server/src/repositories/activity.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { ActivityEntity } from 'src/entities/activity.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IActivityRepository } from 'src/interfaces/activity.repository';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { IsNull, Repository } from 'typeorm';
export interface ActivitySearch {
diff --git a/server/src/repositories/album.repository.ts b/server/src/repositories/album.repository.ts
index 4646ef3d9..f98be2161 100644
--- a/server/src/repositories/album.repository.ts
+++ b/server/src/repositories/album.repository.ts
@@ -1,19 +1,19 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
+import { dataSource } from 'src/database.config';
import { Chunked, ChunkedArray, DATABASE_PARAMETER_CHUNK_SIZE, DummyValue, GenerateSql } from 'src/decorators';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
-import { dataSource } from 'src/infra/database.config';
-import { Instrumentation } from 'src/infra/instrumentation';
import {
AlbumAsset,
AlbumAssetCount,
AlbumAssets,
AlbumInfoOptions,
IAlbumRepository,
-} from 'src/interfaces/album.repository';
-import { setUnion } from 'src/utils';
+} from 'src/interfaces/album.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { setUnion } from 'src/utils/set';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts
index 14a7db9c9..d03d04806 100644
--- a/server/src/repositories/api-key.repository.ts
+++ b/server/src/repositories/api-key.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { APIKeyEntity } from 'src/entities/api-key.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/asset-stack.repository.ts b/server/src/repositories/asset-stack.repository.ts
index 9859c5fad..660dfbe47 100644
--- a/server/src/repositories/asset-stack.repository.ts
+++ b/server/src/repositories/asset-stack.repository.ts
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AssetStackEntity } from 'src/entities/asset-stack.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository';
+import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/immich/api-v1/asset/asset-repository.ts b/server/src/repositories/asset-v1.repository.ts
similarity index 72%
rename from server/src/immich/api-v1/asset/asset-repository.ts
rename to server/src/repositories/asset-v1.repository.ts
index e7b870239..6f53d820c 100644
--- a/server/src/immich/api-v1/asset/asset-repository.ts
+++ b/server/src/repositories/asset-v1.repository.ts
@@ -1,43 +1,16 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
+import { CuratedLocationsResponseDto, CuratedObjectsResponseDto } from 'src/dtos/asset-v1-response.dto';
+import { AssetSearchDto, CheckExistingAssetsDto, SearchPropertiesDto } from 'src/dtos/asset-v1.dto';
import { AssetEntity } from 'src/entities/asset.entity';
-import { ExifEntity } from 'src/entities/exif.entity';
-import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
-import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
-import { SearchPropertiesDto } from 'src/immich/api-v1/asset/dto/search-properties.dto';
-import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
-import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
-import { OptionalBetween } from 'src/infra/infra.utils';
+import { AssetCheck, AssetOwnerCheck, IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
+import { OptionalBetween } from 'src/utils/database';
import { In } from 'typeorm/find-options/operator/In.js';
import { Repository } from 'typeorm/repository/Repository.js';
-export interface AssetCheck {
- id: string;
- checksum: Buffer;
-}
-
-export interface AssetOwnerCheck extends AssetCheck {
- ownerId: string;
-}
-
-export interface IAssetRepositoryV1 {
- get(id: string): Promise;
- getLocationsByUserId(userId: string): Promise;
- getDetectedObjectsByUserId(userId: string): Promise;
- getAllByUserId(userId: string, dto: AssetSearchDto): Promise;
- getSearchPropertiesByUserId(userId: string): Promise;
- getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise;
- getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise;
- getByOriginalPath(originalPath: string): Promise;
-}
-
-export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
@Injectable()
export class AssetRepositoryV1 implements IAssetRepositoryV1 {
- constructor(
- @InjectRepository(AssetEntity) private assetRepository: Repository,
- @InjectRepository(ExifEntity) private exifRepository: Repository,
- ) {}
+ constructor(@InjectRepository(AssetEntity) private assetRepository: Repository) {}
/**
* Retrieves all assets by user ID.
diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts
index 2ffdf8dd2..68b786cbf 100644
--- a/server/src/repositories/asset.repository.ts
+++ b/server/src/repositories/asset.repository.ts
@@ -8,8 +8,6 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
-import { OptionalBetween, paginate, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils';
-import { Instrumentation } from 'src/infra/instrumentation';
import {
AssetBuilderOptions,
AssetCreate,
@@ -30,9 +28,11 @@ import {
TimeBucketSize,
WithProperty,
WithoutProperty,
-} from 'src/interfaces/asset.repository';
-import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.repository';
-import { Paginated, PaginationMode, PaginationOptions } from 'src/utils';
+} from 'src/interfaces/asset.interface';
+import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface';
+import { OptionalBetween, searchAssetBuilder } from 'src/utils/database';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
import {
Brackets,
FindOptionsRelations,
diff --git a/server/src/repositories/audit.repository.ts b/server/src/repositories/audit.repository.ts
index 1d892dab2..50f5631f3 100644
--- a/server/src/repositories/audit.repository.ts
+++ b/server/src/repositories/audit.repository.ts
@@ -1,7 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm';
import { AuditEntity } from 'src/entities/audit.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.repository';
+import { AuditSearch, IAuditRepository } from 'src/interfaces/audit.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { LessThan, MoreThan, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/communication.repository.ts b/server/src/repositories/communication.repository.ts
index e92fe1387..046190e1a 100644
--- a/server/src/repositories/communication.repository.ts
+++ b/server/src/repositories/communication.repository.ts
@@ -7,9 +7,6 @@ import {
WebSocketServer,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';
-import { AuthService } from 'src/domain/auth/auth.service';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
import {
ClientEvent,
ICommunicationRepository,
@@ -17,7 +14,10 @@ import {
OnConnectCallback,
OnServerEventCallback,
ServerEvent,
-} from 'src/interfaces/communication.repository';
+} from 'src/interfaces/communication.interface';
+import { AuthService } from 'src/services/auth.service';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
@Instrumentation()
@WebSocketGateway({
diff --git a/server/src/repositories/crypto.repository.ts b/server/src/repositories/crypto.repository.ts
index 121943af3..84b74052c 100644
--- a/server/src/repositories/crypto.repository.ts
+++ b/server/src/repositories/crypto.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { compareSync, hash } from 'bcrypt';
import { createHash, randomBytes, randomUUID } from 'node:crypto';
import { createReadStream } from 'node:fs';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
@Instrumentation()
@Injectable()
diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts
index 60586edc5..4ff24eeaa 100644
--- a/server/src/repositories/database.repository.ts
+++ b/server/src/repositories/database.repository.ts
@@ -1,10 +1,7 @@
import { Injectable } from '@nestjs/common';
import { InjectDataSource } from '@nestjs/typeorm';
import AsyncLock from 'async-lock';
-import { Version, VersionType } from 'src/domain/domain.constant';
-import { vectorExt } from 'src/infra/database.config';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
+import { vectorExt } from 'src/database.config';
import {
DatabaseExtension,
DatabaseLock,
@@ -13,7 +10,10 @@ import {
VectorIndex,
VectorUpdateResult,
extName,
-} from 'src/interfaces/database.repository';
+} from 'src/interfaces/database.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+import { Version, VersionType } from 'src/utils/version';
import { isValidInteger } from 'src/validation';
import { DataSource, EntityManager, QueryRunner } from 'typeorm';
diff --git a/server/src/repositories/filesystem.provider.spec.ts b/server/src/repositories/filesystem.provider.spec.ts
index 56f54b036..c9790767c 100644
--- a/server/src/repositories/filesystem.provider.spec.ts
+++ b/server/src/repositories/filesystem.provider.spec.ts
@@ -1,5 +1,5 @@
import mockfs from 'mock-fs';
-import { CrawlOptionsDto } from 'src/domain/library/library.dto';
+import { CrawlOptionsDto } from 'src/dtos/library.dto';
import { FilesystemProvider } from 'src/repositories/filesystem.provider';
interface Test {
diff --git a/server/src/repositories/filesystem.provider.ts b/server/src/repositories/filesystem.provider.ts
index 2da49625e..b4f231120 100644
--- a/server/src/repositories/filesystem.provider.ts
+++ b/server/src/repositories/filesystem.provider.ts
@@ -4,10 +4,7 @@ import { glob, globStream } from 'fast-glob';
import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
import fs from 'node:fs/promises';
import path from 'node:path';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { CrawlOptionsDto } from 'src/domain/library/library.dto';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
+import { CrawlOptionsDto } from 'src/dtos/library.dto';
import {
DiskUsage,
IStorageRepository,
@@ -15,7 +12,10 @@ import {
ImmichZipStream,
StorageEventType,
WatchEvents,
-} from 'src/interfaces/storage.repository';
+} from 'src/interfaces/storage.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
@Instrumentation()
export class FilesystemProvider implements IStorageRepository {
diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts
index a7637e0e6..b55996ed0 100644
--- a/server/src/repositories/job.repository.ts
+++ b/server/src/repositories/job.repository.ts
@@ -6,10 +6,78 @@ import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullm
import { CronJob, CronTime } from 'cron';
import { setTimeout } from 'node:timers/promises';
import { bullConfig } from 'src/config';
-import { JOBS_TO_QUEUE, JobName, QueueName } from 'src/domain/job/job.constants';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
-import { IJobRepository, JobCounts, JobItem, QueueCleanType, QueueStatus } from 'src/interfaces/job.repository';
+import {
+ IJobRepository,
+ JobCounts,
+ JobItem,
+ JobName,
+ QueueCleanType,
+ QueueName,
+ QueueStatus,
+} from 'src/interfaces/job.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+
+export const JOBS_TO_QUEUE: Record = {
+ // misc
+ [JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK,
+ [JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK,
+ [JobName.USER_DELETE_CHECK]: QueueName.BACKGROUND_TASK,
+ [JobName.USER_DELETION]: QueueName.BACKGROUND_TASK,
+ [JobName.DELETE_FILES]: QueueName.BACKGROUND_TASK,
+ [JobName.CLEAN_OLD_AUDIT_LOGS]: QueueName.BACKGROUND_TASK,
+ [JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
+ [JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK,
+
+ // conversion
+ [JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
+ [JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
+
+ // thumbnails
+ [JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
+ [JobName.GENERATE_JPEG_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
+ [JobName.GENERATE_WEBP_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
+ [JobName.GENERATE_THUMBHASH_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
+ [JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
+
+ // metadata
+ [JobName.QUEUE_METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
+ [JobName.METADATA_EXTRACTION]: QueueName.METADATA_EXTRACTION,
+ [JobName.LINK_LIVE_PHOTOS]: QueueName.METADATA_EXTRACTION,
+
+ // storage template
+ [JobName.STORAGE_TEMPLATE_MIGRATION]: QueueName.STORAGE_TEMPLATE_MIGRATION,
+ [JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: QueueName.STORAGE_TEMPLATE_MIGRATION,
+
+ // migration
+ [JobName.QUEUE_MIGRATION]: QueueName.MIGRATION,
+ [JobName.MIGRATE_ASSET]: QueueName.MIGRATION,
+ [JobName.MIGRATE_PERSON]: QueueName.MIGRATION,
+
+ // facial recognition
+ [JobName.QUEUE_FACE_DETECTION]: QueueName.FACE_DETECTION,
+ [JobName.FACE_DETECTION]: QueueName.FACE_DETECTION,
+ [JobName.QUEUE_FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
+ [JobName.FACIAL_RECOGNITION]: QueueName.FACIAL_RECOGNITION,
+
+ // smart search
+ [JobName.QUEUE_SMART_SEARCH]: QueueName.SMART_SEARCH,
+ [JobName.SMART_SEARCH]: QueueName.SMART_SEARCH,
+
+ // XMP sidecars
+ [JobName.QUEUE_SIDECAR]: QueueName.SIDECAR,
+ [JobName.SIDECAR_DISCOVERY]: QueueName.SIDECAR,
+ [JobName.SIDECAR_SYNC]: QueueName.SIDECAR,
+ [JobName.SIDECAR_WRITE]: QueueName.SIDECAR,
+
+ // Library management
+ [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY,
+ [JobName.LIBRARY_SCAN]: QueueName.LIBRARY,
+ [JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
+ [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY,
+ [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY,
+ [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
+};
@Instrumentation()
@Injectable()
diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts
index 19ac40bd5..d53354e25 100644
--- a/server/src/repositories/library.repository.ts
+++ b/server/src/repositories/library.repository.ts
@@ -1,10 +1,10 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
-import { LibraryStatsResponseDto } from 'src/domain/library/library.dto';
+import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { IsNull, Not } from 'typeorm';
import { Repository } from 'typeorm/repository/Repository.js';
diff --git a/server/src/repositories/machine-learning.repository.ts b/server/src/repositories/machine-learning.repository.ts
index 46d16dcd7..3d8f0cac1 100644
--- a/server/src/repositories/machine-learning.repository.ts
+++ b/server/src/repositories/machine-learning.repository.ts
@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { readFile } from 'node:fs/promises';
-import { CLIPConfig, ModelConfig, RecognitionConfig } from 'src/domain/smart-info/dto/model-config.dto';
-import { Instrumentation } from 'src/infra/instrumentation';
+import { CLIPConfig, ModelConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
import {
CLIPMode,
DetectFaceResult,
@@ -9,7 +8,8 @@ import {
ModelType,
TextModelInput,
VisionModelInput,
-} from 'src/interfaces/machine-learning.repository';
+} from 'src/interfaces/machine-learning.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
const errorPrefix = 'Machine learning request';
diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts
index d0f4332b4..52a538909 100644
--- a/server/src/repositories/media.repository.ts
+++ b/server/src/repositories/media.repository.ts
@@ -4,16 +4,16 @@ import { Writable } from 'node:stream';
import { promisify } from 'node:util';
import sharp from 'sharp';
import { Colorspace } from 'src/entities/system-config.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
import {
CropOptions,
IMediaRepository,
ResizeOptions,
TranscodeOptions,
VideoInfo,
-} from 'src/interfaces/media.repository';
-import { handlePromiseError } from 'src/utils';
+} from 'src/interfaces/media.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+import { handlePromiseError } from 'src/utils/misc';
const probe = promisify(ffmpeg.ffprobe);
sharp.concurrency(0);
diff --git a/server/src/repositories/metadata.repository.ts b/server/src/repositories/metadata.repository.ts
index 5da664f14..511023a8e 100644
--- a/server/src/repositories/metadata.repository.ts
+++ b/server/src/repositories/metadata.repository.ts
@@ -6,21 +6,15 @@ import { getName } from 'i18n-iso-countries';
import { createReadStream, existsSync } from 'node:fs';
import { readFile } from 'node:fs/promises';
import readLine from 'node:readline';
+import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants';
import { DummyValue, GenerateSql } from 'src/decorators';
-import {
- citiesFile,
- geodataAdmin1Path,
- geodataAdmin2Path,
- geodataCities500Path,
- geodataDatePath,
-} from 'src/domain/domain.constant';
import { ExifEntity } from 'src/entities/exif.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
-import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.repository';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
+import { GeoPoint, IMetadataRepository, ImmichTags, ReverseGeocodeResult } from 'src/interfaces/metadata.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
import { DataSource, QueryRunner, Repository } from 'typeorm';
import { QueryDeepPartialEntity } from 'typeorm/query-builder/QueryPartialEntity.js';
diff --git a/server/src/repositories/move.repository.ts b/server/src/repositories/move.repository.ts
index e567bcca3..a8416ff0a 100644
--- a/server/src/repositories/move.repository.ts
+++ b/server/src/repositories/move.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { MoveEntity, PathType } from 'src/entities/move.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IMoveRepository, MoveCreate } from 'src/interfaces/move.repository';
+import { IMoveRepository, MoveCreate } from 'src/interfaces/move.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/partner.repository.ts b/server/src/repositories/partner.repository.ts
index 4c4bbc0ed..8465493b5 100644
--- a/server/src/repositories/partner.repository.ts
+++ b/server/src/repositories/partner.repository.ts
@@ -1,8 +1,8 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { PartnerEntity } from 'src/entities/partner.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.repository';
+import { IPartnerRepository, PartnerIds } from 'src/interfaces/partner.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { DeepPartial, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/person.repository.ts b/server/src/repositories/person.repository.ts
index 6a0008c3d..867d6b174 100644
--- a/server/src/repositories/person.repository.ts
+++ b/server/src/repositories/person.repository.ts
@@ -4,8 +4,6 @@ import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { asVector, paginate } from 'src/infra/infra.utils';
-import { Instrumentation } from 'src/infra/instrumentation';
import {
AssetFaceId,
IPersonRepository,
@@ -14,8 +12,10 @@ import {
PersonSearchOptions,
PersonStatistics,
UpdateFacesData,
-} from 'src/interfaces/person.repository';
-import { Paginated, PaginationOptions } from 'src/utils';
+} from 'src/interfaces/person.interface';
+import { asVector } from 'src/utils/database';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { Paginated, PaginationOptions, paginate } from 'src/utils/pagination';
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts
index aefb3c933..a151c0750 100644
--- a/server/src/repositories/search.repository.ts
+++ b/server/src/repositories/search.repository.ts
@@ -1,17 +1,13 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
+import { vectorExt } from 'src/database.config';
import { DummyValue, GenerateSql } from 'src/decorators';
-import { getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
-import { vectorExt } from 'src/infra/database.config';
-import { asVector, paginatedBuilder, searchAssetBuilder } from 'src/infra/infra.utils';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ImmichLogger } from 'src/infra/logger';
-import { DatabaseExtension } from 'src/interfaces/database.repository';
+import { DatabaseExtension } from 'src/interfaces/database.interface';
import {
AssetSearchOptions,
Embedding,
@@ -20,8 +16,12 @@ import {
ISearchRepository,
SearchPaginationOptions,
SmartSearchOptions,
-} from 'src/interfaces/search.repository';
-import { Paginated, PaginationMode, PaginationResult } from 'src/utils';
+} from 'src/interfaces/search.interface';
+import { asVector, searchAssetBuilder } from 'src/utils/database';
+import { Instrumentation } from 'src/utils/instrumentation';
+import { ImmichLogger } from 'src/utils/logger';
+import { getCLIPModelInfo } from 'src/utils/misc';
+import { Paginated, PaginationMode, PaginationResult, paginatedBuilder } from 'src/utils/pagination';
import { isValidInteger } from 'src/validation';
import { Repository, SelectQueryBuilder } from 'typeorm';
diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts
index fcef0a396..5f14a881c 100644
--- a/server/src/repositories/server-info.repository.ts
+++ b/server/src/repositories/server-info.repository.ts
@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.repository';
+import { GitHubRelease, IServerInfoRepository } from 'src/interfaces/server-info.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
@Instrumentation()
@Injectable()
diff --git a/server/src/repositories/shared-link.repository.ts b/server/src/repositories/shared-link.repository.ts
index 968b6dd73..48dbb3ab9 100644
--- a/server/src/repositories/shared-link.repository.ts
+++ b/server/src/repositories/shared-link.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/system-config.repository.ts b/server/src/repositories/system-config.repository.ts
index af6296ce0..baa3218b0 100644
--- a/server/src/repositories/system-config.repository.ts
+++ b/server/src/repositories/system-config.repository.ts
@@ -2,8 +2,8 @@ import { InjectRepository } from '@nestjs/typeorm';
import { readFile } from 'node:fs/promises';
import { Chunked, DummyValue, GenerateSql } from 'src/decorators';
import { SystemConfigEntity } from 'src/entities/system-config.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { In, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/system-metadata.repository.ts b/server/src/repositories/system-metadata.repository.ts
index 978dba9bd..80936e46f 100644
--- a/server/src/repositories/system-metadata.repository.ts
+++ b/server/src/repositories/system-metadata.repository.ts
@@ -1,7 +1,7 @@
import { InjectRepository } from '@nestjs/typeorm';
import { SystemMetadata, SystemMetadataEntity } from 'src/entities/system-metadata.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts
index a2ae74878..6fa827906 100644
--- a/server/src/repositories/tag.repository.ts
+++ b/server/src/repositories/tag.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { AssetEntity } from 'src/entities/asset.entity';
import { TagEntity } from 'src/entities/tag.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { ITagRepository } from 'src/interfaces/tag.repository';
+import { ITagRepository } from 'src/interfaces/tag.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/user-token.repository.ts b/server/src/repositories/user-token.repository.ts
index 31d6d2190..cbf3a3e3b 100644
--- a/server/src/repositories/user-token.repository.ts
+++ b/server/src/repositories/user-token.repository.ts
@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { UserTokenEntity } from 'src/entities/user-token.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
-import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
+import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/repositories/user.repository.ts b/server/src/repositories/user.repository.ts
index bd94e71a5..f0e00d049 100644
--- a/server/src/repositories/user.repository.ts
+++ b/server/src/repositories/user.repository.ts
@@ -3,13 +3,13 @@ import { InjectRepository } from '@nestjs/typeorm';
import { DummyValue, GenerateSql } from 'src/decorators';
import { AssetEntity } from 'src/entities/asset.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { Instrumentation } from 'src/infra/instrumentation';
import {
IUserRepository,
UserFindOptions,
UserListFilter,
UserStatsQueryResponse,
-} from 'src/interfaces/user.repository';
+} from 'src/interfaces/user.interface';
+import { Instrumentation } from 'src/utils/instrumentation';
import { IsNull, Not, Repository } from 'typeorm';
@Instrumentation()
diff --git a/server/src/domain/activity/activity.spec.ts b/server/src/services/activity.service.spec.ts
similarity index 96%
rename from server/src/domain/activity/activity.spec.ts
rename to server/src/services/activity.service.spec.ts
index 467540be3..e7049ea6c 100644
--- a/server/src/domain/activity/activity.spec.ts
+++ b/server/src/services/activity.service.spec.ts
@@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common';
-import { ReactionType } from 'src/domain/activity/activity.dto';
-import { ActivityService } from 'src/domain/activity/activity.service';
-import { IActivityRepository } from 'src/interfaces/activity.repository';
+import { ReactionType } from 'src/dtos/activity.dto';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
+import { ActivityService } from 'src/services/activity.service';
import { activityStub } from 'test/fixtures/activity.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
diff --git a/server/src/domain/activity/activity.service.ts b/server/src/services/activity.service.ts
similarity index 91%
rename from server/src/domain/activity/activity.service.ts
rename to server/src/services/activity.service.ts
index 81d77fa8d..7589fb8cc 100644
--- a/server/src/domain/activity/activity.service.ts
+++ b/server/src/services/activity.service.ts
@@ -10,11 +10,11 @@ import {
ReactionLevel,
ReactionType,
mapActivity,
-} from 'src/domain/activity/activity.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+} from 'src/dtos/activity.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { ActivityEntity } from 'src/entities/activity.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IActivityRepository } from 'src/interfaces/activity.repository';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
@Injectable()
export class ActivityService {
diff --git a/server/src/domain/album/album.service.spec.ts b/server/src/services/album.service.spec.ts
similarity index 98%
rename from server/src/domain/album/album.service.spec.ts
rename to server/src/services/album.service.spec.ts
index 5b3a91ecf..48462fac4 100644
--- a/server/src/domain/album/album.service.spec.ts
+++ b/server/src/services/album.service.spec.ts
@@ -1,10 +1,10 @@
import { BadRequestException } from '@nestjs/common';
import _ from 'lodash';
-import { AlbumService } from 'src/domain/album/album.service';
-import { BulkIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AlbumService } from 'src/services/album.service';
import { albumStub } from 'test/fixtures/album.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { userStub } from 'test/fixtures/user.stub';
diff --git a/server/src/domain/album/album.service.ts b/server/src/services/album.service.ts
similarity index 93%
rename from server/src/domain/album/album.service.ts
rename to server/src/services/album.service.ts
index 4d459ea44..483ddc3b0 100644
--- a/server/src/domain/album/album.service.ts
+++ b/server/src/services/album.service.ts
@@ -1,27 +1,27 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
import {
+ AddUsersDto,
AlbumCountResponseDto,
+ AlbumInfoDto,
AlbumResponseDto,
+ CreateAlbumDto,
+ GetAlbumsDto,
+ UpdateAlbumDto,
mapAlbum,
mapAlbumWithAssets,
mapAlbumWithoutAssets,
-} from 'src/domain/album/album-response.dto';
-import { AddUsersDto } from 'src/domain/album/dto/album-add-users.dto';
-import { CreateAlbumDto } from 'src/domain/album/dto/album-create.dto';
-import { UpdateAlbumDto } from 'src/domain/album/dto/album-update.dto';
-import { AlbumInfoDto } from 'src/domain/album/dto/album.dto';
-import { GetAlbumsDto } from 'src/domain/album/dto/get-albums.dto';
-import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+} from 'src/dtos/album.dto';
+import { BulkIdErrorReason, BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { setUnion } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { setUnion } from 'src/utils/set';
@Injectable()
export class AlbumService {
diff --git a/server/src/domain/api-key/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts
similarity index 94%
rename from server/src/domain/api-key/api-key.service.spec.ts
rename to server/src/services/api-key.service.spec.ts
index 0400c13a2..3c8463c8f 100644
--- a/server/src/domain/api-key/api-key.service.spec.ts
+++ b/server/src/services/api-key.service.spec.ts
@@ -1,7 +1,7 @@
import { BadRequestException } from '@nestjs/common';
-import { APIKeyService } from 'src/domain/api-key/api-key.service';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { APIKeyService } from 'src/services/api-key.service';
import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { newKeyRepositoryMock } from 'test/repositories/api-key.repository.mock';
diff --git a/server/src/domain/api-key/api-key.service.ts b/server/src/services/api-key.service.ts
similarity index 89%
rename from server/src/domain/api-key/api-key.service.ts
rename to server/src/services/api-key.service.ts
index 540c1d71f..5de908b4d 100644
--- a/server/src/domain/api-key/api-key.service.ts
+++ b/server/src/services/api-key.service.ts
@@ -1,9 +1,9 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/domain/api-key/api-key.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { APIKeyEntity } from 'src/entities/api-key.entity';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@Injectable()
export class APIKeyService {
diff --git a/server/src/apps/api.service.ts b/server/src/services/api.service.ts
similarity index 83%
rename from server/src/apps/api.service.ts
rename to server/src/services/api.service.ts
index 31134cefa..87107b23f 100644
--- a/server/src/apps/api.service.ts
+++ b/server/src/services/api.service.ts
@@ -3,16 +3,16 @@ import { Cron, CronExpression, Interval } from '@nestjs/schedule';
import { NextFunction, Request, Response } from 'express';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
-import { AuthService } from 'src/domain/auth/auth.service';
-import { DatabaseService } from 'src/domain/database/database.service';
-import { ONE_HOUR, WEB_ROOT } from 'src/domain/domain.constant';
-import { JobService } from 'src/domain/job/job.service';
-import { ServerInfoService } from 'src/domain/server-info/server-info.service';
-import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
-import { StorageService } from 'src/domain/storage/storage.service';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
-import { ImmichLogger } from 'src/infra/logger';
-import { OpenGraphTags } from 'src/utils';
+import { ONE_HOUR, WEB_ROOT } from 'src/constants';
+import { AuthService } from 'src/services/auth.service';
+import { DatabaseService } from 'src/services/database.service';
+import { JobService } from 'src/services/job.service';
+import { ServerInfoService } from 'src/services/server-info.service';
+import { SharedLinkService } from 'src/services/shared-link.service';
+import { StorageService } from 'src/services/storage.service';
+import { SystemConfigService } from 'src/services/system-config.service';
+import { ImmichLogger } from 'src/utils/logger';
+import { OpenGraphTags } from 'src/utils/misc';
const render = (index: string, meta: OpenGraphTags) => {
const tags = `
diff --git a/server/src/immich/api-v1/asset/asset.service.spec.ts b/server/src/services/asset-v1.service.spec.ts
similarity index 90%
rename from server/src/immich/api-v1/asset/asset.service.spec.ts
rename to server/src/services/asset-v1.service.spec.ts
index d10590475..898fb5a99 100644
--- a/server/src/immich/api-v1/asset/asset.service.spec.ts
+++ b/server/src/services/asset-v1.service.spec.ts
@@ -1,16 +1,15 @@
import { when } from 'jest-when';
-import { JobName } from 'src/domain/job/job.constants';
+import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
+import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
-import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
-import { AssetService } from 'src/immich/api-v1/asset/asset.service';
-import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
-import { AssetRejectReason, AssetUploadAction } from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AssetServiceV1 } from 'src/services/asset-v1.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { fileStub } from 'test/fixtures/file.stub';
@@ -61,7 +60,7 @@ const _getAsset_1 = () => {
};
describe('AssetService', () => {
- let sut: AssetService;
+ let sut: AssetServiceV1;
let accessMock: IAccessRepositoryMock;
let assetRepositoryMockV1: jest.Mocked;
let assetMock: jest.Mocked;
@@ -89,7 +88,7 @@ describe('AssetService', () => {
storageMock = newStorageRepositoryMock();
userMock = newUserRepositoryMock();
- sut = new AssetService(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
+ sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
when(assetRepositoryMockV1.get)
.calledWith(assetStub.livePhotoStillAsset.id)
diff --git a/server/src/immich/api-v1/asset/asset.service.ts b/server/src/services/asset-v1.service.ts
similarity index 84%
rename from server/src/immich/api-v1/asset/asset.service.ts
rename to server/src/services/asset-v1.service.ts
index 0a5722690..a24ddbd69 100644
--- a/server/src/immich/api-v1/asset/asset.service.ts
+++ b/server/src/services/asset-v1.service.ts
@@ -6,42 +6,45 @@ import {
NotFoundException,
} from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
-import { UploadFile } from 'src/domain/asset/asset.service';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { JobName } from 'src/domain/job/job.constants';
-import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
-import { LibraryType } from 'src/entities/library.entity';
-import { IAssetRepositoryV1 } from 'src/immich/api-v1/asset/asset-repository';
-import { AssetBulkUploadCheckDto } from 'src/immich/api-v1/asset/dto/asset-check.dto';
-import { AssetSearchDto } from 'src/immich/api-v1/asset/dto/asset-search.dto';
-import { CheckExistingAssetsDto } from 'src/immich/api-v1/asset/dto/check-existing-assets.dto';
-import { CreateAssetDto } from 'src/immich/api-v1/asset/dto/create-asset.dto';
-import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from 'src/immich/api-v1/asset/dto/get-asset-thumbnail.dto';
-import { ServeFileDto } from 'src/immich/api-v1/asset/dto/serve-file.dto';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
import {
AssetBulkUploadCheckResponseDto,
+ AssetFileUploadResponseDto,
AssetRejectReason,
AssetUploadAction,
-} from 'src/immich/api-v1/asset/response-dto/asset-check-response.dto';
-import { AssetFileUploadResponseDto } from 'src/immich/api-v1/asset/response-dto/asset-file-upload-response.dto';
-import { CheckExistingAssetsResponseDto } from 'src/immich/api-v1/asset/response-dto/check-existing-assets-response.dto';
-import { CuratedLocationsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-locations-response.dto';
-import { CuratedObjectsResponseDto } from 'src/immich/api-v1/asset/response-dto/curated-objects-response.dto';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils';
+ CheckExistingAssetsResponseDto,
+ CuratedLocationsResponseDto,
+ CuratedObjectsResponseDto,
+} from 'src/dtos/asset-v1-response.dto';
+import {
+ AssetBulkUploadCheckDto,
+ AssetSearchDto,
+ CheckExistingAssetsDto,
+ CreateAssetDto,
+ GetAssetThumbnailDto,
+ GetAssetThumbnailFormatEnum,
+ ServeFileDto,
+} from 'src/dtos/asset-v1.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
+import { LibraryType } from 'src/entities/library.entity';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { UploadFile } from 'src/services/asset.service';
+import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
import { QueryFailedError } from 'typeorm';
@Injectable()
-export class AssetService {
- readonly logger = new ImmichLogger(AssetService.name);
+/** @deprecated */
+export class AssetServiceV1 {
+ readonly logger = new ImmichLogger(AssetServiceV1.name);
private access: AccessCore;
constructor(
diff --git a/server/src/domain/asset/asset.service.spec.ts b/server/src/services/asset.service.spec.ts
similarity index 97%
rename from server/src/domain/asset/asset.service.spec.ts
rename to server/src/services/asset.service.spec.ts
index 7b49cfef9..a8e30a388 100644
--- a/server/src/domain/asset/asset.service.spec.ts
+++ b/server/src/services/asset.service.spec.ts
@@ -1,19 +1,17 @@
import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { when } from 'jest-when';
-import { AssetService, UploadFieldName } from 'src/domain/asset/asset.service';
-import { AssetJobName } from 'src/domain/asset/dto/asset-ids.dto';
-import { AssetStatsResponseDto } from 'src/domain/asset/dto/asset-statistics.dto';
-import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { JobName } from 'src/domain/job/job.constants';
+import { mapAsset } from 'src/dtos/asset-response.dto';
+import { AssetJobName, AssetStatsResponseDto, UploadFieldName } from 'src/dtos/asset.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
-import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository';
-import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository, JobItem } from 'src/interfaces/job.repository';
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
+import { AssetStats, IAssetRepository, TimeBucketSize } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { IJobRepository, JobItem, JobName } from 'src/interfaces/job.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AssetService } from 'src/services/asset.service';
import { assetStackStub, assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';
diff --git a/server/src/domain/asset/asset.service.ts b/server/src/services/asset.service.ts
similarity index 91%
rename from server/src/domain/asset/asset.service.ts
rename to server/src/services/asset.service.ts
index 38446b576..7020d5061 100644
--- a/server/src/domain/asset/asset.service.ts
+++ b/server/src/services/asset.service.ts
@@ -6,45 +6,48 @@ import sanitize from 'sanitize-filename';
import { AccessCore, Permission } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { AssetJobName, AssetJobsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { UpdateStackParentDto } from 'src/domain/asset/dto/asset-stack.dto';
-import { AssetStatsDto, mapStats } from 'src/domain/asset/dto/asset-statistics.dto';
-import { AssetBulkDeleteDto, AssetBulkUpdateDto, UpdateAssetDto } from 'src/domain/asset/dto/asset.dto';
-import { MapMarkerDto } from 'src/domain/asset/dto/map-marker.dto';
-import { MemoryLaneDto } from 'src/domain/asset/dto/memory-lane.dto';
-import { TimeBucketAssetDto, TimeBucketDto } from 'src/domain/asset/dto/time-bucket.dto';
import {
AssetResponseDto,
MemoryLaneResponseDto,
SanitizedAssetResponseDto,
mapAsset,
-} from 'src/domain/asset/response-dto/asset-response.dto';
-import { MapMarkerResponseDto } from 'src/domain/asset/response-dto/map-marker-response.dto';
-import { TimeBucketResponseDto } from 'src/domain/asset/response-dto/time-bucket-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
-import { IAssetDeletionJob, ISidecarWriteJob } from 'src/domain/job/job.interface';
+} from 'src/dtos/asset-response.dto';
+import {
+ AssetBulkDeleteDto,
+ AssetBulkUpdateDto,
+ AssetJobName,
+ AssetJobsDto,
+ AssetStatsDto,
+ UpdateAssetDto,
+ UploadFieldName,
+ mapStats,
+} from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { MapMarkerDto, MapMarkerResponseDto, MemoryLaneDto } from 'src/dtos/search.dto';
+import { UpdateStackParentDto } from 'src/dtos/stack.dto';
+import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { LibraryType } from 'src/entities/library.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository';
-import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository';
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { usePagination } from 'src/utils';
-
-export enum UploadFieldName {
- ASSET_DATA = 'assetData',
- LIVE_PHOTO_DATA = 'livePhotoData',
- SIDECAR_DATA = 'sidecarData',
- PROFILE_DATA = 'file',
-}
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
+import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import {
+ IAssetDeletionJob,
+ IJobRepository,
+ ISidecarWriteJob,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobItem,
+ JobName,
+ JobStatus,
+} from 'src/interfaces/job.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
+import { usePagination } from 'src/utils/pagination';
export interface UploadRequest {
auth: AuthDto | null;
diff --git a/server/src/domain/audit/audit.service.spec.ts b/server/src/services/audit.service.spec.ts
similarity index 85%
rename from server/src/domain/audit/audit.service.spec.ts
rename to server/src/services/audit.service.spec.ts
index ecda57b08..4af5c1f94 100644
--- a/server/src/domain/audit/audit.service.spec.ts
+++ b/server/src/services/audit.service.spec.ts
@@ -1,12 +1,12 @@
-import { AuditService } from 'src/domain/audit/audit.service';
import { DatabaseAction, EntityType } from 'src/entities/audit.entity';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IAuditRepository } from 'src/interfaces/audit.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { JobStatus } from 'src/interfaces/job.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { JobStatus } from 'src/interfaces/job.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AuditService } from 'src/services/audit.service';
import { auditStub } from 'test/fixtures/audit.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
diff --git a/server/src/domain/audit/audit.service.ts b/server/src/services/audit.service.ts
similarity index 90%
rename from server/src/domain/audit/audit.service.ts
rename to server/src/services/audit.service.ts
index 91acb54d9..ff5e0d9c7 100644
--- a/server/src/domain/audit/audit.service.ts
+++ b/server/src/services/audit.service.ts
@@ -1,6 +1,7 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { resolve } from 'node:path';
+import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import {
@@ -10,22 +11,20 @@ import {
FileChecksumResponseDto,
FileReportItemDto,
PathEntityType,
-} from 'src/domain/audit/audit.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { AUDIT_LOG_MAX_DURATION } from 'src/domain/domain.constant';
-import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants';
+} from 'src/dtos/audit.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { DatabaseAction } from 'src/entities/audit.entity';
import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IAuditRepository } from 'src/interfaces/audit.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { JobStatus } from 'src/interfaces/job.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { usePagination } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IAuditRepository } from 'src/interfaces/audit.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { usePagination } from 'src/utils/pagination';
@Injectable()
export class AuditService {
diff --git a/server/src/domain/auth/auth.service.spec.ts b/server/src/services/auth.service.spec.ts
similarity index 97%
rename from server/src/domain/auth/auth.service.spec.ts
rename to server/src/services/auth.service.spec.ts
index e091cdcd9..30773f3f1 100644
--- a/server/src/domain/auth/auth.service.spec.ts
+++ b/server/src/services/auth.service.spec.ts
@@ -2,17 +2,17 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common';
import { IncomingHttpHeaders } from 'node:http';
import { Issuer, generators } from 'openid-client';
import { Socket } from 'socket.io';
-import { AuthType } from 'src/domain/auth/auth.constant';
-import { AuthDto, SignUpDto } from 'src/domain/auth/auth.dto';
-import { AuthService } from 'src/domain/auth/auth.service';
+import { AuthType } from 'src/constants';
+import { AuthDto, SignUpDto } from 'src/dtos/auth.dto';
import { UserEntity } from 'src/entities/user.entity';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { AuthService } from 'src/services/auth.service';
import { keyStub } from 'test/fixtures/api-key.stub';
import { authStub, loginResponseStub } from 'test/fixtures/auth.stub';
import { sharedLinkStub } from 'test/fixtures/shared-link.stub';
diff --git a/server/src/domain/auth/auth.service.ts b/server/src/services/auth.service.ts
similarity index 96%
rename from server/src/domain/auth/auth.service.ts
rename to server/src/services/auth.service.ts
index 2d807a249..8563d8353 100644
--- a/server/src/domain/auth/auth.service.ts
+++ b/server/src/services/auth.service.ts
@@ -10,9 +10,6 @@ import cookieParser from 'cookie';
import { DateTime } from 'luxon';
import { IncomingHttpHeaders } from 'node:http';
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
-import { AccessCore, Permission } from 'src/cores/access.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { UserCore } from 'src/cores/user.core';
import {
AuthType,
IMMICH_ACCESS_COOKIE,
@@ -21,7 +18,10 @@ import {
IMMICH_IS_AUTHENTICATED,
LOGIN_URL,
MOBILE_REDIRECT,
-} from 'src/domain/auth/auth.constant';
+} from 'src/constants';
+import { AccessCore, Permission } from 'src/cores/access.core';
+import { SystemConfigCore } from 'src/cores/system-config.core';
+import { UserCore } from 'src/cores/user.core';
import {
AuthDeviceResponseDto,
AuthDto,
@@ -35,20 +35,20 @@ import {
SignUpDto,
mapLoginResponse,
mapUserToken,
-} from 'src/domain/auth/auth.dto';
-import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
+} from 'src/dtos/auth.dto';
+import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { SystemConfig } from 'src/entities/system-config.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { HumanReadableSize } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { HumanReadableSize } from 'src/utils/bytes';
+import { ImmichLogger } from 'src/utils/logger';
export interface LoginDetails {
isSecure: boolean;
diff --git a/server/src/domain/database/database.service.spec.ts b/server/src/services/database.service.spec.ts
similarity index 97%
rename from server/src/domain/database/database.service.spec.ts
rename to server/src/services/database.service.spec.ts
index 191c0221b..6fa5e7fd8 100644
--- a/server/src/domain/database/database.service.spec.ts
+++ b/server/src/services/database.service.spec.ts
@@ -1,7 +1,7 @@
-import { DatabaseService } from 'src/domain/database/database.service';
-import { Version, VersionType } from 'src/domain/domain.constant';
-import { ImmichLogger } from 'src/infra/logger';
-import { DatabaseExtension, IDatabaseRepository, VectorIndex } from 'src/interfaces/database.repository';
+import { DatabaseExtension, IDatabaseRepository, VectorIndex } from 'src/interfaces/database.interface';
+import { DatabaseService } from 'src/services/database.service';
+import { ImmichLogger } from 'src/utils/logger';
+import { Version, VersionType } from 'src/utils/version';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
describe(DatabaseService.name, () => {
diff --git a/server/src/domain/database/database.service.ts b/server/src/services/database.service.ts
similarity index 97%
rename from server/src/domain/database/database.service.ts
rename to server/src/services/database.service.ts
index 7bb16a4e1..a333c0053 100644
--- a/server/src/domain/database/database.service.ts
+++ b/server/src/services/database.service.ts
@@ -1,6 +1,4 @@
import { Inject, Injectable } from '@nestjs/common';
-import { Version, VersionType } from 'src/domain/domain.constant';
-import { ImmichLogger } from 'src/infra/logger';
import {
DatabaseExtension,
DatabaseLock,
@@ -8,7 +6,9 @@ import {
VectorExtension,
VectorIndex,
extName,
-} from 'src/interfaces/database.repository';
+} from 'src/interfaces/database.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { Version, VersionType } from 'src/utils/version';
@Injectable()
export class DatabaseService {
diff --git a/server/src/domain/download/download.service.spec.ts b/server/src/services/download.service.spec.ts
similarity index 96%
rename from server/src/domain/download/download.service.spec.ts
rename to server/src/services/download.service.spec.ts
index 6e1eafbec..babc21fa8 100644
--- a/server/src/domain/download/download.service.spec.ts
+++ b/server/src/services/download.service.spec.ts
@@ -1,10 +1,10 @@
import { BadRequestException } from '@nestjs/common';
import { when } from 'jest-when';
-import { DownloadResponseDto } from 'src/domain/download/download.dto';
-import { DownloadService } from 'src/domain/download/download.service';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { CacheControl, ImmichFileResponse } from 'src/utils';
+import { DownloadResponseDto } from 'src/dtos/download.dto';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { DownloadService } from 'src/services/download.service';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
diff --git a/server/src/domain/download/download.service.ts b/server/src/services/download.service.ts
similarity index 90%
rename from server/src/domain/download/download.service.ts
rename to server/src/services/download.service.ts
index 8c0168a6c..b0b68a1e8 100644
--- a/server/src/domain/download/download.service.ts
+++ b/server/src/services/download.service.ts
@@ -1,15 +1,17 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { parse } from 'node:path';
import { AccessCore, Permission } from 'src/cores/access.core';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/domain/download/download.dto';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
import { AssetEntity } from 'src/entities/asset.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.repository';
-import { CacheControl, HumanReadableSize, ImmichFileResponse, usePagination } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface';
+import { HumanReadableSize } from 'src/utils/bytes';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { mimeTypes } from 'src/utils/mime-types';
+import { usePagination } from 'src/utils/pagination';
@Injectable()
export class DownloadService {
diff --git a/server/src/domain/job/job.service.spec.ts b/server/src/services/job.service.spec.ts
similarity index 97%
rename from server/src/domain/job/job.service.spec.ts
rename to server/src/services/job.service.spec.ts
index ec905b062..b680abaf1 100644
--- a/server/src/domain/job/job.service.spec.ts
+++ b/server/src/services/job.service.spec.ts
@@ -1,13 +1,20 @@
import { BadRequestException } from '@nestjs/common';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
-import { JobCommand, JobName, QueueName } from 'src/domain/job/job.constants';
-import { JobService } from 'src/domain/job/job.service';
import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository, JobHandler, JobItem, JobStatus } from 'src/interfaces/job.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ICommunicationRepository } from 'src/interfaces/communication.interface';
+import {
+ IJobRepository,
+ JobCommand,
+ JobHandler,
+ JobItem,
+ JobName,
+ JobStatus,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { JobService } from 'src/services/job.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock';
diff --git a/server/src/domain/job/job.service.ts b/server/src/services/job.service.ts
similarity index 94%
rename from server/src/domain/job/job.service.ts
rename to server/src/services/job.service.ts
index 192b093c1..e6d9b0781 100644
--- a/server/src/domain/job/job.service.ts
+++ b/server/src/services/job.service.ts
@@ -1,15 +1,24 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
-import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { ConcurrentQueueName, JobCommand, JobName, QueueName } from 'src/domain/job/job.constants';
-import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/domain/job/job.dto';
+import { mapAsset } from 'src/dtos/asset-response.dto';
+import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType } from 'src/entities/asset.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository, JobHandler, JobItem, JobStatus, QueueCleanType } from 'src/interfaces/job.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import {
+ ConcurrentQueueName,
+ IJobRepository,
+ JobCommand,
+ JobHandler,
+ JobItem,
+ JobName,
+ JobStatus,
+ QueueCleanType,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
@Injectable()
export class JobService {
diff --git a/server/src/domain/library/library.service.spec.ts b/server/src/services/library.service.spec.ts
similarity index 98%
rename from server/src/domain/library/library.service.spec.ts
rename to server/src/services/library.service.spec.ts
index 3a8440fda..d00fef3db 100644
--- a/server/src/domain/library/library.service.spec.ts
+++ b/server/src/services/library.service.spec.ts
@@ -3,21 +3,20 @@ import { when } from 'jest-when';
import { R_OK } from 'node:constants';
import { Stats } from 'node:fs';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { JobName } from 'src/domain/job/job.constants';
-import { ILibraryFileJob, ILibraryOfflineJob, ILibraryRefreshJob } from 'src/domain/job/job.interface';
-import { mapLibrary } from 'src/domain/library/library.dto';
-import { LibraryService } from 'src/domain/library/library.service';
+import { ILibraryOfflineJob } from 'src/domain/job/job.interface';
+import { mapLibrary } from 'src/dtos/library.dto';
import { AssetType } from 'src/entities/asset.entity';
import { LibraryType } from 'src/entities/library.entity';
import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity';
import { UserEntity } from 'src/entities/user.entity';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { LibraryService } from 'src/services/library.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { libraryStub } from 'test/fixtures/library.stub';
diff --git a/server/src/domain/library/library.service.ts b/server/src/services/library.service.ts
similarity index 96%
rename from server/src/domain/library/library.service.ts
rename to server/src/services/library.service.ts
index 654021947..536d26392 100644
--- a/server/src/domain/library/library.service.ts
+++ b/server/src/services/library.service.ts
@@ -1,5 +1,4 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { OnEvent } from '@nestjs/event-emitter';
import { R_OK } from 'node:constants';
import { EventEmitter } from 'node:events';
import { Stats } from 'node:fs';
@@ -7,15 +6,8 @@ import path, { basename, parse } from 'node:path';
import picomatch from 'picomatch';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
-import {
- IBaseJob,
- IEntityJob,
- ILibraryFileJob,
- ILibraryOfflineJob,
- ILibraryRefreshJob,
-} from 'src/domain/job/job.interface';
+import { OnEventInternal } from 'src/decorators';
+import { ILibraryOfflineJob } from 'src/domain/job/job.interface';
import {
CreateLibraryDto,
LibraryResponseDto,
@@ -27,19 +19,30 @@ import {
ValidateLibraryImportPathResponseDto,
ValidateLibraryResponseDto,
mapLibrary,
-} from 'src/domain/library/library.dto';
+} from 'src/dtos/library.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
-import { LibraryType } from 'src/entities/library.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAssetRepository, WithProperty } from 'src/interfaces/asset.repository';
-import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { handlePromiseError, usePagination } from 'src/utils';
+import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
+import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface';
+import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import {
+ IBaseJob,
+ IEntityJob,
+ IJobRepository,
+ ILibraryFileJob,
+ ILibraryRefreshJob,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobName,
+ JobStatus,
+} from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
+import { handlePromiseError } from 'src/utils/misc';
+import { usePagination } from 'src/utils/pagination';
import { validateCronExpression } from 'src/validation';
const LIBRARY_SCAN_BATCH_SIZE = 1000;
@@ -102,7 +105,7 @@ export class LibraryService extends EventEmitter {
});
}
- @OnEvent(InternalEvent.VALIDATE_CONFIG)
+ @OnEventInternal(InternalEvent.VALIDATE_CONFIG)
validateConfig({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) {
const { scan } = newConfig.library;
if (!validateCronExpression(scan.cronExpression)) {
diff --git a/server/src/domain/media/media.service.spec.ts b/server/src/services/media.service.spec.ts
similarity index 99%
rename from server/src/domain/media/media.service.spec.ts
rename to server/src/services/media.service.spec.ts
index 97a6d161b..4397730ab 100644
--- a/server/src/domain/media/media.service.spec.ts
+++ b/server/src/services/media.service.spec.ts
@@ -1,6 +1,4 @@
import { Stats } from 'node:fs';
-import { JobName } from 'src/domain/job/job.constants';
-import { MediaService } from 'src/domain/media/media.service';
import { AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import {
@@ -12,14 +10,15 @@ import {
TranscodePolicy,
VideoCodec,
} from 'src/entities/system-config.entity';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { MediaService } from 'src/services/media.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { faceStub } from 'test/fixtures/face.stub';
import { probeStub } from 'test/fixtures/media.stub';
diff --git a/server/src/domain/media/media.service.ts b/server/src/services/media.service.ts
similarity index 95%
rename from server/src/domain/media/media.service.ts
rename to server/src/services/media.service.ts
index c254689b4..26aa2dce9 100644
--- a/server/src/domain/media/media.service.ts
+++ b/server/src/services/media.service.ts
@@ -1,19 +1,7 @@
import { Inject, Injectable, UnsupportedMediaTypeException } from '@nestjs/common';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
-import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface';
-import {
- H264Config,
- HEVCConfig,
- NVENCConfig,
- QSVConfig,
- RKMPPConfig,
- ThumbnailConfig,
- VAAPIConfig,
- VP9Config,
-} from 'src/domain/media/media.util';
-import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto';
+import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/entities/move.entity';
import {
@@ -24,21 +12,35 @@ import {
TranscodeTarget,
VideoCodec,
} from 'src/entities/system-config.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import {
- AudioStreamInfo,
- IMediaRepository,
- VideoCodecHWConfig,
- VideoStreamInfo,
-} from 'src/interfaces/media.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { usePagination } from 'src/utils';
+ IBaseJob,
+ IEntityJob,
+ IJobRepository,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobItem,
+ JobName,
+ JobStatus,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { AudioStreamInfo, IMediaRepository, VideoCodecHWConfig, VideoStreamInfo } from 'src/interfaces/media.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import {
+ H264Config,
+ HEVCConfig,
+ NVENCConfig,
+ QSVConfig,
+ RKMPPConfig,
+ ThumbnailConfig,
+ VAAPIConfig,
+ VP9Config,
+} from 'src/utils/media';
+import { usePagination } from 'src/utils/pagination';
@Injectable()
export class MediaService {
diff --git a/server/src/domain/metadata/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts
similarity index 97%
rename from server/src/domain/metadata/metadata.service.spec.ts
rename to server/src/services/metadata.service.spec.ts
index 2108c5fc4..4dafa0ac5 100644
--- a/server/src/domain/metadata/metadata.service.spec.ts
+++ b/server/src/services/metadata.service.spec.ts
@@ -3,23 +3,22 @@ import { when } from 'jest-when';
import { randomBytes } from 'node:crypto';
import { Stats } from 'node:fs';
import { constants } from 'node:fs/promises';
-import { JobName } from 'src/domain/job/job.constants';
-import { MetadataService, Orientation } from 'src/domain/metadata/metadata.service';
import { AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
import { SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { MetadataService, Orientation } from 'src/services/metadata.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { fileStub } from 'test/fixtures/file.stub';
import { probeStub } from 'test/fixtures/media.stub';
diff --git a/server/src/domain/metadata/metadata.service.ts b/server/src/services/metadata.service.ts
similarity index 95%
rename from server/src/domain/metadata/metadata.service.ts
rename to server/src/services/metadata.service.ts
index 0e949626c..849a12da9 100644
--- a/server/src/domain/metadata/metadata.service.ts
+++ b/server/src/services/metadata.service.ts
@@ -8,24 +8,32 @@ import path from 'node:path';
import { Subscription } from 'rxjs';
import { StorageCore } from 'src/cores/storage.core';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
-import { IBaseJob, IEntityJob, ISidecarWriteJob } from 'src/domain/job/job.interface';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { handlePromiseError, usePagination } from 'src/utils';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import {
+ IBaseJob,
+ IEntityJob,
+ IJobRepository,
+ ISidecarWriteJob,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobName,
+ JobStatus,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMetadataRepository, ImmichTags } from 'src/interfaces/metadata.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { handlePromiseError } from 'src/utils/misc';
+import { usePagination } from 'src/utils/pagination';
/** look for a date from these tags (in order) */
const EXIF_DATE_TAGS: Array = [
diff --git a/server/src/apps/microservices.service.ts b/server/src/services/microservices.service.ts
similarity index 81%
rename from server/src/apps/microservices.service.ts
rename to server/src/services/microservices.service.ts
index cfcd9cae5..07ef3f35f 100644
--- a/server/src/apps/microservices.service.ts
+++ b/server/src/services/microservices.service.ts
@@ -1,20 +1,19 @@
import { Injectable } from '@nestjs/common';
-import { AssetService } from 'src/domain/asset/asset.service';
-import { AuditService } from 'src/domain/audit/audit.service';
-import { DatabaseService } from 'src/domain/database/database.service';
-import { JobName } from 'src/domain/job/job.constants';
-import { IDeleteFilesJob } from 'src/domain/job/job.interface';
-import { JobService } from 'src/domain/job/job.service';
-import { LibraryService } from 'src/domain/library/library.service';
-import { MediaService } from 'src/domain/media/media.service';
-import { MetadataService } from 'src/domain/metadata/metadata.service';
-import { PersonService } from 'src/domain/person/person.service';
-import { SmartInfoService } from 'src/domain/smart-info/smart-info.service';
-import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service';
-import { StorageService } from 'src/domain/storage/storage.service';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
-import { UserService } from 'src/domain/user/user.service';
-import { otelSDK } from 'src/infra/instrumentation';
+import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
+import { AssetService } from 'src/services/asset.service';
+import { AuditService } from 'src/services/audit.service';
+import { DatabaseService } from 'src/services/database.service';
+import { JobService } from 'src/services/job.service';
+import { LibraryService } from 'src/services/library.service';
+import { MediaService } from 'src/services/media.service';
+import { MetadataService } from 'src/services/metadata.service';
+import { PersonService } from 'src/services/person.service';
+import { SmartInfoService } from 'src/services/smart-info.service';
+import { StorageTemplateService } from 'src/services/storage-template.service';
+import { StorageService } from 'src/services/storage.service';
+import { SystemConfigService } from 'src/services/system-config.service';
+import { UserService } from 'src/services/user.service';
+import { otelSDK } from 'src/utils/instrumentation';
@Injectable()
export class MicroservicesService {
diff --git a/server/src/domain/partner/partner.service.spec.ts b/server/src/services/partner.service.spec.ts
similarity index 94%
rename from server/src/domain/partner/partner.service.spec.ts
rename to server/src/services/partner.service.spec.ts
index 7d4821282..a3c4af736 100644
--- a/server/src/domain/partner/partner.service.spec.ts
+++ b/server/src/services/partner.service.spec.ts
@@ -1,9 +1,9 @@
import { BadRequestException } from '@nestjs/common';
-import { PartnerResponseDto } from 'src/domain/partner/partner.dto';
-import { PartnerService } from 'src/domain/partner/partner.service';
+import { PartnerResponseDto } from 'src/dtos/partner.dto';
import { UserAvatarColor } from 'src/entities/user.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.repository';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IPartnerRepository, PartnerDirection } from 'src/interfaces/partner.interface';
+import { PartnerService } from 'src/services/partner.service';
import { authStub } from 'test/fixtures/auth.stub';
import { partnerStub } from 'test/fixtures/partner.stub';
import { newPartnerRepositoryMock } from 'test/repositories/partner.repository.mock';
diff --git a/server/src/domain/partner/partner.service.ts b/server/src/services/partner.service.ts
similarity index 89%
rename from server/src/domain/partner/partner.service.ts
rename to server/src/services/partner.service.ts
index 34a88a7ea..14503cc7f 100644
--- a/server/src/domain/partner/partner.service.ts
+++ b/server/src/services/partner.service.ts
@@ -1,11 +1,11 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { PartnerResponseDto, UpdatePartnerDto } from 'src/domain/partner/partner.dto';
-import { mapUser } from 'src/domain/user/response-dto/user-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { PartnerResponseDto, UpdatePartnerDto } from 'src/dtos/partner.dto';
+import { mapUser } from 'src/dtos/user.dto';
import { PartnerEntity } from 'src/entities/partner.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.repository';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface';
@Injectable()
export class PartnerService {
diff --git a/server/src/domain/person/person.service.spec.ts b/server/src/services/person.service.spec.ts
similarity index 98%
rename from server/src/domain/person/person.service.spec.ts
rename to server/src/services/person.service.spec.ts
index 9da66e868..10e42e1b6 100644
--- a/server/src/domain/person/person.service.spec.ts
+++ b/server/src/services/person.service.spec.ts
@@ -1,21 +1,20 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
-import { BulkIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { JobName } from 'src/domain/job/job.constants';
-import { PersonResponseDto, mapFaces, mapPerson } from 'src/domain/person/person.dto';
-import { PersonService } from 'src/domain/person/person.service';
+import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
+import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { Colorspace, SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { CacheControl, ImmichFileResponse } from 'src/utils';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { FaceSearchResult, ISearchRepository } from 'src/interfaces/search.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { PersonService } from 'src/services/person.service';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { faceStub } from 'test/fixtures/face.stub';
diff --git a/server/src/domain/person/person.service.ts b/server/src/services/person.service.ts
similarity index 94%
rename from server/src/domain/person/person.service.ts
rename to server/src/services/person.service.ts
index cd7ba9224..504716a55 100644
--- a/server/src/domain/person/person.service.ts
+++ b/server/src/services/person.service.ts
@@ -1,14 +1,11 @@
import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common';
+import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { AccessCore, Permission } from 'src/cores/access.core';
import { StorageCore } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { BulkIdErrorReason, BulkIdResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { mimeTypes } from 'src/domain/domain.constant';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
-import { IBaseJob, IDeferrableJob, IEntityJob } from 'src/domain/job/job.interface';
-import { FACE_THUMBNAIL_SIZE } from 'src/domain/media/media.constant';
+import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import {
AssetFaceResponseDto,
AssetFaceUpdateDto,
@@ -23,22 +20,34 @@ import {
PersonUpdateDto,
mapFaces,
mapPerson,
-} from 'src/domain/person/person.dto';
+} from 'src/dtos/person.dto';
import { PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository, JobItem, JobStatus } from 'src/interfaces/job.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { CropOptions, IMediaRepository } from 'src/interfaces/media.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { CacheControl, ImmichFileResponse, usePagination } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import {
+ IBaseJob,
+ IDeferrableJob,
+ IEntityJob,
+ IJobRepository,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobItem,
+ JobName,
+ JobStatus,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { CropOptions, IMediaRepository } from 'src/interfaces/media.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
+import { usePagination } from 'src/utils/pagination';
import { IsNull } from 'typeorm';
@Injectable()
diff --git a/server/src/domain/search/search.service.spec.ts b/server/src/services/search.service.spec.ts
similarity index 92%
rename from server/src/domain/search/search.service.spec.ts
rename to server/src/services/search.service.spec.ts
index f72212809..72b543f2d 100644
--- a/server/src/domain/search/search.service.spec.ts
+++ b/server/src/services/search.service.spec.ts
@@ -1,14 +1,14 @@
-import { mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { SearchDto } from 'src/domain/search/dto/search.dto';
-import { SearchService } from 'src/domain/search/search.service';
+import { mapAsset } from 'src/dtos/asset-response.dto';
+import { SearchDto } from 'src/dtos/search.dto';
import { SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { IMetadataRepository } from 'src/interfaces/metadata.repository';
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { SearchService } from 'src/services/search.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { personStub } from 'test/fixtures/person.stub';
diff --git a/server/src/domain/search/search.service.ts b/server/src/services/search.service.ts
similarity index 90%
rename from server/src/domain/search/search.service.ts
rename to server/src/services/search.service.ts
index cedee68d0..03fa154a3 100644
--- a/server/src/domain/search/search.service.ts
+++ b/server/src/services/search.service.ts
@@ -1,28 +1,29 @@
import { Inject, Injectable } from '@nestjs/common';
import { FeatureFlag, SystemConfigCore } from 'src/cores/system-config.core';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { PersonResponseDto } from 'src/domain/person/person.dto';
-import { SearchSuggestionRequestDto, SearchSuggestionType } from 'src/domain/search/dto/search-suggestion.dto';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { PersonResponseDto } from 'src/dtos/person.dto';
import {
MetadataSearchDto,
PlacesResponseDto,
SearchDto,
SearchPeopleDto,
SearchPlacesDto,
+ SearchResponseDto,
+ SearchSuggestionRequestDto,
+ SearchSuggestionType,
SmartSearchDto,
mapPlaces,
-} from 'src/domain/search/dto/search.dto';
-import { SearchResponseDto } from 'src/domain/search/response-dto/search-response.dto';
+} from 'src/dtos/search.dto';
import { AssetOrder } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { IMetadataRepository } from 'src/interfaces/metadata.repository';
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { ISearchRepository, SearchExploreItem, SearchStrategy } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
@Injectable()
export class SearchService {
diff --git a/server/src/domain/server-info/server-info.service.spec.ts b/server/src/services/server-info.service.spec.ts
similarity index 95%
rename from server/src/domain/server-info/server-info.service.spec.ts
rename to server/src/services/server-info.service.spec.ts
index 1685f8bda..bbb608b21 100644
--- a/server/src/domain/server-info/server-info.service.spec.ts
+++ b/server/src/services/server-info.service.spec.ts
@@ -1,12 +1,12 @@
-import { serverVersion } from 'src/domain/domain.constant';
-import { ServerInfoService } from 'src/domain/server-info/server-info.service';
+import { serverVersion } from 'src/constants';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
-import { ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IServerInfoRepository } from 'src/interfaces/server-info.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { ServerInfoService } from 'src/services/server-info.service';
import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
diff --git a/server/src/domain/server-info/server-info.service.ts b/server/src/services/server-info.service.ts
similarity index 93%
rename from server/src/domain/server-info/server-info.service.ts
rename to server/src/services/server-info.service.ts
index 13921949c..94195fd4a 100644
--- a/server/src/domain/server-info/server-info.service.ts
+++ b/server/src/services/server-info.service.ts
@@ -1,8 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
+import { isDev, serverVersion } from 'src/constants';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { Version, isDev, mimeTypes, serverVersion } from 'src/domain/domain.constant';
import {
ServerConfigDto,
ServerFeaturesDto,
@@ -11,16 +11,18 @@ import {
ServerPingResponse,
ServerStatsResponseDto,
UsageByUserDto,
-} from 'src/domain/server-info/server-info.dto';
+} from 'src/dtos/server-info.dto';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IServerInfoRepository } from 'src/interfaces/server-info.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
-import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.repository';
-import { asHumanReadable } from 'src/utils';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
+import { IUserRepository, UserStatsQueryResponse } from 'src/interfaces/user.interface';
+import { asHumanReadable } from 'src/utils/bytes';
+import { ImmichLogger } from 'src/utils/logger';
+import { mimeTypes } from 'src/utils/mime-types';
+import { Version } from 'src/utils/version';
@Injectable()
export class ServerInfoService {
diff --git a/server/src/domain/shared-link/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts
similarity index 97%
rename from server/src/domain/shared-link/shared-link.service.spec.ts
rename to server/src/services/shared-link.service.spec.ts
index 926ec16b1..cad52928c 100644
--- a/server/src/domain/shared-link/shared-link.service.spec.ts
+++ b/server/src/services/shared-link.service.spec.ts
@@ -1,10 +1,10 @@
import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common';
import _ from 'lodash';
-import { AssetIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { SharedLinkService } from 'src/domain/shared-link/shared-link.service';
+import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { SharedLinkType } from 'src/entities/shared-link.entity';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { SharedLinkService } from 'src/services/shared-link.service';
import { albumStub } from 'test/fixtures/album.stub';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
diff --git a/server/src/domain/shared-link/shared-link.service.ts b/server/src/services/shared-link.service.ts
similarity index 92%
rename from server/src/domain/shared-link/shared-link.service.ts
rename to server/src/services/shared-link.service.ts
index e01deee7b..cea0e8414 100644
--- a/server/src/domain/shared-link/shared-link.service.ts
+++ b/server/src/services/shared-link.service.ts
@@ -1,20 +1,22 @@
import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common';
import { AccessCore, Permission } from 'src/cores/access.core';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import {
+ SharedLinkCreateDto,
+ SharedLinkEditDto,
+ SharedLinkPasswordDto,
SharedLinkResponseDto,
mapSharedLink,
mapSharedLinkWithoutMetadata,
-} from 'src/domain/shared-link/shared-link-response.dto';
-import { SharedLinkCreateDto, SharedLinkEditDto, SharedLinkPasswordDto } from 'src/domain/shared-link/shared-link.dto';
+} from 'src/dtos/shared-link.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
-import { OpenGraphTags } from 'src/utils';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
+import { OpenGraphTags } from 'src/utils/misc';
@Injectable()
export class SharedLinkService {
diff --git a/server/src/domain/smart-info/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts
similarity index 86%
rename from server/src/domain/smart-info/smart-info.service.spec.ts
rename to server/src/services/smart-info.service.spec.ts
index 29ce75fb4..81d7935c3 100644
--- a/server/src/domain/smart-info/smart-info.service.spec.ts
+++ b/server/src/services/smart-info.service.spec.ts
@@ -1,14 +1,13 @@
-import { JobName } from 'src/domain/job/job.constants';
-import { cleanModelName, getCLIPModelInfo } from 'src/domain/smart-info/smart-info.constant';
-import { SmartInfoService } from 'src/domain/smart-info/smart-info.service';
import { AssetEntity } from 'src/entities/asset.entity';
import { SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { SmartInfoService } from 'src/services/smart-info.service';
+import { getCLIPModelInfo } from 'src/utils/misc';
import { assetStub } from 'test/fixtures/asset.stub';
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock';
@@ -124,16 +123,14 @@ describe(SmartInfoService.name, () => {
});
});
- describe('cleanModelName', () => {
- it('should clean name', () => {
- expect(cleanModelName('ViT-B-32::openai')).toEqual('ViT-B-32__openai');
- expect(cleanModelName('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual('XLM-Roberta-Large-Vit-L-14');
- });
- });
-
describe('getCLIPModelInfo', () => {
it('should return the model info', () => {
expect(getCLIPModelInfo('ViT-B-32__openai')).toEqual({ dimSize: 512 });
+ expect(getCLIPModelInfo('M-CLIP/XLM-Roberta-Large-Vit-L-14')).toEqual({ dimSize: 768 });
+ });
+
+ it('should clean the model name', () => {
+ expect(getCLIPModelInfo('ViT-B-32::openai')).toEqual({ dimSize: 512 });
});
it('should throw an error if the model is not present', () => {
diff --git a/server/src/domain/smart-info/smart-info.service.ts b/server/src/services/smart-info.service.ts
similarity index 87%
rename from server/src/domain/smart-info/smart-info.service.ts
rename to server/src/services/smart-info.service.ts
index a7d470008..fb19e90a7 100644
--- a/server/src/domain/smart-info/smart-info.service.ts
+++ b/server/src/services/smart-info.service.ts
@@ -1,15 +1,21 @@
import { Inject, Injectable } from '@nestjs/common';
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from 'src/domain/job/job.constants';
-import { IBaseJob, IEntityJob } from 'src/domain/job/job.interface';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.repository';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { usePagination } from 'src/utils';
+import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
+import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import {
+ IBaseJob,
+ IEntityJob,
+ IJobRepository,
+ JOBS_ASSET_PAGINATION_SIZE,
+ JobName,
+ JobStatus,
+ QueueName,
+} from 'src/interfaces/job.interface';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { usePagination } from 'src/utils/pagination';
@Injectable()
export class SmartInfoService {
diff --git a/server/src/domain/storage-template/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts
similarity index 97%
rename from server/src/domain/storage-template/storage-template.service.spec.ts
rename to server/src/services/storage-template.service.spec.ts
index ecadb3e86..1254705ae 100644
--- a/server/src/domain/storage-template/storage-template.service.spec.ts
+++ b/server/src/services/storage-template.service.spec.ts
@@ -1,19 +1,19 @@
import { when } from 'jest-when';
import { Stats } from 'node:fs';
import { SystemConfigCore, defaults } from 'src/cores/system-config.core';
-import { StorageTemplateService } from 'src/domain/storage-template/storage-template.service';
import { AssetPathType } from 'src/entities/move.entity';
import { SystemConfig, SystemConfigKey } from 'src/entities/system-config.entity';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
-import { JobStatus } from 'src/interfaces/job.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { JobStatus } from 'src/interfaces/job.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { StorageTemplateService } from 'src/services/storage-template.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { userStub } from 'test/fixtures/user.stub';
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
diff --git a/server/src/domain/storage-template/storage-template.service.ts b/server/src/services/storage-template.service.ts
similarity index 91%
rename from server/src/domain/storage-template/storage-template.service.ts
rename to server/src/services/storage-template.service.ts
index b4af03a9f..39a0196f2 100644
--- a/server/src/domain/storage-template/storage-template.service.ts
+++ b/server/src/services/storage-template.service.ts
@@ -1,13 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
-import { OnEvent } from '@nestjs/event-emitter';
import handlebar from 'handlebars';
import { DateTime } from 'luxon';
import path from 'node:path';
import sanitize from 'sanitize-filename';
-import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { JOBS_ASSET_PAGINATION_SIZE } from 'src/domain/job/job.constants';
-import { IEntityJob } from 'src/domain/job/job.interface';
import {
supportedDayTokens,
supportedHourTokens,
@@ -16,23 +11,27 @@ import {
supportedSecondTokens,
supportedWeekTokens,
supportedYearTokens,
-} from 'src/domain/system-config/system-config.constants';
+} from 'src/constants';
+import { StorageCore, StorageFolder } from 'src/cores/storage.core';
+import { SystemConfigCore } from 'src/cores/system-config.core';
+import { OnEventInternal } from 'src/decorators';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { AssetPathType } from 'src/entities/move.entity';
import { SystemConfig } from 'src/entities/system-config.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.repository';
-import { JobStatus } from 'src/interfaces/job.repository';
-import { IMoveRepository } from 'src/interfaces/move.repository';
-import { IPersonRepository } from 'src/interfaces/person.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { getLivePhotoMotionFilename, usePagination } from 'src/utils';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { InternalEvent, InternalEventMap } from 'src/interfaces/communication.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface';
+import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
+import { IMoveRepository } from 'src/interfaces/move.interface';
+import { IPersonRepository } from 'src/interfaces/person.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { getLivePhotoMotionFilename } from 'src/utils/file';
+import { ImmichLogger } from 'src/utils/logger';
+import { usePagination } from 'src/utils/pagination';
export interface MoveAssetMetadata {
storageLabel: string | null;
@@ -87,7 +86,7 @@ export class StorageTemplateService {
);
}
- @OnEvent(InternalEvent.VALIDATE_CONFIG)
+ @OnEventInternal(InternalEvent.VALIDATE_CONFIG)
validate({ newConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) {
try {
const { compiled } = this.compile(newConfig.storageTemplate.template);
diff --git a/server/src/domain/storage/storage.service.spec.ts b/server/src/services/storage.service.spec.ts
similarity index 90%
rename from server/src/domain/storage/storage.service.spec.ts
rename to server/src/services/storage.service.spec.ts
index 2791c9630..977f632d5 100644
--- a/server/src/domain/storage/storage.service.spec.ts
+++ b/server/src/services/storage.service.spec.ts
@@ -1,5 +1,5 @@
-import { StorageService } from 'src/domain/storage/storage.service';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { StorageService } from 'src/services/storage.service';
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
describe(StorageService.name, () => {
diff --git a/server/src/domain/storage/storage.service.ts b/server/src/services/storage.service.ts
similarity index 77%
rename from server/src/domain/storage/storage.service.ts
rename to server/src/services/storage.service.ts
index 9df3dd778..81fdb4f41 100644
--- a/server/src/domain/storage/storage.service.ts
+++ b/server/src/services/storage.service.ts
@@ -1,9 +1,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
-import { IDeleteFilesJob } from 'src/domain/job/job.interface';
-import { ImmichLogger } from 'src/infra/logger';
-import { JobStatus } from 'src/interfaces/job.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
+import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ImmichLogger } from 'src/utils/logger';
@Injectable()
export class StorageService {
diff --git a/server/src/domain/system-config/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts
similarity index 97%
rename from server/src/domain/system-config/system-config.service.spec.ts
rename to server/src/services/system-config.service.spec.ts
index f81d726eb..122708a63 100644
--- a/server/src/domain/system-config/system-config.service.spec.ts
+++ b/server/src/services/system-config.service.spec.ts
@@ -1,7 +1,5 @@
import { BadRequestException } from '@nestjs/common';
import { defaults } from 'src/cores/system-config.core';
-import { QueueName } from 'src/domain/job/job.constants';
-import { SystemConfigService } from 'src/domain/system-config/system-config.service';
import {
AudioCodec,
CQMode,
@@ -15,10 +13,12 @@ import {
TranscodePolicy,
VideoCodec,
} from 'src/entities/system-config.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { ICommunicationRepository, ServerEvent } from 'src/interfaces/communication.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { ICommunicationRepository, ServerEvent } from 'src/interfaces/communication.interface';
+import { QueueName } from 'src/interfaces/job.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { SystemConfigService } from 'src/services/system-config.service';
+import { ImmichLogger } from 'src/utils/logger';
import { newCommunicationRepositoryMock } from 'test/repositories/communication.repository.mock';
import { newSystemConfigRepositoryMock } from 'test/repositories/system-config.repository.mock';
diff --git a/server/src/domain/system-config/system-config.service.ts b/server/src/services/system-config.service.ts
similarity index 89%
rename from server/src/domain/system-config/system-config.service.ts
rename to server/src/services/system-config.service.ts
index d69593d21..5842342db 100644
--- a/server/src/domain/system-config/system-config.service.ts
+++ b/server/src/services/system-config.service.ts
@@ -1,10 +1,6 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { OnEvent } from '@nestjs/event-emitter';
import { instanceToPlain } from 'class-transformer';
import _ from 'lodash';
-import { SystemConfigCore } from 'src/cores/system-config.core';
-import { SystemConfigDto, mapConfig } from 'src/domain/system-config/dto/system-config.dto';
-import { SystemConfigTemplateStorageOptionDto } from 'src/domain/system-config/response-dto/system-config-template-storage-option.dto';
import {
supportedDayTokens,
supportedHourTokens,
@@ -14,18 +10,21 @@ import {
supportedSecondTokens,
supportedWeekTokens,
supportedYearTokens,
-} from 'src/domain/system-config/system-config.constants';
+} from 'src/constants';
+import { SystemConfigCore } from 'src/cores/system-config.core';
+import { OnEventInternal } from 'src/decorators';
+import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto';
import { LogLevel, SystemConfig } from 'src/entities/system-config.entity';
-import { ImmichLogger } from 'src/infra/logger';
import {
ClientEvent,
ICommunicationRepository,
InternalEvent,
InternalEventMap,
ServerEvent,
-} from 'src/interfaces/communication.repository';
-import { ISearchRepository } from 'src/interfaces/search.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+} from 'src/interfaces/communication.interface';
+import { ISearchRepository } from 'src/interfaces/search.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { ImmichLogger } from 'src/utils/logger';
@Injectable()
export class SystemConfigService {
@@ -61,7 +60,7 @@ export class SystemConfigService {
return mapConfig(config);
}
- @OnEvent(InternalEvent.VALIDATE_CONFIG)
+ @OnEventInternal(InternalEvent.VALIDATE_CONFIG)
validateConfig({ newConfig, oldConfig }: InternalEventMap[InternalEvent.VALIDATE_CONFIG]) {
if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) {
throw new Error('Logging cannot be changed while the environment variable LOG_LEVEL is set.');
diff --git a/server/src/domain/tag/tag.service.spec.ts b/server/src/services/tag.service.spec.ts
similarity index 97%
rename from server/src/domain/tag/tag.service.spec.ts
rename to server/src/services/tag.service.spec.ts
index 9f630cdc0..2d684616a 100644
--- a/server/src/domain/tag/tag.service.spec.ts
+++ b/server/src/services/tag.service.spec.ts
@@ -1,9 +1,9 @@
import { BadRequestException } from '@nestjs/common';
import { when } from 'jest-when';
-import { AssetIdErrorReason } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { TagService } from 'src/domain/tag/tag.service';
+import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { TagType } from 'src/entities/tag.entity';
-import { ITagRepository } from 'src/interfaces/tag.repository';
+import { ITagRepository } from 'src/interfaces/tag.interface';
+import { TagService } from 'src/services/tag.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub';
diff --git a/server/src/domain/tag/tag.service.ts b/server/src/services/tag.service.ts
similarity index 85%
rename from server/src/domain/tag/tag.service.ts
rename to server/src/services/tag.service.ts
index b04e251f7..c04f9b14c 100644
--- a/server/src/domain/tag/tag.service.ts
+++ b/server/src/services/tag.service.ts
@@ -1,11 +1,10 @@
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
-import { AssetIdsDto } from 'src/domain/asset/dto/asset-ids.dto';
-import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AssetResponseDto, mapAsset } from 'src/domain/asset/response-dto/asset-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { TagResponseDto, mapTag } from 'src/domain/tag/tag-response.dto';
-import { CreateTagDto, UpdateTagDto } from 'src/domain/tag/tag.dto';
-import { ITagRepository } from 'src/interfaces/tag.repository';
+import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto';
+import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
+import { AssetIdsDto } from 'src/dtos/asset.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { CreateTagDto, TagResponseDto, UpdateTagDto, mapTag } from 'src/dtos/tag.dto';
+import { ITagRepository } from 'src/interfaces/tag.interface';
@Injectable()
export class TagService {
diff --git a/server/src/domain/trash/trash.service.spec.ts b/server/src/services/trash.service.spec.ts
similarity index 92%
rename from server/src/domain/trash/trash.service.spec.ts
rename to server/src/services/trash.service.spec.ts
index 021c8e526..e43926e4d 100644
--- a/server/src/domain/trash/trash.service.spec.ts
+++ b/server/src/services/trash.service.spec.ts
@@ -1,9 +1,8 @@
import { BadRequestException } from '@nestjs/common';
-import { JobName } from 'src/domain/job/job.constants';
-import { TrashService } from 'src/domain/trash/trash.service';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { TrashService } from 'src/services/trash.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { authStub } from 'test/fixtures/auth.stub';
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
diff --git a/server/src/domain/trash/trash.service.ts b/server/src/services/trash.service.ts
similarity index 80%
rename from server/src/domain/trash/trash.service.ts
rename to server/src/services/trash.service.ts
index 2043b4248..5f1ee29f7 100644
--- a/server/src/domain/trash/trash.service.ts
+++ b/server/src/services/trash.service.ts
@@ -1,14 +1,13 @@
import { Inject } from '@nestjs/common';
import { DateTime } from 'luxon';
import { AccessCore, Permission } from 'src/cores/access.core';
-import { BulkIdsDto } from 'src/domain/asset/response-dto/asset-ids-response.dto';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/domain/job/job.constants';
-import { IAccessRepository } from 'src/interfaces/access.repository';
-import { IAssetRepository } from 'src/interfaces/asset.repository';
-import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { usePagination } from 'src/utils';
+import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { IAccessRepository } from 'src/interfaces/access.interface';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
+import { ClientEvent, ICommunicationRepository } from 'src/interfaces/communication.interface';
+import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface';
+import { usePagination } from 'src/utils/pagination';
export class TrashService {
private access: AccessCore;
diff --git a/server/src/domain/user/user.service.spec.ts b/server/src/services/user.service.spec.ts
similarity index 96%
rename from server/src/domain/user/user.service.spec.ts
rename to server/src/services/user.service.spec.ts
index 5b9a1de95..973f644d3 100644
--- a/server/src/domain/user/user.service.spec.ts
+++ b/server/src/services/user.service.spec.ts
@@ -5,19 +5,17 @@ import {
NotFoundException,
} from '@nestjs/common';
import { when } from 'jest-when';
-import { JobName } from 'src/domain/job/job.constants';
-import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
-import { mapUser } from 'src/domain/user/response-dto/user-response.dto';
-import { UserService } from 'src/domain/user/user.service';
+import { UpdateUserDto, mapUser } from 'src/dtos/user.dto';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository } from 'src/interfaces/user.repository';
-import { CacheControl, ImmichFileResponse } from 'src/utils';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IJobRepository, JobName } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository } from 'src/interfaces/user.interface';
+import { UserService } from 'src/services/user.service';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
import { authStub } from 'test/fixtures/auth.stub';
import { systemConfigStub } from 'test/fixtures/system-config.stub';
import { userStub } from 'test/fixtures/user.stub';
diff --git a/server/src/domain/user/user.service.ts b/server/src/services/user.service.ts
similarity index 87%
rename from server/src/domain/user/user.service.ts
rename to server/src/services/user.service.ts
index 349a775ec..6649927da 100644
--- a/server/src/domain/user/user.service.ts
+++ b/server/src/services/user.service.ts
@@ -4,27 +4,19 @@ import { randomBytes } from 'node:crypto';
import { StorageCore, StorageFolder } from 'src/cores/storage.core';
import { SystemConfigCore } from 'src/cores/system-config.core';
import { UserCore } from 'src/cores/user.core';
-import { AuthDto } from 'src/domain/auth/auth.dto';
-import { JobName } from 'src/domain/job/job.constants';
-import { IEntityJob } from 'src/domain/job/job.interface';
-import { CreateUserDto } from 'src/domain/user/dto/create-user.dto';
-import { DeleteUserDto } from 'src/domain/user/dto/delete-user.dto';
-import { UpdateUserDto } from 'src/domain/user/dto/update-user.dto';
-import {
- CreateProfileImageResponseDto,
- mapCreateProfileImageResponse,
-} from 'src/domain/user/response-dto/create-profile-image-response.dto';
-import { UserResponseDto, mapUser } from 'src/domain/user/response-dto/user-response.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
+import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto';
+import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto, mapUser } from 'src/dtos/user.dto';
import { UserEntity, UserStatus } from 'src/entities/user.entity';
-import { ImmichLogger } from 'src/infra/logger';
-import { IAlbumRepository } from 'src/interfaces/album.repository';
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
-import { IJobRepository, JobStatus } from 'src/interfaces/job.repository';
-import { ILibraryRepository } from 'src/interfaces/library.repository';
-import { IStorageRepository } from 'src/interfaces/storage.repository';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
-import { IUserRepository, UserFindOptions } from 'src/interfaces/user.repository';
-import { CacheControl, ImmichFileResponse } from 'src/utils';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
+import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
+import { IStorageRepository } from 'src/interfaces/storage.interface';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
+import { IUserRepository, UserFindOptions } from 'src/interfaces/user.interface';
+import { CacheControl, ImmichFileResponse } from 'src/utils/file';
+import { ImmichLogger } from 'src/utils/logger';
@Injectable()
export class UserService {
diff --git a/server/src/utils.spec.ts b/server/src/utils.spec.ts
deleted file mode 100644
index c5ae5f6e5..000000000
--- a/server/src/utils.spec.ts
+++ /dev/null
@@ -1,103 +0,0 @@
-import { isDecimalNumber, isNumberInRange, parseLatitude, parseLongitude, toNumberOrNull } from 'src/utils';
-
-describe('checks if a number is a decimal number', () => {
- it('returns false for non-decimal numbers', () => {
- expect(isDecimalNumber(Number.NaN)).toBe(false);
- expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false);
- expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false);
- });
-
- it('returns true for decimal numbers', () => {
- expect(isDecimalNumber(0)).toBe(true);
- expect(isDecimalNumber(-0)).toBe(true);
- expect(isDecimalNumber(10.123_45)).toBe(true);
- expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true);
- expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true);
- });
-});
-
-describe('checks if a number is within a range', () => {
- it('returns false for numbers outside the range', () => {
- expect(isNumberInRange(0, 10, 10)).toBe(false);
- expect(isNumberInRange(0.01, 10, 10)).toBe(false);
- expect(isNumberInRange(50.1, 0, 50)).toBe(false);
- });
-
- it('returns true for numbers inside the range', () => {
- expect(isNumberInRange(0, 0, 50)).toBe(true);
- expect(isNumberInRange(50, 0, 50)).toBe(true);
- expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true);
- });
-});
-
-describe('converts input to a number or null', () => {
- it('returns null for invalid inputs', () => {
- expect(toNumberOrNull(null)).toBeNull();
- // eslint-disable-next-line unicorn/no-useless-undefined
- expect(toNumberOrNull(undefined)).toBeNull();
- expect(toNumberOrNull('')).toBeNull();
- expect(toNumberOrNull(Number.NaN)).toBeNull();
- });
-
- it('returns a number for valid inputs', () => {
- expect(toNumberOrNull(0)).toBeCloseTo(0);
- expect(toNumberOrNull('0')).toBeCloseTo(0);
- expect(toNumberOrNull('-123.45')).toBeCloseTo(-123.45);
- });
-});
-
-describe('parsing latitude from string input', () => {
- it('returns null for invalid inputs', () => {
- expect(parseLatitude('')).toBeNull();
- expect(parseLatitude('NaN')).toBeNull();
- expect(parseLatitude('Infinity')).toBeNull();
- expect(parseLatitude('-Infinity')).toBeNull();
- expect(parseLatitude('90.001')).toBeNull();
- expect(parseLatitude(-90.000_001)).toBeNull();
- expect(parseLatitude('1000')).toBeNull();
- expect(parseLatitude(-1000)).toBeNull();
- });
-
- it('returns the numeric coordinate for valid inputs', () => {
- expect(parseLatitude('90')).toBeCloseTo(90);
- expect(parseLatitude('-90')).toBeCloseTo(-90);
- expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999);
- expect(parseLatitude('-89.9')).toBeCloseTo(-89.9);
- expect(parseLatitude(0)).toBeCloseTo(0);
- expect(parseLatitude('-0.0')).toBeCloseTo(-0);
- });
-});
-
-describe('parsing latitude from null input', () => {
- it('returns null for null input', () => {
- expect(parseLatitude(null)).toBeNull();
- });
-});
-
-describe('parsing longitude from string input', () => {
- it('returns null for invalid inputs', () => {
- expect(parseLongitude('')).toBeNull();
- expect(parseLongitude('NaN')).toBeNull();
- expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull();
- expect(parseLongitude('-Infinity')).toBeNull();
- expect(parseLongitude('180.001')).toBeNull();
- expect(parseLongitude('-180.000001')).toBeNull();
- expect(parseLongitude(1000)).toBeNull();
- expect(parseLongitude('-1000')).toBeNull();
- });
-
- it('returns the numeric coordinate for valid inputs', () => {
- expect(parseLongitude(180)).toBeCloseTo(180);
- expect(parseLongitude('-180')).toBeCloseTo(-180);
- expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999);
- expect(parseLongitude(-179.9)).toBeCloseTo(-179.9);
- expect(parseLongitude('0')).toBeCloseTo(0);
- expect(parseLongitude('-0.0')).toBeCloseTo(-0);
- });
-});
-
-describe('parsing longitude from null input', () => {
- it('returns null for null input', () => {
- expect(parseLongitude(null)).toBeNull();
- });
-});
diff --git a/server/src/utils.ts b/server/src/utils.ts
deleted file mode 100644
index 863dde7ca..000000000
--- a/server/src/utils.ts
+++ /dev/null
@@ -1,181 +0,0 @@
-import { basename, extname } from 'node:path';
-import { ImmichLogger } from 'src/infra/logger';
-
-const KiB = Math.pow(1024, 1);
-const MiB = Math.pow(1024, 2);
-const GiB = Math.pow(1024, 3);
-const TiB = Math.pow(1024, 4);
-const PiB = Math.pow(1024, 5);
-
-export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB };
-
-export function asHumanReadable(bytes: number, precision = 1): string {
- const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
-
- let magnitude = 0;
- let remainder = bytes;
- while (remainder >= 1024) {
- if (magnitude + 1 < units.length) {
- magnitude++;
- remainder /= 1024;
- } else {
- break;
- }
- }
-
- return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
-}
-
-export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
-
-export function isDecimalNumber(number_: number): boolean {
- return !Number.isNaN(number_) && Number.isFinite(number_);
-}
-
-/**
- * Check if `num` is a valid number and is between `start` and `end` (inclusive)
- */
-export function isNumberInRange(number_: number, start: number, end: number): boolean {
- return isDecimalNumber(number_) && number_ >= start && number_ <= end;
-}
-
-export function toNumberOrNull(input: number | string | null | undefined): number | null {
- if (input === null || input === undefined) {
- return null;
- }
-
- const number_ = typeof input === 'string' ? Number.parseFloat(input) : input;
- return isDecimalNumber(number_) ? number_ : null;
-}
-
-export function parseLatitude(input: string | number | null): number | null {
- if (input === null) {
- return null;
- }
- const latitude = typeof input === 'string' ? Number.parseFloat(input) : input;
-
- if (isNumberInRange(latitude, -90, 90)) {
- return latitude;
- }
- return null;
-}
-
-export function parseLongitude(input: string | number | null): number | null {
- if (input === null) {
- return null;
- }
-
- const longitude = typeof input === 'string' ? Number.parseFloat(input) : input;
-
- if (isNumberInRange(longitude, -180, 180)) {
- return longitude;
- }
- return null;
-}
-
-// NOTE: The following Set utils have been added here, to easily determine where they are used.
-// They should be replaced with native Set operations, when they are added to the language.
-// Proposal reference: https://github.com/tc39/proposal-set-methods
-
-export const setUnion = (...sets: Set[]): Set => {
- const union = new Set(sets[0]);
- for (const set of sets.slice(1)) {
- for (const element of set) {
- union.add(element);
- }
- }
- return union;
-};
-
-export const setDifference = (setA: Set, ...sets: Set[]): Set => {
- const difference = new Set(setA);
- for (const set of sets) {
- for (const element of set) {
- difference.delete(element);
- }
- }
- return difference;
-};
-
-export const setIsSuperset = (set: Set, subset: Set): boolean => {
- for (const element of subset) {
- if (!set.has(element)) {
- return false;
- }
- }
- return true;
-};
-
-export const setIsEqual = (setA: Set, setB: Set): boolean => {
- return setA.size === setB.size && setIsSuperset(setA, setB);
-};
-
-export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => {
- promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
-};
-
-export enum CacheControl {
- PRIVATE_WITH_CACHE = 'private_with_cache',
- PRIVATE_WITHOUT_CACHE = 'private_without_cache',
- NONE = 'none',
-}
-
-export class ImmichFileResponse {
- public readonly path!: string;
- public readonly contentType!: string;
- public readonly cacheControl!: CacheControl;
-
- constructor(response: ImmichFileResponse) {
- Object.assign(this, response);
- }
-}
-
-export interface OpenGraphTags {
- title: string;
- description: string;
- imageUrl?: string;
-}
-
-export function getFileNameWithoutExtension(path: string): string {
- return basename(path, extname(path));
-}
-
-export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
- return getFileNameWithoutExtension(stillName) + extname(motionName);
-}
-
-export interface PaginationOptions {
- take: number;
- skip?: number;
-}
-
-export enum PaginationMode {
- LIMIT_OFFSET = 'limit-offset',
- SKIP_TAKE = 'skip-take',
-}
-
-export interface PaginatedBuilderOptions {
- take: number;
- skip?: number;
- mode?: PaginationMode;
-}
-
-export interface PaginationResult {
- items: T[];
- hasNextPage: boolean;
-}
-
-export type Paginated = Promise>;
-
-export async function* usePagination(
- pageSize: number,
- getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated,
-) {
- let hasNextPage = true;
-
- for (let skip = 0; hasNextPage; skip += pageSize) {
- const result = await getNextPage({ take: pageSize, skip });
- hasNextPage = result.hasNextPage;
- yield result.items;
- }
-}
diff --git a/server/src/utils/bytes.ts b/server/src/utils/bytes.ts
new file mode 100644
index 000000000..e837c81b9
--- /dev/null
+++ b/server/src/utils/bytes.ts
@@ -0,0 +1,24 @@
+const KiB = Math.pow(1024, 1);
+const MiB = Math.pow(1024, 2);
+const GiB = Math.pow(1024, 3);
+const TiB = Math.pow(1024, 4);
+const PiB = Math.pow(1024, 5);
+
+export const HumanReadableSize = { KiB, MiB, GiB, TiB, PiB };
+
+export function asHumanReadable(bytes: number, precision = 1): string {
+ const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB'];
+
+ let magnitude = 0;
+ let remainder = bytes;
+ while (remainder >= 1024) {
+ if (magnitude + 1 < units.length) {
+ magnitude++;
+ remainder /= 1024;
+ } else {
+ break;
+ }
+ }
+
+ return `${remainder.toFixed(magnitude == 0 ? 0 : precision)} ${units[magnitude]}`;
+}
diff --git a/server/src/infra/infra.utils.ts b/server/src/utils/database.ts
similarity index 75%
rename from server/src/infra/infra.utils.ts
rename to server/src/utils/database.ts
index 2b75af379..7ec677756 100644
--- a/server/src/infra/infra.utils.ts
+++ b/server/src/utils/database.ts
@@ -1,18 +1,7 @@
import _ from 'lodash';
import { AssetEntity } from 'src/entities/asset.entity';
-import { AssetSearchBuilderOptions } from 'src/interfaces/search.repository';
-import { Paginated, PaginatedBuilderOptions, PaginationMode, PaginationOptions, PaginationResult } from 'src/utils';
-import {
- Between,
- FindManyOptions,
- IsNull,
- LessThanOrEqual,
- MoreThanOrEqual,
- Not,
- ObjectLiteral,
- Repository,
- SelectQueryBuilder,
-} from 'typeorm';
+import { AssetSearchBuilderOptions } from 'src/interfaces/search.interface';
+import { Between, IsNull, LessThanOrEqual, MoreThanOrEqual, Not, SelectQueryBuilder } from 'typeorm';
/**
* Allows optional values unlike the regular Between and uses MoreThanOrEqual
@@ -28,47 +17,6 @@ export function OptionalBetween(from?: T, to?: T) {
}
}
-function paginationHelper(items: Entity[], take: number): PaginationResult {
- const hasNextPage = items.length > take;
- items.splice(take);
-
- return { items, hasNextPage };
-}
-
-export async function paginate(
- repository: Repository,
- { take, skip }: PaginationOptions,
- searchOptions?: FindManyOptions,
-): Paginated {
- const items = await repository.find(
- _.omitBy(
- {
- ...searchOptions,
- // Take one more item to check if there's a next page
- take: take + 1,
- skip,
- },
- _.isUndefined,
- ),
- );
-
- return paginationHelper(items, take);
-}
-
-export async function paginatedBuilder(
- qb: SelectQueryBuilder,
- { take, skip, mode }: PaginatedBuilderOptions,
-): Paginated {
- if (mode === PaginationMode.LIMIT_OFFSET) {
- qb.limit(take + 1).offset(skip);
- } else {
- qb.take(take + 1).skip(skip);
- }
-
- const items = await qb.getMany();
- return paginationHelper(items, take);
-}
-
export const asVector = (embedding: number[], quote = false) =>
quote ? `'[${embedding.join(',')}]'` : `[${embedding.join(',')}]`;
diff --git a/server/src/utils/file.ts b/server/src/utils/file.ts
new file mode 100644
index 000000000..a80f17bea
--- /dev/null
+++ b/server/src/utils/file.ts
@@ -0,0 +1,88 @@
+import { HttpException, StreamableFile } from '@nestjs/common';
+import { NextFunction, Response } from 'express';
+import { access, constants } from 'node:fs/promises';
+import { basename, extname, isAbsolute } from 'node:path';
+import { promisify } from 'node:util';
+import { ImmichReadStream } from 'src/interfaces/storage.interface';
+import { ImmichLogger } from 'src/utils/logger';
+import { isConnectionAborted } from 'src/utils/misc';
+
+export function getFileNameWithoutExtension(path: string): string {
+ return basename(path, extname(path));
+}
+
+export function getLivePhotoMotionFilename(stillName: string, motionName: string) {
+ return getFileNameWithoutExtension(stillName) + extname(motionName);
+}
+
+export enum CacheControl {
+ PRIVATE_WITH_CACHE = 'private_with_cache',
+ PRIVATE_WITHOUT_CACHE = 'private_without_cache',
+ NONE = 'none',
+}
+
+export class ImmichFileResponse {
+ public readonly path!: string;
+ public readonly contentType!: string;
+ public readonly cacheControl!: CacheControl;
+
+ constructor(response: ImmichFileResponse) {
+ Object.assign(this, response);
+ }
+}
+type SendFile = Parameters;
+type SendFileOptions = SendFile[1];
+
+const logger = new ImmichLogger('SendFile');
+
+export const sendFile = async (
+ res: Response,
+ next: NextFunction,
+ handler: () => Promise,
+): Promise => {
+ const _sendFile = (path: string, options: SendFileOptions) =>
+ promisify(res.sendFile).bind(res)(path, options);
+
+ try {
+ const file = await handler();
+ switch (file.cacheControl) {
+ case CacheControl.PRIVATE_WITH_CACHE: {
+ res.set('Cache-Control', 'private, max-age=86400, no-transform');
+ break;
+ }
+
+ case CacheControl.PRIVATE_WITHOUT_CACHE: {
+ res.set('Cache-Control', 'private, no-cache, no-transform');
+ break;
+ }
+ }
+
+ res.header('Content-Type', file.contentType);
+
+ const options: SendFileOptions = { dotfiles: 'allow' };
+ if (!isAbsolute(file.path)) {
+ options.root = process.cwd();
+ }
+
+ await access(file.path, constants.R_OK);
+
+ return _sendFile(file.path, options);
+ } catch (error: Error | any) {
+ // ignore client-closed connection
+ if (isConnectionAborted(error)) {
+ return;
+ }
+
+ // log non-http errors
+ if (error instanceof HttpException === false) {
+ logger.error(`Unable to send file: ${error.name}`, error.stack);
+ }
+
+ res.header('Cache-Control', 'none');
+ next(error);
+ }
+};
+
+export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
+ return new StreamableFile(stream, { type, length });
+};
diff --git a/server/src/infra/instrumentation.ts b/server/src/utils/instrumentation.ts
similarity index 97%
rename from server/src/infra/instrumentation.ts
rename to server/src/utils/instrumentation.ts
index a30f0523a..12d44aeac 100644
--- a/server/src/infra/instrumentation.ts
+++ b/server/src/utils/instrumentation.ts
@@ -13,9 +13,8 @@ import { snakeCase, startCase } from 'lodash';
import { OpenTelemetryModuleOptions } from 'nestjs-otel/lib/interfaces';
import { copyMetadataFromFunctionToFunction } from 'nestjs-otel/lib/opentelemetry.utils';
import { performance } from 'node:perf_hooks';
-import { excludePaths } from 'src/config';
+import { excludePaths, serverVersion } from 'src/constants';
import { DecorateAll } from 'src/decorators';
-import { serverVersion } from 'src/domain/domain.constant';
let metricsEnabled = process.env.IMMICH_METRICS === 'true';
const hostMetrics =
diff --git a/server/src/infra/logger.ts b/server/src/utils/logger.ts
similarity index 100%
rename from server/src/infra/logger.ts
rename to server/src/utils/logger.ts
diff --git a/server/src/domain/media/media.util.ts b/server/src/utils/media.ts
similarity index 99%
rename from server/src/domain/media/media.util.ts
rename to server/src/utils/media.ts
index 3e58ca81a..5f1218766 100644
--- a/server/src/domain/media/media.util.ts
+++ b/server/src/utils/media.ts
@@ -1,4 +1,4 @@
-import { SystemConfigFFmpegDto } from 'src/domain/system-config/dto/system-config-ffmpeg.dto';
+import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { CQMode, ToneMapping, TranscodeHWAccel, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity';
import {
AudioStreamInfo,
@@ -7,7 +7,7 @@ import {
VideoCodecHWConfig,
VideoCodecSWConfig,
VideoStreamInfo,
-} from 'src/interfaces/media.repository';
+} from 'src/interfaces/media.interface';
class BaseConfig implements VideoCodecSWConfig {
presets = ['veryslow', 'slower', 'slow', 'medium', 'fast', 'faster', 'veryfast', 'superfast', 'ultrafast'];
diff --git a/server/src/domain/domain.constant.spec.ts b/server/src/utils/mime-types.spec.ts
similarity index 68%
rename from server/src/domain/domain.constant.spec.ts
rename to server/src/utils/mime-types.spec.ts
index e8a1c7b72..96c1921ba 100644
--- a/server/src/domain/domain.constant.spec.ts
+++ b/server/src/utils/mime-types.spec.ts
@@ -1,4 +1,4 @@
-import { mimeTypes, Version, VersionType } from 'src/domain/domain.constant';
+import { mimeTypes } from 'src/utils/mime-types';
describe('mimeTypes', () => {
for (const { mimetype, extension } of [
@@ -196,74 +196,3 @@ describe('mimeTypes', () => {
}
});
});
-
-describe('Version', () => {
- const tests = [
- { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH },
- { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR },
- { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR },
- { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH },
- { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR },
- { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR },
- { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL },
- { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL },
- { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL },
- { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
- { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
- { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH },
- { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR },
- { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
- { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH },
- ];
-
- describe('isOlderThan', () => {
- for (const { this: thisVersion, other: otherVersion, compare, type } of tests) {
- const expected = compare < 0 ? type : VersionType.EQUAL;
- it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => {
- expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected);
- });
- }
- });
-
- describe('isEqual', () => {
- for (const { this: thisVersion, other: otherVersion, compare } of tests) {
- const bool = compare === 0;
- it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => {
- expect(thisVersion.isEqual(otherVersion)).toEqual(bool);
- });
- }
- });
-
- describe('isNewerThan', () => {
- for (const { this: thisVersion, other: otherVersion, compare, type } of tests) {
- const expected = compare > 0 ? type : VersionType.EQUAL;
- it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => {
- expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected);
- });
- }
- });
-
- describe('fromString', () => {
- const tests = [
- { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) },
- { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) },
- { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) },
- { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) },
- { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) },
- { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) },
- { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) },
- { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) },
- { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) },
- { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) },
- ];
-
- for (const { scenario, value, expected } of tests) {
- it(`should correctly parse ${scenario}`, () => {
- const actual = Version.fromString(value);
- expect(actual.major).toEqual(expected.major);
- expect(actual.minor).toEqual(expected.minor);
- expect(actual.patch).toEqual(expected.patch);
- });
- }
- });
-});
diff --git a/server/src/domain/domain.constant.ts b/server/src/utils/mime-types.ts
similarity index 56%
rename from server/src/domain/domain.constant.ts
rename to server/src/utils/mime-types.ts
index 29c4837b6..789fc6a1c 100644
--- a/server/src/domain/domain.constant.ts
+++ b/server/src/utils/mime-types.ts
@@ -1,97 +1,6 @@
-import { Duration } from 'luxon';
-import { readFileSync } from 'node:fs';
-import { extname, join } from 'node:path';
+import { extname } from 'node:path';
import { AssetType } from 'src/entities/asset.entity';
-export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
-export const ONE_HOUR = Duration.fromObject({ hours: 1 });
-
-export interface IVersion {
- major: number;
- minor: number;
- patch: number;
-}
-
-export enum VersionType {
- EQUAL = 0,
- PATCH = 1,
- MINOR = 2,
- MAJOR = 3,
-}
-
-export class Version implements IVersion {
- public readonly types = ['major', 'minor', 'patch'] as const;
-
- constructor(
- public major: number,
- public minor: number = 0,
- public patch: number = 0,
- ) {}
-
- toString() {
- return `${this.major}.${this.minor}.${this.patch}`;
- }
-
- toJSON() {
- const { major, minor, patch } = this;
- return { major, minor, patch };
- }
-
- static fromString(version: string): Version {
- const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i;
- const matchResult = version.match(regex);
- if (matchResult) {
- const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string };
- return new Version(Number(major), Number(minor), Number(patch));
- } else {
- throw new Error(`Invalid version format: ${version}`);
- }
- }
-
- private compare(version: Version): [number, VersionType] {
- for (const [i, key] of this.types.entries()) {
- const diff = this[key] - version[key];
- if (diff !== 0) {
- return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType];
- }
- }
-
- return [0, VersionType.EQUAL];
- }
-
- isOlderThan(version: Version): VersionType {
- const [bool, type] = this.compare(version);
- return bool < 0 ? type : VersionType.EQUAL;
- }
-
- isEqual(version: Version): boolean {
- const [bool] = this.compare(version);
- return bool === 0;
- }
-
- isNewerThan(version: Version): VersionType {
- const [bool, type] = this.compare(version);
- return bool > 0 ? type : VersionType.EQUAL;
- }
-}
-
-export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
-export const isDev = process.env.NODE_ENV === 'development';
-
-const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
-export const serverVersion = Version.fromString(version);
-
-export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
-export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
-
-const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources';
-
-export const citiesFile = 'cities500.txt';
-export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt');
-export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt');
-export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt');
-export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile);
-
const image: Record = {
'.3fr': ['image/3fr', 'image/x-hasselblad-3fr'],
'.ari': ['image/ari', 'image/x-arriflex-ari'],
diff --git a/server/src/immich/app.utils.ts b/server/src/utils/misc.ts
similarity index 63%
rename from server/src/immich/app.utils.ts
rename to server/src/utils/misc.ts
index f0e4ba661..3837c6279 100644
--- a/server/src/immich/app.utils.ts
+++ b/server/src/utils/misc.ts
@@ -1,4 +1,4 @@
-import { HttpException, INestApplication, StreamableFile } from '@nestjs/common';
+import { INestApplication } from '@nestjs/common';
import {
DocumentBuilder,
OpenAPIObject,
@@ -7,75 +7,48 @@ import {
SwaggerModule,
} from '@nestjs/swagger';
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
-import { NextFunction, Response } from 'express';
import _ from 'lodash';
import { writeFileSync } from 'node:fs';
-import { access, constants } from 'node:fs/promises';
-import path, { isAbsolute } from 'node:path';
-import { promisify } from 'node:util';
-import { IMMICH_ACCESS_COOKIE, IMMICH_API_KEY_HEADER, IMMICH_API_KEY_NAME } from 'src/domain/auth/auth.constant';
-import { serverVersion } from 'src/domain/domain.constant';
-import { ImmichLogger } from 'src/infra/logger';
-import { ImmichReadStream } from 'src/interfaces/storage.repository';
+import path from 'node:path';
+import {
+ CLIP_MODEL_INFO,
+ IMMICH_ACCESS_COOKIE,
+ IMMICH_API_KEY_HEADER,
+ IMMICH_API_KEY_NAME,
+ serverVersion,
+} from 'src/constants';
import { Metadata } from 'src/middleware/auth.guard';
-import { CacheControl, ImmichFileResponse, isConnectionAborted } from 'src/utils';
+import { ImmichLogger } from 'src/utils/logger';
-type SendFile = Parameters;
-type SendFileOptions = SendFile[1];
+export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
-const logger = new ImmichLogger('SendFile');
+export const handlePromiseError = (promise: Promise, logger: ImmichLogger): void => {
+ promise.catch((error: Error | any) => logger.error(`Promise error: ${error}`, error?.stack));
+};
-export const sendFile = async (
- res: Response,
- next: NextFunction,
- handler: () => Promise,
-): Promise => {
- const _sendFile = (path: string, options: SendFileOptions) =>
- promisify(res.sendFile).bind(res)(path, options);
+export interface OpenGraphTags {
+ title: string;
+ description: string;
+ imageUrl?: string;
+}
- try {
- const file = await handler();
- switch (file.cacheControl) {
- case CacheControl.PRIVATE_WITH_CACHE: {
- res.set('Cache-Control', 'private, max-age=86400, no-transform');
- break;
- }
-
- case CacheControl.PRIVATE_WITHOUT_CACHE: {
- res.set('Cache-Control', 'private, no-cache, no-transform');
- break;
- }
- }
-
- res.header('Content-Type', file.contentType);
-
- const options: SendFileOptions = { dotfiles: 'allow' };
- if (!isAbsolute(file.path)) {
- options.root = process.cwd();
- }
-
- await access(file.path, constants.R_OK);
-
- return _sendFile(file.path, options);
- } catch (error: Error | any) {
- // ignore client-closed connection
- if (isConnectionAborted(error)) {
- return;
- }
-
- // log non-http errors
- if (error instanceof HttpException === false) {
- logger.error(`Unable to send file: ${error.name}`, error.stack);
- }
-
- res.header('Cache-Control', 'none');
- next(error);
+function cleanModelName(modelName: string): string {
+ const token = modelName.split('/').at(-1);
+ if (!token) {
+ throw new Error(`Invalid model name: ${modelName}`);
}
-};
-export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) => {
- return new StreamableFile(stream, { type, length });
-};
+ return token.replaceAll(':', '_');
+}
+
+export function getCLIPModelInfo(modelName: string) {
+ const modelInfo = CLIP_MODEL_INFO[cleanModelName(modelName)];
+ if (!modelInfo) {
+ throw new Error(`Unknown CLIP model: ${modelName}`);
+ }
+
+ return modelInfo;
+}
function sortKeys(target: T): T {
if (!target || typeof target !== 'object' || Array.isArray(target)) {
diff --git a/server/src/utils/pagination.ts b/server/src/utils/pagination.ts
new file mode 100644
index 000000000..dec1a9de0
--- /dev/null
+++ b/server/src/utils/pagination.ts
@@ -0,0 +1,79 @@
+import _ from 'lodash';
+import { FindManyOptions, ObjectLiteral, Repository, SelectQueryBuilder } from 'typeorm';
+
+export interface PaginationOptions {
+ take: number;
+ skip?: number;
+}
+
+export enum PaginationMode {
+ LIMIT_OFFSET = 'limit-offset',
+ SKIP_TAKE = 'skip-take',
+}
+
+export interface PaginatedBuilderOptions {
+ take: number;
+ skip?: number;
+ mode?: PaginationMode;
+}
+
+export interface PaginationResult {
+ items: T[];
+ hasNextPage: boolean;
+}
+
+export type Paginated = Promise>;
+
+export async function* usePagination(
+ pageSize: number,
+ getNextPage: (pagination: PaginationOptions) => PaginationResult | Paginated,
+) {
+ let hasNextPage = true;
+
+ for (let skip = 0; hasNextPage; skip += pageSize) {
+ const result = await getNextPage({ take: pageSize, skip });
+ hasNextPage = result.hasNextPage;
+ yield result.items;
+ }
+}
+
+function paginationHelper(items: Entity[], take: number): PaginationResult {
+ const hasNextPage = items.length > take;
+ items.splice(take);
+
+ return { items, hasNextPage };
+}
+
+export async function paginate(
+ repository: Repository,
+ { take, skip }: PaginationOptions,
+ searchOptions?: FindManyOptions,
+): Paginated {
+ const items = await repository.find(
+ _.omitBy(
+ {
+ ...searchOptions,
+ // Take one more item to check if there's a next page
+ take: take + 1,
+ skip,
+ },
+ _.isUndefined,
+ ),
+ );
+
+ return paginationHelper(items, take);
+}
+
+export async function paginatedBuilder(
+ qb: SelectQueryBuilder,
+ { take, skip, mode }: PaginatedBuilderOptions,
+): Paginated {
+ if (mode === PaginationMode.LIMIT_OFFSET) {
+ qb.limit(take + 1).offset(skip);
+ } else {
+ qb.take(take + 1).skip(skip);
+ }
+
+ const items = await qb.getMany();
+ return paginationHelper(items, take);
+}
diff --git a/server/src/utils/set.ts b/server/src/utils/set.ts
new file mode 100644
index 000000000..971d3f7e5
--- /dev/null
+++ b/server/src/utils/set.ts
@@ -0,0 +1,36 @@
+// NOTE: The following Set utils have been added here, to easily determine where they are used.
+// They should be replaced with native Set operations, when they are added to the language.
+// Proposal reference: https://github.com/tc39/proposal-set-methods
+
+export const setUnion = (...sets: Set[]): Set => {
+ const union = new Set(sets[0]);
+ for (const set of sets.slice(1)) {
+ for (const element of set) {
+ union.add(element);
+ }
+ }
+ return union;
+};
+
+export const setDifference = (setA: Set, ...sets: Set[]): Set => {
+ const difference = new Set(setA);
+ for (const set of sets) {
+ for (const element of set) {
+ difference.delete(element);
+ }
+ }
+ return difference;
+};
+
+export const setIsSuperset = (set: Set, subset: Set): boolean => {
+ for (const element of subset) {
+ if (!set.has(element)) {
+ return false;
+ }
+ }
+ return true;
+};
+
+export const setIsEqual = (setA: Set, setB: Set): boolean => {
+ return setA.size === setB.size && setIsSuperset(setA, setB);
+};
diff --git a/server/src/infra/sql-generator/index.ts b/server/src/utils/sql.ts
similarity index 90%
rename from server/src/infra/sql-generator/index.ts
rename to server/src/utils/sql.ts
index 10730fc36..1afe4d5a8 100644
--- a/server/src/infra/sql-generator/index.ts
+++ b/server/src/utils/sql.ts
@@ -5,11 +5,11 @@ import { Test } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { mkdir, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
+import { format } from 'sql-formatter';
+import { databaseConfig } from 'src/database.config';
import { GENERATE_SQL_KEY, GenerateSqlQueries } from 'src/decorators';
import { databaseEntities } from 'src/entities';
-import { databaseConfig } from 'src/infra/database.config';
-import { SqlLogger } from 'src/infra/sql-generator/sql.logger';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
import { AccessRepository } from 'src/repositories/access.repository';
import { AlbumRepository } from 'src/repositories/album.repository';
import { ApiKeyRepository } from 'src/repositories/api-key.repository';
@@ -26,6 +26,30 @@ import { SystemMetadataRepository } from 'src/repositories/system-metadata.repos
import { TagRepository } from 'src/repositories/tag.repository';
import { UserTokenRepository } from 'src/repositories/user-token.repository';
import { UserRepository } from 'src/repositories/user.repository';
+import { Logger } from 'typeorm';
+
+export class SqlLogger implements Logger {
+ queries: string[] = [];
+ errors: Array<{ error: string | Error; query: string }> = [];
+
+ clear() {
+ this.queries = [];
+ this.errors = [];
+ }
+
+ logQuery(query: string) {
+ this.queries.push(format(query, { language: 'postgresql' }));
+ }
+
+ logQueryError(error: string | Error, query: string) {
+ this.errors.push({ error, query });
+ }
+
+ logQuerySlow() {}
+ logSchemaBuild() {}
+ logMigration() {}
+ log() {}
+}
const reflector = new Reflector();
const repositories = [
@@ -178,7 +202,7 @@ class SqlGenerator {
}
}
-new SqlGenerator({ targetDir: './src/infra/sql' })
+new SqlGenerator({ targetDir: './src/queries' })
.run()
.then(() => {
console.log('Done');
diff --git a/server/src/utils/version.spec.ts b/server/src/utils/version.spec.ts
new file mode 100644
index 000000000..34c8abb41
--- /dev/null
+++ b/server/src/utils/version.spec.ts
@@ -0,0 +1,72 @@
+import { Version, VersionType } from 'src/utils/version';
+
+describe('Version', () => {
+ const tests = [
+ { this: new Version(0, 0, 1), other: new Version(0, 0, 0), compare: 1, type: VersionType.PATCH },
+ { this: new Version(0, 1, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MINOR },
+ { this: new Version(1, 0, 0), other: new Version(0, 0, 0), compare: 1, type: VersionType.MAJOR },
+ { this: new Version(0, 0, 0), other: new Version(0, 0, 1), compare: -1, type: VersionType.PATCH },
+ { this: new Version(0, 0, 0), other: new Version(0, 1, 0), compare: -1, type: VersionType.MINOR },
+ { this: new Version(0, 0, 0), other: new Version(1, 0, 0), compare: -1, type: VersionType.MAJOR },
+ { this: new Version(0, 0, 0), other: new Version(0, 0, 0), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(0, 0, 1), other: new Version(0, 0, 1), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(0, 1, 0), other: new Version(0, 1, 0), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(1, 0, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(1, 0), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(1, 0), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH },
+ { this: new Version(1, 1), other: new Version(1, 0, 1), compare: 1, type: VersionType.MINOR },
+ { this: new Version(1), other: new Version(1, 0, 0), compare: 0, type: VersionType.EQUAL },
+ { this: new Version(1), other: new Version(1, 0, 1), compare: -1, type: VersionType.PATCH },
+ ];
+
+ describe('isOlderThan', () => {
+ for (const { this: thisVersion, other: otherVersion, compare, type } of tests) {
+ const expected = compare < 0 ? type : VersionType.EQUAL;
+ it(`should return '${expected}' when comparing ${thisVersion} to ${otherVersion}`, () => {
+ expect(thisVersion.isOlderThan(otherVersion)).toEqual(expected);
+ });
+ }
+ });
+
+ describe('isEqual', () => {
+ for (const { this: thisVersion, other: otherVersion, compare } of tests) {
+ const bool = compare === 0;
+ it(`should return ${bool} when comparing ${thisVersion} to ${otherVersion}`, () => {
+ expect(thisVersion.isEqual(otherVersion)).toEqual(bool);
+ });
+ }
+ });
+
+ describe('isNewerThan', () => {
+ for (const { this: thisVersion, other: otherVersion, compare, type } of tests) {
+ const expected = compare > 0 ? type : VersionType.EQUAL;
+ it(`should return ${expected} when comparing ${thisVersion} to ${otherVersion}`, () => {
+ expect(thisVersion.isNewerThan(otherVersion)).toEqual(expected);
+ });
+ }
+ });
+
+ describe('fromString', () => {
+ const tests = [
+ { scenario: 'leading v', value: 'v1.72.2', expected: new Version(1, 72, 2) },
+ { scenario: 'uppercase v', value: 'V1.72.2', expected: new Version(1, 72, 2) },
+ { scenario: 'missing v', value: '1.72.2', expected: new Version(1, 72, 2) },
+ { scenario: 'large patch', value: '1.72.123', expected: new Version(1, 72, 123) },
+ { scenario: 'large minor', value: '1.123.0', expected: new Version(1, 123, 0) },
+ { scenario: 'large major', value: '123.0.0', expected: new Version(123, 0, 0) },
+ { scenario: 'major bump', value: 'v2.0.0', expected: new Version(2, 0, 0) },
+ { scenario: 'has dash', value: '14.10-1', expected: new Version(14, 10, 1) },
+ { scenario: 'missing patch', value: '14.10', expected: new Version(14, 10, 0) },
+ { scenario: 'only major', value: '14', expected: new Version(14, 0, 0) },
+ ];
+
+ for (const { scenario, value, expected } of tests) {
+ it(`should correctly parse ${scenario}`, () => {
+ const actual = Version.fromString(value);
+ expect(actual.major).toEqual(expected.major);
+ expect(actual.minor).toEqual(expected.minor);
+ expect(actual.patch).toEqual(expected.patch);
+ });
+ }
+ });
+});
diff --git a/server/src/utils/version.ts b/server/src/utils/version.ts
new file mode 100644
index 000000000..6eca12eb4
--- /dev/null
+++ b/server/src/utils/version.ts
@@ -0,0 +1,64 @@
+export type IVersion = { major: number; minor: number; patch: number };
+
+export enum VersionType {
+ EQUAL = 0,
+ PATCH = 1,
+ MINOR = 2,
+ MAJOR = 3,
+}
+
+export class Version implements IVersion {
+ public readonly types = ['major', 'minor', 'patch'] as const;
+
+ constructor(
+ public major: number,
+ public minor: number = 0,
+ public patch: number = 0,
+ ) {}
+
+ toString() {
+ return `${this.major}.${this.minor}.${this.patch}`;
+ }
+
+ toJSON() {
+ const { major, minor, patch } = this;
+ return { major, minor, patch };
+ }
+
+ static fromString(version: string): Version {
+ const regex = /v?(?\d+)(?:\.(?\d+))?(?:[.-](?\d+))?/i;
+ const matchResult = version.match(regex);
+ if (matchResult) {
+ const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string };
+ return new Version(Number(major), Number(minor), Number(patch));
+ } else {
+ throw new Error(`Invalid version format: ${version}`);
+ }
+ }
+
+ private compare(version: Version): [number, VersionType] {
+ for (const [i, key] of this.types.entries()) {
+ const diff = this[key] - version[key];
+ if (diff !== 0) {
+ return [diff > 0 ? 1 : -1, (VersionType.MAJOR - i) as VersionType];
+ }
+ }
+
+ return [0, VersionType.EQUAL];
+ }
+
+ isOlderThan(version: Version): VersionType {
+ const [bool, type] = this.compare(version);
+ return bool < 0 ? type : VersionType.EQUAL;
+ }
+
+ isEqual(version: Version): boolean {
+ const [bool] = this.compare(version);
+ return bool === 0;
+ }
+
+ isNewerThan(version: Version): VersionType {
+ const [bool, type] = this.compare(version);
+ return bool > 0 ? type : VersionType.EQUAL;
+ }
+}
diff --git a/server/test/fixtures/auth.stub.ts b/server/test/fixtures/auth.stub.ts
index e0add44d2..2e56d0001 100644
--- a/server/test/fixtures/auth.stub.ts
+++ b/server/test/fixtures/auth.stub.ts
@@ -1,4 +1,4 @@
-import { AuthDto } from 'src/domain/auth/auth.dto';
+import { AuthDto } from 'src/dtos/auth.dto';
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
import { UserTokenEntity } from 'src/entities/user-token.entity';
import { UserEntity } from 'src/entities/user.entity';
diff --git a/server/test/fixtures/library.stub.ts b/server/test/fixtures/library.stub.ts
index b3fa4b7f0..dde250a7a 100644
--- a/server/test/fixtures/library.stub.ts
+++ b/server/test/fixtures/library.stub.ts
@@ -1,6 +1,6 @@
import { join } from 'node:path';
+import { APP_MEDIA_LOCATION } from 'src/constants';
import { THUMBNAIL_DIR } from 'src/cores/storage.core';
-import { APP_MEDIA_LOCATION } from 'src/domain/domain.constant';
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
import { userStub } from 'test/fixtures/user.stub';
diff --git a/server/test/fixtures/media.stub.ts b/server/test/fixtures/media.stub.ts
index 1be792c25..5070586ac 100644
--- a/server/test/fixtures/media.stub.ts
+++ b/server/test/fixtures/media.stub.ts
@@ -1,4 +1,4 @@
-import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.repository';
+import { AudioStreamInfo, VideoFormat, VideoInfo, VideoStreamInfo } from 'src/interfaces/media.interface';
const probeStubDefaultFormat: VideoFormat = {
formatName: 'mov,mp4,m4a,3gp,3g2,mj2',
diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts
index 6855c8c7a..34e3da515 100644
--- a/server/test/fixtures/shared-link.stub.ts
+++ b/server/test/fixtures/shared-link.stub.ts
@@ -1,8 +1,8 @@
-import { AlbumResponseDto } from 'src/domain/album/album-response.dto';
-import { AssetResponseDto } from 'src/domain/asset/response-dto/asset-response.dto';
-import { ExifResponseDto } from 'src/domain/asset/response-dto/exif-response.dto';
-import { SharedLinkResponseDto } from 'src/domain/shared-link/shared-link-response.dto';
-import { mapUser } from 'src/domain/user/response-dto/user-response.dto';
+import { AlbumResponseDto } from 'src/dtos/album.dto';
+import { AssetResponseDto } from 'src/dtos/asset-response.dto';
+import { ExifResponseDto } from 'src/dtos/exif.dto';
+import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto';
+import { mapUser } from 'src/dtos/user.dto';
import { AssetOrder } from 'src/entities/album.entity';
import { AssetType } from 'src/entities/asset.entity';
import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity';
diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts
index 27123ddb6..537c65db4 100644
--- a/server/test/fixtures/tag.stub.ts
+++ b/server/test/fixtures/tag.stub.ts
@@ -1,4 +1,4 @@
-import { TagResponseDto } from 'src/domain/tag/tag-response.dto';
+import { TagResponseDto } from 'src/dtos/tag.dto';
import { TagEntity, TagType } from 'src/entities/tag.entity';
import { userStub } from 'test/fixtures/user.stub';
diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts
index 8d2233cf6..a61451277 100644
--- a/server/test/repositories/access.repository.mock.ts
+++ b/server/test/repositories/access.repository.mock.ts
@@ -1,5 +1,5 @@
import { AccessCore } from 'src/cores/access.core';
-import { IAccessRepository } from 'src/interfaces/access.repository';
+import { IAccessRepository } from 'src/interfaces/access.interface';
export interface IAccessRepositoryMock {
activity: jest.Mocked;
diff --git a/server/test/repositories/activity.repository.mock.ts b/server/test/repositories/activity.repository.mock.ts
index 84cf7e124..276b57c6c 100644
--- a/server/test/repositories/activity.repository.mock.ts
+++ b/server/test/repositories/activity.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IActivityRepository } from 'src/interfaces/activity.repository';
+import { IActivityRepository } from 'src/interfaces/activity.interface';
export const newActivityRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/album.repository.mock.ts b/server/test/repositories/album.repository.mock.ts
index b50fe6750..626358d81 100644
--- a/server/test/repositories/album.repository.mock.ts
+++ b/server/test/repositories/album.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IAlbumRepository } from 'src/interfaces/album.repository';
+import { IAlbumRepository } from 'src/interfaces/album.interface';
export const newAlbumRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/api-key.repository.mock.ts b/server/test/repositories/api-key.repository.mock.ts
index d74a41028..32b8388a3 100644
--- a/server/test/repositories/api-key.repository.mock.ts
+++ b/server/test/repositories/api-key.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IKeyRepository } from 'src/interfaces/api-key.repository';
+import { IKeyRepository } from 'src/interfaces/api-key.interface';
export const newKeyRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/asset-stack.repository.mock.ts b/server/test/repositories/asset-stack.repository.mock.ts
index cf38d045f..76ada96cd 100644
--- a/server/test/repositories/asset-stack.repository.mock.ts
+++ b/server/test/repositories/asset-stack.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IAssetStackRepository } from 'src/interfaces/asset-stack.repository';
+import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
export const newAssetStackRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts
index c72d00414..22d4f3707 100644
--- a/server/test/repositories/asset.repository.mock.ts
+++ b/server/test/repositories/asset.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IAssetRepository } from 'src/interfaces/asset.repository';
+import { IAssetRepository } from 'src/interfaces/asset.interface';
export const newAssetRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/audit.repository.mock.ts b/server/test/repositories/audit.repository.mock.ts
index 654f67794..9e4adf560 100644
--- a/server/test/repositories/audit.repository.mock.ts
+++ b/server/test/repositories/audit.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IAuditRepository } from 'src/interfaces/audit.repository';
+import { IAuditRepository } from 'src/interfaces/audit.interface';
export const newAuditRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/communication.repository.mock.ts b/server/test/repositories/communication.repository.mock.ts
index 3d4eaeb37..38284f377 100644
--- a/server/test/repositories/communication.repository.mock.ts
+++ b/server/test/repositories/communication.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ICommunicationRepository } from 'src/interfaces/communication.repository';
+import { ICommunicationRepository } from 'src/interfaces/communication.interface';
export const newCommunicationRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/crypto.repository.mock.ts b/server/test/repositories/crypto.repository.mock.ts
index 6cf4adffd..cbd90ec67 100644
--- a/server/test/repositories/crypto.repository.mock.ts
+++ b/server/test/repositories/crypto.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ICryptoRepository } from 'src/interfaces/crypto.repository';
+import { ICryptoRepository } from 'src/interfaces/crypto.interface';
export const newCryptoRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts
index 82abe16a8..704189571 100644
--- a/server/test/repositories/database.repository.mock.ts
+++ b/server/test/repositories/database.repository.mock.ts
@@ -1,5 +1,5 @@
-import { Version } from 'src/domain/domain.constant';
-import { IDatabaseRepository } from 'src/interfaces/database.repository';
+import { IDatabaseRepository } from 'src/interfaces/database.interface';
+import { Version } from 'src/utils/version';
export const newDatabaseRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/job.repository.mock.ts b/server/test/repositories/job.repository.mock.ts
index 851da70da..9cd21fe87 100644
--- a/server/test/repositories/job.repository.mock.ts
+++ b/server/test/repositories/job.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IJobRepository } from 'src/interfaces/job.repository';
+import { IJobRepository } from 'src/interfaces/job.interface';
export const newJobRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts
index b3c62d681..6cdfb38f4 100644
--- a/server/test/repositories/library.repository.mock.ts
+++ b/server/test/repositories/library.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ILibraryRepository } from 'src/interfaces/library.repository';
+import { ILibraryRepository } from 'src/interfaces/library.interface';
export const newLibraryRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/machine-learning.repository.mock.ts b/server/test/repositories/machine-learning.repository.mock.ts
index 50da10cb4..bc35b4c85 100644
--- a/server/test/repositories/machine-learning.repository.mock.ts
+++ b/server/test/repositories/machine-learning.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IMachineLearningRepository } from 'src/interfaces/machine-learning.repository';
+import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface';
export const newMachineLearningRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/media.repository.mock.ts b/server/test/repositories/media.repository.mock.ts
index d1806c636..b904766ea 100644
--- a/server/test/repositories/media.repository.mock.ts
+++ b/server/test/repositories/media.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IMediaRepository } from 'src/interfaces/media.repository';
+import { IMediaRepository } from 'src/interfaces/media.interface';
export const newMediaRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/metadata.repository.mock.ts b/server/test/repositories/metadata.repository.mock.ts
index a5f13daf5..ec21ab8c1 100644
--- a/server/test/repositories/metadata.repository.mock.ts
+++ b/server/test/repositories/metadata.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IMetadataRepository } from 'src/interfaces/metadata.repository';
+import { IMetadataRepository } from 'src/interfaces/metadata.interface';
export const newMetadataRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/move.repository.mock.ts b/server/test/repositories/move.repository.mock.ts
index 265799ab6..b7adec2a7 100644
--- a/server/test/repositories/move.repository.mock.ts
+++ b/server/test/repositories/move.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IMoveRepository } from 'src/interfaces/move.repository';
+import { IMoveRepository } from 'src/interfaces/move.interface';
export const newMoveRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/partner.repository.mock.ts b/server/test/repositories/partner.repository.mock.ts
index 74c42ba8d..04370730b 100644
--- a/server/test/repositories/partner.repository.mock.ts
+++ b/server/test/repositories/partner.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IPartnerRepository } from 'src/interfaces/partner.repository';
+import { IPartnerRepository } from 'src/interfaces/partner.interface';
export const newPartnerRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/person.repository.mock.ts b/server/test/repositories/person.repository.mock.ts
index b4b817535..5b94fbc3d 100644
--- a/server/test/repositories/person.repository.mock.ts
+++ b/server/test/repositories/person.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IPersonRepository } from 'src/interfaces/person.repository';
+import { IPersonRepository } from 'src/interfaces/person.interface';
export const newPersonRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/search.repository.mock.ts b/server/test/repositories/search.repository.mock.ts
index eb6c13ea4..24e648ee2 100644
--- a/server/test/repositories/search.repository.mock.ts
+++ b/server/test/repositories/search.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ISearchRepository } from 'src/interfaces/search.repository';
+import { ISearchRepository } from 'src/interfaces/search.interface';
export const newSearchRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/shared-link.repository.mock.ts b/server/test/repositories/shared-link.repository.mock.ts
index ea084c1ed..2fcaf7aee 100644
--- a/server/test/repositories/shared-link.repository.mock.ts
+++ b/server/test/repositories/shared-link.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ISharedLinkRepository } from 'src/interfaces/shared-link.repository';
+import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface';
export const newSharedLinkRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts
index 72b87aed1..d5049999c 100644
--- a/server/test/repositories/storage.repository.mock.ts
+++ b/server/test/repositories/storage.repository.mock.ts
@@ -1,6 +1,6 @@
import { WatchOptions } from 'chokidar';
import { StorageCore } from 'src/cores/storage.core';
-import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.repository';
+import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.interface';
interface MockWatcherOptions {
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>;
diff --git a/server/test/repositories/system-config.repository.mock.ts b/server/test/repositories/system-config.repository.mock.ts
index 89ce6a050..0ef11ce18 100644
--- a/server/test/repositories/system-config.repository.mock.ts
+++ b/server/test/repositories/system-config.repository.mock.ts
@@ -1,5 +1,5 @@
import { SystemConfigCore } from 'src/cores/system-config.core';
-import { ISystemConfigRepository } from 'src/interfaces/system-config.repository';
+import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
export const newSystemConfigRepositoryMock = (reset = true): jest.Mocked => {
if (reset) {
diff --git a/server/test/repositories/system-info.repository.mock.ts b/server/test/repositories/system-info.repository.mock.ts
index 93ec9c7dd..bdc11f9d6 100644
--- a/server/test/repositories/system-info.repository.mock.ts
+++ b/server/test/repositories/system-info.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IServerInfoRepository } from 'src/interfaces/server-info.repository';
+import { IServerInfoRepository } from 'src/interfaces/server-info.interface';
export const newServerInfoRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/system-metadata.repository.mock.ts b/server/test/repositories/system-metadata.repository.mock.ts
index f93278aaa..5ffc5dd89 100644
--- a/server/test/repositories/system-metadata.repository.mock.ts
+++ b/server/test/repositories/system-metadata.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.repository';
+import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
export const newSystemMetadataRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts
index 9f5283664..0c31c546c 100644
--- a/server/test/repositories/tag.repository.mock.ts
+++ b/server/test/repositories/tag.repository.mock.ts
@@ -1,4 +1,4 @@
-import { ITagRepository } from 'src/interfaces/tag.repository';
+import { ITagRepository } from 'src/interfaces/tag.interface';
export const newTagRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/user-token.repository.mock.ts b/server/test/repositories/user-token.repository.mock.ts
index 23de88d35..b3fa7e73f 100644
--- a/server/test/repositories/user-token.repository.mock.ts
+++ b/server/test/repositories/user-token.repository.mock.ts
@@ -1,4 +1,4 @@
-import { IUserTokenRepository } from 'src/interfaces/user-token.repository';
+import { IUserTokenRepository } from 'src/interfaces/user-token.interface';
export const newUserTokenRepositoryMock = (): jest.Mocked => {
return {
diff --git a/server/test/repositories/user.repository.mock.ts b/server/test/repositories/user.repository.mock.ts
index 4db0b16af..80d9a4cfd 100644
--- a/server/test/repositories/user.repository.mock.ts
+++ b/server/test/repositories/user.repository.mock.ts
@@ -1,5 +1,5 @@
import { UserCore } from 'src/cores/user.core';
-import { IUserRepository } from 'src/interfaces/user.repository';
+import { IUserRepository } from 'src/interfaces/user.interface';
export const newUserRepositoryMock = (reset = true): jest.Mocked => {
if (reset) {
diff --git a/server/test/utils.ts b/server/test/utils.ts
index 777a6a00c..c7732eabc 100644
--- a/server/test/utils.ts
+++ b/server/test/utils.ts
@@ -5,17 +5,14 @@ import fs from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { EventEmitter } from 'node:stream';
-import { Server } from 'node:tls';
-import { ApiModule } from 'src/apps/api.module';
-import { ApiService } from 'src/apps/api.service';
-import { MicroservicesService } from 'src/apps/microservices.service';
-import { QueueName } from 'src/domain/job/job.constants';
-import { dataSource } from 'src/infra/database.config';
-import { InfraModule, InfraTestModule } from 'src/infra/infra.module';
-import { IJobRepository, JobItem, JobItemHandler } from 'src/interfaces/job.repository';
-import { IMediaRepository } from 'src/interfaces/media.repository';
-import { StorageEventType } from 'src/interfaces/storage.repository';
+import { AppTestModule } from 'src/app.module';
+import { dataSource } from 'src/database.config';
+import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface';
+import { IMediaRepository } from 'src/interfaces/media.interface';
+import { StorageEventType } from 'src/interfaces/storage.interface';
import { MediaRepository } from 'src/repositories/media.repository';
+import { ApiService } from 'src/services/api.service';
+import { MicroservicesService } from 'src/services/microservices.service';
import { EntityTarget, ObjectLiteral } from 'typeorm';
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string;
@@ -105,12 +102,7 @@ let app: INestApplication;
export const testApp = {
create: async (): Promise => {
- const moduleFixture = await Test.createTestingModule({
- imports: [ApiModule],
- providers: [ApiService, MicroservicesService],
- })
- .overrideModule(InfraModule)
- .useModule(InfraTestModule)
+ const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] })
.overrideProvider(IJobRepository)
.useClass(JobMock)
.overrideProvider(IMediaRepository)
@@ -118,15 +110,11 @@ export const testApp = {
.compile();
app = await moduleFixture.createNestApplication().init();
- await app.listen(0);
+ await app.get(ApiService).init();
await db.reset();
await app.get(ApiService).init();
await app.get(MicroservicesService).init();
- const port = app.getHttpServer().address().port;
- const protocol = app instanceof Server ? 'https' : 'http';
- process.env.IMMICH_INSTANCE_URL = protocol + '://127.0.0.1:' + port;
-
return app;
},
reset: async (options?: ResetOptions) => {
diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
index 4d09e1705..3a5105629 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte
@@ -5,20 +5,31 @@
import { getAssetJobName } from '$lib/utils';
import { clickOutside } from '$lib/utils/click-outside';
import { getContextMenuPosition } from '$lib/utils/context-menu';
- import { AssetJobName, AssetTypeEnum, type AssetResponseDto } from '@immich/sdk';
+ import { AssetJobName, AssetTypeEnum, type AssetResponseDto, type AlbumResponseDto } from '@immich/sdk';
import {
+ mdiAccountCircleOutline,
mdiAlertOutline,
+ mdiArchiveArrowDownOutline,
+ mdiArchiveArrowUpOutline,
mdiArrowLeft,
+ mdiCogRefreshOutline,
mdiContentCopy,
+ mdiDatabaseRefreshOutline,
mdiDeleteOutline,
mdiDotsVertical,
+ mdiFolderDownloadOutline,
mdiHeart,
mdiHeartOutline,
+ mdiImageAlbum,
+ mdiImageMinusOutline,
+ mdiImageOutline,
+ mdiImageRefreshOutline,
mdiInformationOutline,
mdiMagnifyMinusOutline,
mdiMagnifyPlusOutline,
mdiMotionPauseOutline,
mdiPlaySpeed,
+ mdiPresentationPlay,
mdiShareVariantOutline,
} from '@mdi/js';
import { createEventDispatcher } from 'svelte';
@@ -26,6 +37,7 @@
import MenuOption from '../shared-components/context-menu/menu-option.svelte';
export let asset: AssetResponseDto;
+ export let album: AlbumResponseDto | null = null;
export let showCopyButton: boolean;
export let showZoomButton: boolean;
export let showMotionPlayButton: boolean;
@@ -42,6 +54,7 @@
| 'addToAlbum'
| 'addToSharedAlbum'
| 'asProfileImage'
+ | 'setAsAlbumCover'
| 'download'
| 'playSlideShow'
| 'runJob'
@@ -59,6 +72,7 @@
addToAlbum: void;
addToSharedAlbum: void;
asProfileImage: void;
+ setAsAlbumCover: void;
runJob: AssetJobName;
playSlideShow: void;
unstack: void;
@@ -173,37 +187,55 @@
{#if isShowAssetOptions}
{#if showSlideshow}
- onMenuClick('playSlideShow')} text="Slideshow" />
+ onMenuClick('playSlideShow')} text="Slideshow" />
{/if}
{#if showDownloadButton}
- onMenuClick('download')} text="Download" />
+ onMenuClick('download')} text="Download" />
{/if}
- onMenuClick('addToAlbum')} text="Add to Album" />
- onMenuClick('addToSharedAlbum')} text="Add to Shared Album" />
+ onMenuClick('addToAlbum')} text="Add to album" />
+ onMenuClick('addToSharedAlbum')}
+ text="Add to shared album"
+ />
{#if isOwner}
+ {#if hasStackChildren}
+ onMenuClick('unstack')} text="Un-stack" />
+ {/if}
+ {#if album}
+ onMenuClick('setAsAlbumCover')}
+ />
+ {/if}
+ {#if asset.type === AssetTypeEnum.Image}
+ onMenuClick('asProfileImage')}
+ text="Set as profile picture"
+ />
+ {/if}
dispatch('toggleArchive')}
+ icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
text={asset.isArchived ? 'Unarchive' : 'Archive'}
/>
- {#if asset.type === AssetTypeEnum.Image}
- onMenuClick('asProfileImage')} text="As profile picture" />
- {/if}
-
- {#if hasStackChildren}
- onMenuClick('unstack')} text="Un-Stack" />
- {/if}
-
+
onJobClick(AssetJobName.RefreshMetadata)}
text={getAssetJobName(AssetJobName.RefreshMetadata)}
/>
onJobClick(AssetJobName.RegenerateThumbnail)}
text={getAssetJobName(AssetJobName.RegenerateThumbnail)}
/>
{#if asset.type === AssetTypeEnum.Video}
onJobClick(AssetJobName.TranscodeVideo)}
text={getAssetJobName(AssetJobName.TranscodeVideo)}
/>
diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte
index f9d2f787b..51da30411 100644
--- a/web/src/lib/components/asset-viewer/asset-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte
@@ -97,6 +97,9 @@
let isShowActivity = false;
let isLiked: ActivityResponseDto | null = null;
let numberOfComments: number;
+ let fullscreenElement: Element;
+
+ $: isFullScreen = fullscreenElement !== null;
$: {
if (asset.stackCount && asset.stack) {
@@ -512,6 +515,8 @@
]}
/>
+
+
assetViewerHtmlElement.requestFullscreen()}
onPrevious={() => navigateAsset('previous')}
onNext={() => navigateAsset('next')}
onClose={() => ($slideshowState = SlideshowState.StopSlideshow)}
diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
index 37cd7f701..03e8c9df4 100644
--- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte
+++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte
@@ -3,23 +3,46 @@
import ProgressBar, { ProgressBarStatus } from '$lib/components/shared-components/progress-bar/progress-bar.svelte';
import SlideshowSettings from '$lib/components/slideshow-settings.svelte';
import { SlideshowNavigation, slideshowStore } from '$lib/stores/slideshow.store';
- import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiPause, mdiPlay } from '@mdi/js';
+ import { mdiChevronLeft, mdiChevronRight, mdiClose, mdiCog, mdiFullscreen, mdiPause, mdiPlay } from '@mdi/js';
import { onDestroy, onMount } from 'svelte';
+ import { fly } from 'svelte/transition';
+ export let isFullScreen: boolean;
export let onNext = () => {};
export let onPrevious = () => {};
export let onClose = () => {};
+ export let onSetToFullScreen = () => {};
const { restartProgress, stopProgress, slideshowDelay, showProgressBar, slideshowNavigation } = slideshowStore;
let progressBarStatus: ProgressBarStatus;
let progressBar: ProgressBar;
let showSettings = false;
+ let showControls = true;
+ let timer: NodeJS.Timeout;
+ let isOverControls = false;
let unsubscribeRestart: () => void;
let unsubscribeStop: () => void;
+ const resetTimer = () => {
+ clearTimeout(timer);
+ document.body.style.cursor = '';
+ showControls = true;
+ startTimer();
+ };
+
+ const startTimer = () => {
+ timer = setTimeout(() => {
+ if (!isOverControls) {
+ showControls = false;
+ document.body.style.cursor = 'none';
+ }
+ }, 10_000);
+ };
+
onMount(() => {
+ startTimer();
unsubscribeRestart = restartProgress.subscribe((value) => {
if (value) {
progressBar.restart(value);
@@ -52,19 +75,37 @@
};
-
-
- (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
- title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
- />
-
-
- (showSettings = !showSettings)} title="Next" />
-
+
+{#if showControls}
+
+ (isOverControls = true)}
+ on:mouseleave={() => (isOverControls = false)}
+ transition:fly={{ duration: 150 }}
+ >
+
+
+ (progressBarStatus === ProgressBarStatus.Paused ? progressBar.play() : progressBar.pause())}
+ title={progressBarStatus === ProgressBarStatus.Paused ? 'Play' : 'Pause'}
+ />
+
+
+ (showSettings = !showSettings)} title="Next" />
+ {#if !isFullScreen}
+
+ {/if}
+
+{/if}
{#if showSettings}
(showSettings = false)} />
{/if}
diff --git a/web/src/lib/components/asset-viewer/video-viewer.svelte b/web/src/lib/components/asset-viewer/video-viewer.svelte
index f5ff5f0fc..c53fcf600 100644
--- a/web/src/lib/components/asset-viewer/video-viewer.svelte
+++ b/web/src/lib/components/asset-viewer/video-viewer.svelte
@@ -9,7 +9,9 @@
export let assetId: string;
+ let element: HTMLVideoElement | undefined = undefined;
let isVideoLoading = true;
+
const dispatch = createEventDispatcher<{ onVideoEnded: void; onVideoStarted: void }>();
const handleCanPlay = async (event: Event) => {
@@ -29,6 +31,7 @@
diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte
index 19fd73d25..28a2b7253 100644
--- a/web/src/lib/components/shared-components/combobox.svelte
+++ b/web/src/lib/components/shared-components/combobox.svelte
@@ -18,6 +18,7 @@
import type { FormEventHandler } from 'svelte/elements';
import { shortcuts } from '$lib/utils/shortcut';
import { clickOutside } from '$lib/utils/click-outside';
+ import { focusOutside } from '$lib/utils/focus-outside';
/**
* Unique identifier for the combobox.
@@ -40,6 +41,7 @@
let searchQuery = selectedOption?.label || '';
let selectedIndex: number | undefined;
let optionRefs: HTMLElement[] = [];
+ let input: HTMLInputElement;
const inputId = `combobox-${id}`;
const listboxId = `listbox-${id}`;
@@ -51,7 +53,6 @@
const dispatch = createEventDispatcher<{
select: ComboBoxOption | undefined;
- click: void;
}>();
const activate = () => {
@@ -101,6 +102,8 @@
};
const onClear = () => {
+ input?.focus();
+ selectedIndex = undefined;
selectedOption = undefined;
searchQuery = '';
dispatch('select', selectedOption);
@@ -111,11 +114,16 @@
{
- if (e.relatedTarget instanceof Node && !e.currentTarget.contains(e.relatedTarget)) {
- deactivate();
- }
- }}
+ use:focusOutside={{ onFocusOut: deactivate }}
+ use:shortcuts={[
+ {
+ shortcut: { key: 'Escape' },
+ onShortcut: (event) => {
+ event.stopPropagation();
+ closeDropdown();
+ },
+ },
+ ]}
>
{#if isActive}
@@ -133,6 +141,7 @@
aria-controls={listboxId}
aria-expanded={isOpen}
autocomplete="off"
+ bind:this={input}
class:!pl-8={isActive}
class:!rounded-b-none={isOpen}
class:cursor-pointer={!isActive}
@@ -213,9 +222,7 @@
role="option"
aria-selected={selectedIndex === 0}
aria-disabled={true}
- class:bg-gray-100={selectedIndex === 0}
- class:dark:bg-gray-700={selectedIndex === 0}
- class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default"
+ class="text-left w-full px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-default aria-selected:bg-gray-100 aria-selected:dark:bg-gray-700"
id={`${listboxId}-${0}`}
on:click={() => closeDropdown()}
>
diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte
index c10371d83..aabf8c25c 100644
--- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte
+++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte
@@ -23,12 +23,14 @@
diff --git a/web/src/lib/components/shared-components/context-menu/menu-option.svelte b/web/src/lib/components/shared-components/context-menu/menu-option.svelte
index d2e2eb69b..6d5adc419 100644
--- a/web/src/lib/components/shared-components/context-menu/menu-option.svelte
+++ b/web/src/lib/components/shared-components/context-menu/menu-option.svelte
@@ -1,6 +1,9 @@