Compare commits

..

4 Commits

Author SHA1 Message Date
Alex
9764326880 merge main 2024-10-31 11:14:26 -05:00
Alex
1ec5d89723 custom group id 2024-07-31 17:24:06 -05:00
Alex
17181e3330 share handler 2024-07-31 13:51:30 -05:00
Alex
7eccf99797 revert to no flavor 2024-07-31 12:34:42 -05:00
146 changed files with 1904 additions and 1598 deletions

View File

@@ -1,4 +1,4 @@
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3 AS core
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff AS core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

10
cli/package-lock.json generated
View File

@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -59,7 +59,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"typescript": "^5.3.3"
}
},
@@ -1378,9 +1378,9 @@
}
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"dev": true,
"license": "MIT",
"dependencies": {

View File

@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",

View File

@@ -26,6 +26,7 @@ The default configuration looks like this:
"bframes": -1,
"refs": 0,
"gopSize": 0,
"npl": 0,
"temporalAQ": false,
"cqMode": "auto",
"twoPass": false,

View File

@@ -83,12 +83,6 @@ const projects: CommunityProjectProps[] = [
description: 'Power tools for organizing your immich library.',
url: 'https://github.com/varun-raj/immich-power-tools',
},
{
title: 'Immich Public Proxy',
description:
'Share your Immich photos and albums in a safe way without exposing your Immich instance to the public.',
url: 'https://github.com/alangrainger/immich-public-proxy',
},
];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {

34
e2e/package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -99,7 +99,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"typescript": "^5.3.3"
}
},
@@ -1613,9 +1613,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3294,9 +3294,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3307,14 +3307,14 @@
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0"
"exiftool-vendored.exe": "12.97.0",
"exiftool-vendored.pl": "12.97.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -3323,9 +3323,9 @@
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
"dev": true,
"license": "MIT",
"optional": true,

View File

@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",

View File

@@ -1148,78 +1148,6 @@ describe('/asset', () => {
},
},
},
{
input: 'formats/raw/Canon/PowerShot_G12.CR2',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'PowerShot_G12.CR2',
fileCreatedAt: '2015-12-27T09:55:40.000Z',
exifInfo: {
make: 'Canon',
model: 'Canon PowerShot G12',
exifImageHeight: 2736,
exifImageWidth: 3648,
exposureTime: '1/1000',
fNumber: 4,
focalLength: 18.098,
iso: 80,
lensModel: null,
fileSizeInByte: 11_113_617,
dateTimeOriginal: '2015-12-27T09:55:40.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
{
input: 'formats/raw/Fujifilm/X100V_compressed.RAF',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'X100V_compressed.RAF',
fileCreatedAt: '2024-10-12T21:01:01.000Z',
exifInfo: {
make: 'FUJIFILM',
model: 'X100V',
exifImageHeight: 4160,
exifImageWidth: 6240,
exposureTime: '1/4000',
fNumber: 16,
focalLength: 23,
iso: 160,
lensModel: null,
fileSizeInByte: 13_551_312,
dateTimeOriginal: '2024-10-12T21:01:01.000Z',
latitude: null,
longitude: null,
orientation: '6',
},
},
},
{
input: 'formats/raw/Ricoh/GR3/Ricoh_GR3-450.DNG',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'Ricoh_GR3-450.DNG',
fileCreatedAt: '2024-06-08T13:48:39.000Z',
exifInfo: {
dateTimeOriginal: '2024-06-08T13:48:39.000Z',
exifImageHeight: 4064,
exifImageWidth: 6112,
exposureTime: '1/400',
fNumber: 5,
fileSizeInByte: 31_175_472,
focalLength: 18.3,
iso: 100,
latitude: 36.613_24,
lensModel: 'GR LENS 18.3mm F2.8',
longitude: -121.897_85,
make: 'RICOH IMAGING COMPANY, LTD.',
model: 'RICOH GR III',
orientation: '1',
},
},
},
];
it(`should upload and generate a thumbnail for different file types`, async () => {

View File

@@ -305,6 +305,8 @@
"transcoding_threads_description": "Higher values lead to faster encoding, but leave less room for the server to process other tasks while active. This value should not be more than the number of CPU cores. Maximizes utilization if set to 0.",
"transcoding_tone_mapping": "Tone-mapping",
"transcoding_tone_mapping_description": "Attempts to preserve the appearance of HDR videos when converted to SDR. Each algorithm makes different tradeoffs for color, detail and brightness. Hable preserves detail, Mobius preserves color, and Reinhard preserves brightness.",
"transcoding_tone_mapping_npl": "Tone-mapping NPL",
"transcoding_tone_mapping_npl_description": "Colors will be adjusted to look normal for a display of this brightness. Counter-intuitively, lower values increase the brightness of the video and vice versa since it compensates for the brightness of the display. 0 sets this value automatically.",
"transcoding_transcode_policy": "Transcode policy",
"transcoding_transcode_policy_description": "Policy for when a video should be transcoded. HDR videos will always be transcoded (except if transcoding is disabled).",
"transcoding_two_pass_encoding": "Two-pass encoding",

View File

@@ -946,13 +946,13 @@ tqdm = ["tqdm"]
[[package]]
name = "ftfy"
version = "6.3.1"
version = "6.3.0"
description = "Fixes mojibake and other problems with Unicode, after the fact"
optional = false
python-versions = ">=3.9"
files = [
{file = "ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083"},
{file = "ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec"},
{file = "ftfy-6.3.0-py3-none-any.whl", hash = "sha256:17aca296801f44142e3ff2c16f93fbf6a87609ebb3704a9a41dd5d4903396caf"},
{file = "ftfy-6.3.0.tar.gz", hash = "sha256:1c7d6418e72b25a7760feb150acf574b86924dbb2e95b32c0b3abbd1ba3d7ad6"},
]
[package.dependencies]
@@ -1609,13 +1609,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.32.1"
version = "2.32.0"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.32.1-py3-none-any.whl", hash = "sha256:3fb5548b4f2b6477fa5229ee55ac3dddbae56e86c3430bf2ba3fee358eb7e7bb"},
{file = "locust-2.32.1.tar.gz", hash = "sha256:8c3b1094dbf20860fd2f6e26b68f0c6064dc28054f4462664389d102fce1448b"},
{file = "locust-2.32.0-py3-none-any.whl", hash = "sha256:e004514332b8631ca91382d11d224baee4ced040c5f5c8b2233800ebcbc73c0e"},
{file = "locust-2.32.0.tar.gz", hash = "sha256:d8f7f5d9d4e801b2e7b0ee3f31109333673da744ccedf85e7da0151f2d263dd9"},
]
[package.dependencies]
@@ -2749,13 +2749,13 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
version = "0.0.17"
version = "0.0.12"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
]
[[package]]

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.24.4"
"flutter": "3.24.3"
}

View File

@@ -32,6 +32,13 @@ target 'Runner' do
use_modular_headers!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
# share_handler addition start
target 'ShareExtension' do
inherit! :search_paths
pod "share_handler_ios_models", :path => ".symlinks/plugins/share_handler_ios/ios/Models"
end
# share_handler addition end
end
post_install do |installer|

View File

@@ -48,7 +48,7 @@ PODS:
- flutter_udid (0.0.1):
- Flutter
- SAMKeychain
- flutter_web_auth (0.6.0):
- flutter_web_auth (0.5.0):
- Flutter
- fluttertoast (0.0.2):
- Flutter
@@ -81,6 +81,14 @@ PODS:
- SDWebImage (5.19.4):
- SDWebImage/Core (= 5.19.4)
- SDWebImage/Core (5.19.4)
- share_handler_ios (0.0.13):
- Flutter
- share_handler_ios/share_handler_ios_models (= 0.0.13)
- share_handler_ios_models
- share_handler_ios/share_handler_ios_models (0.0.13):
- Flutter
- share_handler_ios_models
- share_handler_ios_models (0.0.9)
- share_plus (0.0.1):
- Flutter
- shared_preferences_foundation (0.0.1):
@@ -120,6 +128,8 @@ DEPENDENCIES:
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
- photo_manager (from `.symlinks/plugins/photo_manager/ios`)
- share_handler_ios (from `.symlinks/plugins/share_handler_ios/ios`)
- share_handler_ios_models (from `.symlinks/plugins/share_handler_ios/ios/Models`)
- share_plus (from `.symlinks/plugins/share_plus/ios`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
@@ -178,6 +188,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/permission_handler_apple/ios"
photo_manager:
:path: ".symlinks/plugins/photo_manager/ios"
share_handler_ios:
:path: ".symlinks/plugins/share_handler_ios/ios"
share_handler_ios_models:
:path: ".symlinks/plugins/share_handler_ios/ios/Models"
share_plus:
:path: ".symlinks/plugins/share_plus/ios"
shared_preferences_foundation:
@@ -202,7 +216,7 @@ SPEC CHECKSUMS:
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
@@ -217,6 +231,8 @@ SPEC CHECKSUMS:
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
share_handler_ios: 088138c17ce73333b4875a35860cd52604041521
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
@@ -226,6 +242,6 @@ SPEC CHECKSUMS:
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
PODFILE CHECKSUM: 1d6c11dc7539b1a7e0f9cc83a63904e0f8555f72
COCOAPODS: 1.15.2

View File

@@ -16,8 +16,22 @@
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
FA74AF1DE1B65E796CC9A190 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F2F1F1634A89EAAC89566694 /* Pods_ShareExtension.framework */; };
FA87C46B2C5AADE800109ACC /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA87C46A2C5AADE800109ACC /* ShareViewController.swift */; };
FA87C46E2C5AADE800109ACC /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = FA87C46C2C5AADE800109ACC /* MainInterface.storyboard */; };
FA87C4722C5AADE800109ACC /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = FA87C4682C5AADE800109ACC /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
FA87C4702C5AADE800109ACC /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
proxyType = 1;
remoteGlobalIDString = FA87C4672C5AADE800109ACC;
remoteInfo = ShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
isa = PBXCopyFilesBuildPhase;
@@ -29,6 +43,17 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
FA87C4732C5AADE800109ACC /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
FA87C4722C5AADE800109ACC /* ShareExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
@@ -41,16 +66,26 @@
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
7B82909F828DFBC0DC7A92BF /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = "<group>"; };
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
B4CED4956FDE3EF1F786D7AD /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = "<group>"; };
D1C2E3E4FBFF4719EF41D366 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = "<group>"; };
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
F2F1F1634A89EAAC89566694 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; };
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
FA87C4682C5AADE800109ACC /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
FA87C46A2C5AADE800109ACC /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
FA87C46D2C5AADE800109ACC /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = "<group>"; };
FA87C46F2C5AADE800109ACC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
FA87C4782C5AAE3500109ACC /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = "<group>"; };
FA87C4792C5AAE5300109ACC /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = "<group>"; };
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
/* End PBXFileReference section */
@@ -63,6 +98,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
FA87C4652C5AADE800109ACC /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
FA74AF1DE1B65E796CC9A190 /* Pods_ShareExtension.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
@@ -72,6 +115,9 @@
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
B4CED4956FDE3EF1F786D7AD /* Pods-ShareExtension.debug.xcconfig */,
D1C2E3E4FBFF4719EF41D366 /* Pods-ShareExtension.release.xcconfig */,
7B82909F828DFBC0DC7A92BF /* Pods-ShareExtension.profile.xcconfig */,
);
path = Pods;
sourceTree = "<group>";
@@ -80,6 +126,7 @@
isa = PBXGroup;
children = (
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
F2F1F1634A89EAAC89566694 /* Pods_ShareExtension.framework */,
);
name = Frameworks;
sourceTree = "<group>";
@@ -109,6 +156,7 @@
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
FA87C4692C5AADE800109ACC /* ShareExtension */,
97C146EF1CF9000F007C117D /* Products */,
0FB772A5B9601143383626CA /* Pods */,
1754452DD81DA6620E279E51 /* Frameworks */,
@@ -118,7 +166,8 @@
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Immich-Debug.app */,
97C146EE1CF9000F007C117D /* Runner.app */,
FA87C4682C5AADE800109ACC /* ShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@@ -126,6 +175,7 @@
97C146F01CF9000F007C117D /* Runner */ = {
isa = PBXGroup;
children = (
FA87C4782C5AAE3500109ACC /* Runner.entitlements */,
65DD438629917FAD0047FFA8 /* BackgroundSync */,
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
97C146FA1CF9000F007C117D /* Main.storyboard */,
@@ -140,6 +190,17 @@
path = Runner;
sourceTree = "<group>";
};
FA87C4692C5AADE800109ACC /* ShareExtension */ = {
isa = PBXGroup;
children = (
FA87C4792C5AAE5300109ACC /* ShareExtension.entitlements */,
FA87C46A2C5AADE800109ACC /* ShareViewController.swift */,
FA87C46C2C5AADE800109ACC /* MainInterface.storyboard */,
FA87C46F2C5AADE800109ACC /* Info.plist */,
);
path = ShareExtension;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ -153,6 +214,7 @@
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
FA87C4732C5AADE800109ACC /* Embed Foundation Extensions */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */,
@@ -160,12 +222,31 @@
buildRules = (
);
dependencies = (
FA87C4712C5AADE800109ACC /* PBXTargetDependency */,
);
name = Runner;
productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
productType = "com.apple.product-type.application";
};
FA87C4672C5AADE800109ACC /* ShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = FA87C4772C5AADE800109ACC /* Build configuration list for PBXNativeTarget "ShareExtension" */;
buildPhases = (
81EA71A2AAF4A216CCE6E79E /* [CP] Check Pods Manifest.lock */,
FA87C4642C5AADE800109ACC /* Sources */,
FA87C4652C5AADE800109ACC /* Frameworks */,
FA87C4662C5AADE800109ACC /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = ShareExtension;
productName = ShareExtension;
productReference = FA87C4682C5AADE800109ACC /* ShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
@@ -173,6 +254,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastSwiftUpdateCheck = 1510;
LastUpgradeCheck = 1510;
ORGANIZATIONNAME = "";
TargetAttributes = {
@@ -180,6 +262,9 @@
CreatedOnToolsVersion = 7.3.1;
LastSwiftMigration = 1100;
};
FA87C4672C5AADE800109ACC = {
CreatedOnToolsVersion = 15.1;
};
};
};
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
@@ -196,6 +281,7 @@
projectRoot = "";
targets = (
97C146ED1CF9000F007C117D /* Runner */,
FA87C4672C5AADE800109ACC /* ShareExtension */,
);
};
/* End PBXProject section */
@@ -212,6 +298,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
FA87C4662C5AADE800109ACC /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FA87C46E2C5AADE800109ACC /* MainInterface.storyboard in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */
@@ -270,6 +364,28 @@
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
81EA71A2AAF4A216CCE6E79E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1;
@@ -316,8 +432,24 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
FA87C4642C5AADE800109ACC /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
FA87C46B2C5AADE800109ACC /* ShareViewController.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
FA87C4712C5AADE800109ACC /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = FA87C4672C5AADE800109ACC /* ShareExtension */;
targetProxy = FA87C4702C5AADE800109ACC /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
isa = PBXVariantGroup;
@@ -335,6 +467,14 @@
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
FA87C46C2C5AADE800109ACC /* MainInterface.storyboard */ = {
isa = PBXVariantGroup;
children = (
FA87C46D2C5AADE800109ACC /* Base */,
);
name = MainInterface.storyboard;
sourceTree = "<group>";
};
/* End PBXVariantGroup section */
/* Begin XCBuildConfiguration section */
@@ -396,23 +536,25 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
PRODUCT_NAME = "Immich-Profile";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -539,22 +681,25 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug;
PRODUCT_NAME = "Immich-Debug";
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -567,22 +712,25 @@
isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 181;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = Immich;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0;
@@ -590,6 +738,129 @@
};
name = Release;
};
FA87C4742C5AADE800109ACC /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = B4CED4956FDE3EF1F786D7AD /* Pods-ShareExtension.debug.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
FA87C4752C5AADE800109ACC /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = D1C2E3E4FBFF4719EF41D366 /* Pods-ShareExtension.release.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
FA87C4762C5AADE800109ACC /* Profile */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 7B82909F828DFBC0DC7A92BF /* Pods-ShareExtension.profile.xcconfig */;
buildSettings = {
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++20";
CLANG_ENABLE_OBJC_WEAK = YES;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
CUSTOM_GROUP_ID = group.app.immich.share;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_USER_SCRIPT_SANDBOXING = YES;
GCC_C_LANGUAGE_STANDARD = gnu17;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = ShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
LOCALIZATION_PREFERS_STRING_CATALOGS = YES;
MARKETING_VERSION = 1.0;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.ShareExtension;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Profile;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
@@ -613,6 +884,16 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
FA87C4772C5AADE800109ACC /* Build configuration list for PBXNativeTarget "ShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
FA87C4742C5AADE800109ACC /* Debug */,
FA87C4752C5AADE800109ACC /* Release */,
FA87C4762C5AADE800109ACC /* Profile */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
/* End XCConfigurationList section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;

View File

@@ -15,7 +15,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -31,7 +31,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -56,7 +56,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
@@ -73,7 +73,7 @@
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Immich.app"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>

View File

@@ -1,124 +1,163 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
<string>ca</string>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>he</string>
<string>hi</string>
<string>hu</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>lv</string>
<string>mn</string>
<string>nb</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ro</string>
<string>ru</string>
<string>sk</string>
<string>sl</string>
<string>sr</string>
<string>sv</string>
<string>th</string>
<string>uk</string>
<string>vi</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.119.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>181</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>
<false/>
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true/>
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true/>
<key>io.flutter.embedded_views_preview</key>
<true/>
</dict>
</plist>
<dict>
<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
<string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string>
</array>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true />
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Immich</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleLocalizations</key>
<array>
<string>en</string>
<string>ar</string>
<string>ca</string>
<string>cs</string>
<string>da</string>
<string>de</string>
<string>es</string>
<string>fi</string>
<string>fr</string>
<string>he</string>
<string>hi</string>
<string>hu</string>
<string>it</string>
<string>ja</string>
<string>ko</string>
<string>lv</string>
<string>mn</string>
<string>nb</string>
<string>nl</string>
<string>pl</string>
<string>pt</string>
<string>ro</string>
<string>ru</string>
<string>sk</string>
<string>sl</string>
<string>sr</string>
<string>sv</string>
<string>th</string>
<string>uk</string>
<string>vi</string>
<string>zh</string>
</array>
<key>CFBundleName</key>
<string>immich_mobile</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.107.1</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>162</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>
<false />
<key>LSApplicationQueriesSchemes</key>
<array>
<string>https</string>
</array>
<key>LSRequiresIPhoneOS</key>
<true />
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true />
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true />
</dict>
<key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string>
<key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true />
<key>UIBackgroundModes</key>
<array>
<string>fetch</string>
<string>processing</string>
</array>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UIStatusBarHidden</key>
<false />
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<true />
<key>io.flutter.embedded_views_preview</key>
<true />
<!-- Add for share_handler start -->
<!-- The 'NSUserActivityTypes' key is only needed if you plan to use the recordSentMessage API
allowing for conversations to show up as direct share suggestions -->
<key>NSUserActivityTypes</key>
<array>
<string>INSendMessageIntent</string>
</array>
<!-- Uncomment below lines if you want to use a custom group id rather than the default. Set it
in Build Settings -> User-Defined -->
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<!-- Add for share_handler end -->
</dict>
</plist>

View File

@@ -1,5 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>

View File

@@ -4,5 +4,9 @@
<dict>
<key>aps-environment</key>
<string>development</string>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="13122.16" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="j1y-V4-xli">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="13104.12"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Share View Controller-->
<scene sceneID="ceB-am-kn3">
<objects>
<viewController id="j1y-V4-xli" customClass="ShareViewController" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" opaque="NO" contentMode="scaleToFill" id="wbc-yd-nQP">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" red="0.0" green="0.0" blue="0.0" alpha="0.0" colorSpace="custom" customColorSpace="sRGB"/>
<viewLayoutGuide key="safeArea" id="1Xd-am-t49"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="CEy-Cv-SGf" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>AppGroupId</key>
<string>$(CUSTOM_GROUP_ID)</string>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>IntentsSupported</key>
<array>
<string>INSendMessageIntent</string>
</array>
<key>NSExtensionActivationRule</key>
<string>SUBQUERY ( extensionItems, $extensionItem, SUBQUERY ( $extensionItem.attachments,
$attachment, ( ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.file-url"
|| ANY $attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.image" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.text" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.movie" || ANY
$attachment.registeredTypeIdentifiers UTI-CONFORMS-TO "public.url" ) ).@count &gt; 0 ).@count
&gt; 0 </string>
<key>PHSupportedMediaTypes</key>
<array>
<string>Video</string>
<string>Image</string>
</array>
</dict>
<key>NSExtensionMainStoryboard</key>
<string>MainInterface</string>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
</dict>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.app.immich.share</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,10 @@
//
// ShareViewController.swift
// ShareExtension
//
// Created by Alex Tran on 7/31/24.
//
import share_handler_ios_models
class ShareViewController: ShareHandlerIosViewController {}

View File

@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:share_handler/share_handler.dart';
import 'package:immich_mobile/utils/download.dart';
import 'package:timezone/data/latest.dart';
import 'package:immich_mobile/constants/locales.dart';
@@ -197,6 +198,29 @@ class ImmichAppState extends ConsumerState<ImmichApp>
// needs to be delayed so that EasyLocalization is working
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
});
initPlatformState();
}
SharedMedia? media;
// Platform messages are asynchronous, so we initialize in an async method.
Future<void> initPlatformState() async {
final handler = ShareHandlerPlatform.instance;
media = await handler.getInitialSharedMedia();
handler.sharedMediaStream.listen((SharedMedia media) {
if (!mounted) return;
setState(() {
this.media = media;
print("Received shared media: $media");
});
});
if (!mounted) return;
setState(() {
// _platformVersion = platformVersion;
});
}
@override
@@ -214,7 +238,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales,
locale: context.locale,
debugShowCheckedModeBanner: true,
debugShowCheckedModeBanner: false,
home: MaterialApp.router(
title: 'Immich',
debugShowCheckedModeBanner: false,

View File

@@ -84,48 +84,34 @@ class AssetNotifier extends StateNotifier<bool> {
_deleteInProgress = true;
state = true;
try {
// Filter the assets based on the backed-up status
final assets = onlyBackedUp
? deleteAssets.where((e) => e.storage == AssetState.merged)
: deleteAssets;
if (assets.isEmpty) {
return false; // No assets to delete
}
// Proceed with local deletion of the filtered assets
final localDeleted = await _deleteLocalAssets(assets);
if (localDeleted.isNotEmpty) {
final localOnlyIds = assets
final localOnlyIds = deleteAssets
.where((e) => e.storage == AssetState.local)
.map((e) => e.id)
.toList();
// Update merged assets to remote-only
// Update merged assets to remote only
final mergedAssets =
assets.where((e) => e.storage == AssetState.merged).map((e) {
deleteAssets.where((e) => e.storage == AssetState.merged).map((e) {
e.localId = null;
return e;
}).toList();
// Update the local database
await _db.writeTxn(() async {
if (mergedAssets.isNotEmpty) {
await _db.assets
.putAll(mergedAssets); // Use the filtered merged assets
await _db.assets.putAll(mergedAssets);
}
await _db.exifInfos.deleteAll(localOnlyIds);
await _db.assets.deleteAll(localOnlyIds);
});
return true;
}
} finally {
_deleteInProgress = false;
state = false;
}
return false;
}

View File

@@ -203,30 +203,18 @@ class MultiselectGrid extends HookConsumerWidget {
void onDeleteLocal(bool onlyBackedUp) async {
processing.value = true;
try {
// Select only the local assets from the selection
final localIds = selection.value.where((a) => a.isLocal).toList();
// Delete only the backed-up assets if 'onlyBackedUp' is true
final isDeleted = await ref
.read(assetProvider.notifier)
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
if (isDeleted) {
// Show a toast with the correct number of deleted assets
final deletedCount = localIds
.where(
(e) => !onlyBackedUp || e.isRemote,
) // Only count backed-up assets
.length;
ImmichToast.show(
context: context,
msg: 'assets_removed_permanently_from_device'
.tr(args: ["$deletedCount"]),
.tr(args: ["${localIds.length}"]),
gravity: ToastGravity.BOTTOM,
);
// Reset the selection
selectionEnabledHook.value = false;
}
} finally {

View File

@@ -23,6 +23,7 @@ class SystemConfigFFmpegDto {
required this.crf,
required this.gopSize,
required this.maxBitrate,
required this.npl,
required this.preferredHwDevice,
required this.preset,
required this.refs,
@@ -61,6 +62,9 @@ class SystemConfigFFmpegDto {
String maxBitrate;
/// Minimum value: 0
int npl;
String preferredHwDevice;
String preset;
@@ -98,6 +102,7 @@ class SystemConfigFFmpegDto {
other.crf == crf &&
other.gopSize == gopSize &&
other.maxBitrate == maxBitrate &&
other.npl == npl &&
other.preferredHwDevice == preferredHwDevice &&
other.preset == preset &&
other.refs == refs &&
@@ -123,6 +128,7 @@ class SystemConfigFFmpegDto {
(crf.hashCode) +
(gopSize.hashCode) +
(maxBitrate.hashCode) +
(npl.hashCode) +
(preferredHwDevice.hashCode) +
(preset.hashCode) +
(refs.hashCode) +
@@ -136,7 +142,7 @@ class SystemConfigFFmpegDto {
(twoPass.hashCode);
@override
String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
String toString() => 'SystemConfigFFmpegDto[accel=$accel, accelDecode=$accelDecode, acceptedAudioCodecs=$acceptedAudioCodecs, acceptedContainers=$acceptedContainers, acceptedVideoCodecs=$acceptedVideoCodecs, bframes=$bframes, cqMode=$cqMode, crf=$crf, gopSize=$gopSize, maxBitrate=$maxBitrate, npl=$npl, preferredHwDevice=$preferredHwDevice, preset=$preset, refs=$refs, targetAudioCodec=$targetAudioCodec, targetResolution=$targetResolution, targetVideoCodec=$targetVideoCodec, temporalAQ=$temporalAQ, threads=$threads, tonemap=$tonemap, transcode=$transcode, twoPass=$twoPass]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
@@ -150,6 +156,7 @@ class SystemConfigFFmpegDto {
json[r'crf'] = this.crf;
json[r'gopSize'] = this.gopSize;
json[r'maxBitrate'] = this.maxBitrate;
json[r'npl'] = this.npl;
json[r'preferredHwDevice'] = this.preferredHwDevice;
json[r'preset'] = this.preset;
json[r'refs'] = this.refs;
@@ -183,6 +190,7 @@ class SystemConfigFFmpegDto {
crf: mapValueOfType<int>(json, r'crf')!,
gopSize: mapValueOfType<int>(json, r'gopSize')!,
maxBitrate: mapValueOfType<String>(json, r'maxBitrate')!,
npl: mapValueOfType<int>(json, r'npl')!,
preferredHwDevice: mapValueOfType<String>(json, r'preferredHwDevice')!,
preset: mapValueOfType<String>(json, r'preset')!,
refs: mapValueOfType<int>(json, r'refs')!,
@@ -251,6 +259,7 @@ class SystemConfigFFmpegDto {
'crf',
'gopSize',
'maxBitrate',
'npl',
'preferredHwDevice',
'preset',
'refs',

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ version: 1.119.1+164
environment:
sdk: '>=3.3.0 <4.0.0'
flutter: 3.24.4
flutter: 3.24.3
dependencies:
flutter:
@@ -42,7 +42,7 @@ dependencies:
path_provider: ^2.1.2
collection: ^1.18.0
http_parser: ^4.0.2
flutter_web_auth: ^0.6.0
flutter_web_auth: ^0.5.0
easy_image_viewer: ^1.4.0
isar: ^3.1.0+1
isar_flutter_libs: ^3.1.0+1
@@ -57,6 +57,8 @@ dependencies:
async: ^2.11.0
dynamic_color: ^1.7.0 #package to apply system theme
background_downloader: ^8.5.5
share_handler: ^0.0.21
share_handler_ios:
#image editing packages
crop_image: ^1.0.13

View File

@@ -11621,6 +11621,10 @@
"maxBitrate": {
"type": "string"
},
"npl": {
"minimum": 0,
"type": "integer"
},
"preferredHwDevice": {
"type": "string"
},
@@ -11669,6 +11673,7 @@
"crf",
"gopSize",
"maxBitrate",
"npl",
"preferredHwDevice",
"preset",
"refs",

View File

@@ -1,18 +1,18 @@
{
"name": "@immich/sdk",
"version": "1.119.1",
"version": "1.111.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.119.1",
"version": "1.111.0",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.8.5",
"@types/node": "^20.14.12",
"typescript": "^5.3.3"
}
},
@@ -22,19 +22,19 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "20.14.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.8"
"undici-types": "~5.26.4"
}
},
"node_modules/typescript": {
"version": "5.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -46,11 +46,10 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"dev": true,
"license": "MIT"
"version": "5.26.5",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
"dev": true
}
}
}

View File

@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"typescript": "^5.3.3"
},
"repository": {

View File

@@ -1104,6 +1104,7 @@ export type SystemConfigFFmpegDto = {
crf: number;
gopSize: number;
maxBitrate: string;
npl: number;
preferredHwDevice: string;
preset: string;
refs: number;

View File

@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3 AS web
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff AS web
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

View File

@@ -83,7 +83,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",
@@ -5110,9 +5110,9 @@
}
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"dependencies": {
"undici-types": "~6.19.8"
}
@@ -8236,9 +8236,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
"dependencies": {
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
@@ -8247,23 +8247,23 @@
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0"
"exiftool-vendored.exe": "12.97.0",
"exiftool-vendored.pl": "12.97.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
"optional": true,
"os": [
"!win32"
@@ -18258,9 +18258,9 @@
}
},
"@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.8.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
"requires": {
"undici-types": "~6.19.8"
}
@@ -20579,29 +20579,29 @@
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
},
"exiftool-vendored": {
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"version": "28.6.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
"requires": {
"@photostructure/tz-lookup": "^11.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0",
"exiftool-vendored.exe": "12.97.0",
"exiftool-vendored.pl": "12.97.0",
"he": "^1.2.0",
"luxon": "^3.5.0"
}
},
"exiftool-vendored.exe": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
"optional": true
},
"exiftool-vendored.pl": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"version": "12.97.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
"optional": true
},
"express": {

View File

@@ -108,7 +108,7 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.8.5",
"@types/node": "^22.8.1",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",

View File

@@ -6,12 +6,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { ClsModule } from 'nestjs-cls';
import { OpenTelemetryModule } from 'nestjs-otel';
import { commands } from 'src/commands';
import { IWorker } from 'src/constants';
import { controllers } from 'src/controllers';
import { entities } from 'src/entities';
import { ImmichWorker } from 'src/enum';
import { IEventRepository } from 'src/interfaces/event.interface';
import { IJobRepository } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import { AuthGuard } from 'src/middleware/auth.guard';
@@ -58,25 +56,23 @@ const imports = [
TypeOrmModule.forFeature(entities),
];
class BaseModule implements OnModuleInit, OnModuleDestroy {
abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
private get worker() {
return this.getWorker();
}
constructor(
@Inject(IWorker) private worker: ImmichWorker,
@Inject(ILoggerRepository) logger: ILoggerRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
) {
logger.setAppName(this.worker);
}
abstract getWorker(): ImmichWorker;
async onModuleInit() {
this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) });
this.jobRepository.setup({ services });
if (this.worker === ImmichWorker.MICROSERVICES) {
this.jobRepository.startWorkers();
}
this.eventRepository.setup({ services });
await this.eventRepository.emit('app.bootstrap', this.worker);
}
@@ -90,15 +86,23 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
@Module({
imports: [...imports, ScheduleModule.forRoot()],
controllers: [...controllers],
providers: [...common, ...middleware, { provide: IWorker, useValue: ImmichWorker.API }],
providers: [...common, ...middleware],
})
export class ApiModule extends BaseModule {}
export class ApiModule extends BaseModule {
getWorker() {
return ImmichWorker.API;
}
}
@Module({
imports: [...imports],
providers: [...common, { provide: IWorker, useValue: ImmichWorker.MICROSERVICES }, SchedulerRegistry],
providers: [...common, SchedulerRegistry],
})
export class MicroservicesModule extends BaseModule {}
export class MicroservicesModule extends BaseModule {
getWorker() {
return ImmichWorker.MICROSERVICES;
}
}
@Module({
imports: [...imports],

View File

@@ -3,7 +3,7 @@ import { ImmichWorker } from 'src/enum';
import { ConfigRepository } from 'src/repositories/config.repository';
const main = async () => {
const { host, workers, port } = new ConfigRepository().getEnv();
const { workers, port } = new ConfigRepository().getEnv();
if (!workers.includes(ImmichWorker.API)) {
process.exit();
}
@@ -11,7 +11,7 @@ const main = async () => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 2000);
try {
const response = await fetch(`http://${host || 'localhost'}:${port}/api/server/ping`, {
const response = await fetch(`http://localhost:${port}/api/server/ping`, {
signal: controller.signal,
});

View File

@@ -36,6 +36,7 @@ export interface SystemConfig {
bframes: number;
refs: number;
gopSize: number;
npl: number;
temporalAQ: boolean;
cqMode: CQMode;
twoPass: boolean;
@@ -177,6 +178,7 @@ export const defaults = Object.freeze<SystemConfig>({
bframes: -1,
refs: 0,
gopSize: 0,
npl: 0,
temporalAQ: false,
cqMode: CQMode.AUTO,
twoPass: false,

View File

@@ -13,8 +13,6 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
export const SALT_ROUNDS = 10;
export const IWorker = 'IWorker';
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
export const serverVersion = new SemVer(version);

View File

@@ -4,7 +4,6 @@ import _ from 'lodash';
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
import { MetadataKey } from 'src/enum';
import { EmitEvent } from 'src/interfaces/event.interface';
import { JobName, QueueName } from 'src/interfaces/job.interface';
import { setUnion } from 'src/utils/set';
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
@@ -123,12 +122,6 @@ export type EventConfig = {
};
export const OnEvent = (config: EventConfig) => SetMetadata(MetadataKey.EVENT_CONFIG, config);
export type JobConfig = {
name: JobName;
queue: QueueName;
};
export const OnJob = (config: JobConfig) => SetMetadata(MetadataKey.JOB_CONFIG, config);
type LifecycleRelease = 'NEXT_RELEASE' | string;
type LifecycleMetadata = {
addedAt?: LifecycleRelease;

View File

@@ -134,6 +134,12 @@ export class SystemConfigFFmpegDto {
@ApiProperty({ type: 'integer' })
gopSize!: number;
@IsInt()
@Min(0)
@Type(() => Number)
@ApiProperty({ type: 'integer' })
npl!: number;
@ValidateBoolean()
temporalAQ!: boolean;

View File

@@ -335,7 +335,6 @@ export enum MetadataKey {
SHARED_ROUTE = 'shared_route',
API_KEY_SECURITY = 'api_key',
EVENT_CONFIG = 'event_config',
JOB_CONFIG = 'job_config',
TELEMETRY_ENABLED = 'telemetry_enabled',
}

View File

@@ -3,7 +3,6 @@ import { SystemConfig } from 'src/config';
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { ImmichWorker } from 'src/enum';
import { JobItem, QueueName } from 'src/interfaces/job.interface';
export const IEventRepository = 'IEventRepository';
@@ -39,8 +38,6 @@ type EventMap = {
'assets.delete': [{ assetIds: string[]; userId: string }];
'assets.restore': [{ assetIds: string[]; userId: string }];
'job.start': [QueueName, JobItem];
// session events
'session.delete': [{ sessionId: string }];

View File

@@ -1,4 +1,3 @@
import { ClassConstructor } from 'class-transformer';
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
export enum QueueName {
@@ -239,8 +238,8 @@ export type JobItem =
// Migration
| { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
| { name: JobName.MIGRATE_ASSET; data: IEntityJob }
| { name: JobName.MIGRATE_PERSON; data: IEntityJob }
| { name: JobName.MIGRATE_ASSET; data?: IEntityJob }
| { name: JobName.MIGRATE_PERSON; data?: IEntityJob }
// Metadata Extraction
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
@@ -287,7 +286,7 @@ export type JobItem =
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; data: IEntityJob }
| { name: JobName.LIBRARY_SYNC_ASSET; data: ILibraryAssetJob }
| { name: JobName.LIBRARY_SYNC_ASSET; data: IEntityJob }
| { name: JobName.LIBRARY_DELETE; data: IEntityJob }
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
@@ -306,15 +305,14 @@ export enum JobStatus {
FAILED = 'failed',
SKIPPED = 'skipped',
}
export type Jobs = { [K in JobItem['name']]: (JobItem & { name: K })['data'] };
export type JobOf<T extends JobName> = Jobs[T];
export type JobHandler<T = any> = (data: T) => Promise<JobStatus>;
export type JobItemHandler = (item: JobItem) => Promise<void>;
export const IJobRepository = 'IJobRepository';
export interface IJobRepository {
setup(options: { services: ClassConstructor<unknown>[] }): void;
startWorkers(): void;
run(job: JobItem): Promise<JobStatus>;
addHandler(queueName: QueueName, concurrency: number, handler: JobItemHandler): void;
addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void;
updateCronJob(name: string, expression?: string, start?: boolean): void;
setConcurrency(queueName: QueueName, concurrency: number): void;

View File

@@ -59,7 +59,6 @@ export interface VideoStreamInfo {
frameCount: number;
isHDR: boolean;
bitrate: number;
pixelFormat: string;
}
export interface AudioStreamInfo {

View File

@@ -1,12 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class RemoveNplFromSystemConfig1730227312171 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
update system_metadata
set value = value #- '{ffmpeg,npl}'
where key = 'system-config' and value->'ffmpeg'->'npl' is not null`);
}
public async down(): Promise<void> {}
}

View File

@@ -1,122 +1,124 @@
import { getQueueToken } from '@nestjs/bullmq';
import { Inject, Injectable } from '@nestjs/common';
import { ModuleRef, Reflector } from '@nestjs/core';
import { ModuleRef } from '@nestjs/core';
import { SchedulerRegistry } from '@nestjs/schedule';
import { JobsOptions, Queue, Worker } from 'bullmq';
import { ClassConstructor } from 'class-transformer';
import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
import { CronJob, CronTime } from 'cron';
import { setTimeout } from 'node:timers/promises';
import { JobConfig } from 'src/decorators';
import { MetadataKey } from 'src/enum';
import { IConfigRepository } from 'src/interfaces/config.interface';
import { IEventRepository } from 'src/interfaces/event.interface';
import {
IEntityJob,
IJobRepository,
JobCounts,
JobItem,
JobName,
JobOf,
JobStatus,
QueueCleanType,
QueueName,
QueueStatus,
} from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
type JobMapItem = {
jobName: JobName;
queueName: QueueName;
handler: (job: JobOf<any>) => Promise<JobStatus>;
label: string;
export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
// 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.CLEAN_OLD_SESSION_TOKENS]: QueueName.BACKGROUND_TASK,
[JobName.PERSON_CLEANUP]: QueueName.BACKGROUND_TASK,
[JobName.USER_SYNC_USAGE]: QueueName.BACKGROUND_TASK,
// backups
[JobName.BACKUP_DATABASE]: QueueName.BACKUP_DATABASE,
// conversion
[JobName.QUEUE_VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
[JobName.VIDEO_CONVERSION]: QueueName.VIDEO_CONVERSION,
// thumbnails
[JobName.QUEUE_GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_THUMBNAILS]: QueueName.THUMBNAIL_GENERATION,
[JobName.GENERATE_PERSON_THUMBNAIL]: QueueName.THUMBNAIL_GENERATION,
// tags
[JobName.TAG_CLEANUP]: QueueName.BACKGROUND_TASK,
// 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,
// duplicate detection
[JobName.QUEUE_DUPLICATE_DETECTION]: QueueName.DUPLICATE_DETECTION,
[JobName.DUPLICATE_DETECTION]: QueueName.DUPLICATE_DETECTION,
// 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_SYNC_FILE]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_FILES]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: QueueName.LIBRARY,
[JobName.LIBRARY_DELETE]: QueueName.LIBRARY,
[JobName.LIBRARY_SYNC_ASSET]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_SYNC_ALL]: QueueName.LIBRARY,
[JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY,
// Notification
[JobName.SEND_EMAIL]: QueueName.NOTIFICATION,
[JobName.NOTIFY_ALBUM_INVITE]: QueueName.NOTIFICATION,
[JobName.NOTIFY_ALBUM_UPDATE]: QueueName.NOTIFICATION,
[JobName.NOTIFY_SIGNUP]: QueueName.NOTIFICATION,
// Version check
[JobName.VERSION_CHECK]: QueueName.BACKGROUND_TASK,
// Trash
[JobName.QUEUE_TRASH_EMPTY]: QueueName.BACKGROUND_TASK,
};
@Injectable()
export class JobRepository implements IJobRepository {
private workers: Partial<Record<QueueName, Worker>> = {};
private handlers: Partial<Record<JobName, JobMapItem>> = {};
constructor(
private moduleRef: ModuleRef,
private schedulerRegistry: SchedulerRegistry,
private moduleReference: ModuleRef,
private schedulerReqistry: SchedulerRegistry,
@Inject(IConfigRepository) private configRepository: IConfigRepository,
@Inject(IEventRepository) private eventRepository: IEventRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
this.logger.setContext(JobRepository.name);
}
setup({ services }: { services: ClassConstructor<unknown>[] }) {
const reflector = this.moduleRef.get(Reflector, { strict: false });
// discovery
for (const Service of services) {
const instance = this.moduleRef.get<any>(Service);
for (const methodName of getMethodNames(instance)) {
const handler = instance[methodName];
const config = reflector.get<JobConfig>(MetadataKey.JOB_CONFIG, handler);
if (!config) {
continue;
}
const { name: jobName, queue: queueName } = config;
const label = `${Service.name}.${handler.name}`;
// one handler per job
if (this.handlers[jobName]) {
const jobKey = getKeyByValue(JobName, jobName);
const errorMessage = `Failed to add job handler for ${label}`;
this.logger.error(
`${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName].label}.`,
);
throw new ImmichStartupError(errorMessage);
}
this.handlers[jobName] = {
label,
jobName,
queueName,
handler: handler.bind(instance),
};
this.logger.verbose(`Added job handler: ${jobName} => ${label}`);
}
}
// no missing handlers
for (const [jobKey, jobName] of Object.entries(JobName)) {
const item = this.handlers[jobName];
if (!item) {
const errorMessage = `Failed to find job handler for Job.${jobKey} ("${jobName}")`;
this.logger.error(
`${errorMessage}. Make sure to add the @OnJob({ name: JobName.${jobKey}, queue: QueueName.XYZ }) decorator for the new job.`,
);
throw new ImmichStartupError(errorMessage);
}
}
}
startWorkers() {
addHandler(queueName: QueueName, concurrency: number, handler: (item: JobItem) => Promise<void>) {
const { bull } = this.configRepository.getEnv();
for (const queueName of Object.values(QueueName)) {
this.logger.debug(`Starting worker for queue: ${queueName}`);
this.workers[queueName] = new Worker(
queueName,
(job) => this.eventRepository.emit('job.start', queueName, job as JobItem),
{ ...bull.config, concurrency: 1 },
);
}
}
async run({ name, data }: JobItem) {
const item = this.handlers[name as JobName];
if (!item) {
this.logger.warn(`Skipping unknown job: "${name}"`);
return JobStatus.SKIPPED;
}
return item.handler(data);
const workerHandler: Processor = async (job: Job) => handler(job as JobItem);
const workerOptions: WorkerOptions = { ...bull.config, concurrency };
this.workers[queueName] = new Worker(queueName, workerHandler, workerOptions);
}
addCronJob(name: string, expression: string, onTick: () => void, start = true): void {
@@ -139,11 +141,11 @@ export class JobRepository implements IJobRepository {
true,
);
this.schedulerRegistry.addCronJob(name, job);
this.schedulerReqistry.addCronJob(name, job);
}
updateCronJob(name: string, expression?: string, start?: boolean): void {
const job = this.schedulerRegistry.getCronJob(name);
const job = this.schedulerReqistry.getCronJob(name);
if (expression) {
job.setTime(new CronTime(expression));
}
@@ -202,10 +204,6 @@ export class JobRepository implements IJobRepository {
) as unknown as Promise<JobCounts>;
}
private getQueueName(name: JobName) {
return (this.handlers[name] as JobMapItem).queueName;
}
async queueAll(items: JobItem[]): Promise<void> {
if (items.length === 0) {
return;
@@ -214,7 +212,7 @@ export class JobRepository implements IJobRepository {
const promises = [];
const itemsByQueue = {} as Record<string, (JobItem & { data: any; options: JobsOptions | undefined })[]>;
for (const item of items) {
const queueName = this.getQueueName(item.name);
const queueName = JOBS_TO_QUEUE[item.name];
const job = {
name: item.name,
data: item.data || {},
@@ -275,11 +273,11 @@ export class JobRepository implements IJobRepository {
}
private getQueue(queue: QueueName): Queue {
return this.moduleRef.get<Queue>(getQueueToken(queue), { strict: false });
return this.moduleReference.get<Queue>(getQueueToken(queue), { strict: false });
}
public async removeJob(jobId: string, name: JobName): Promise<IEntityJob | undefined> {
const existingJob = await this.getQueue(this.getQueueName(name)).getJob(jobId);
const existingJob = await this.getQueue(JOBS_TO_QUEUE[name]).getJob(jobId);
if (!existingJob) {
return;
}

View File

@@ -126,7 +126,6 @@ export class MediaRepository implements IMediaRepository {
rotation: this.parseInt(stream.rotation),
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
bitrate: this.parseInt(stream.bit_rate),
pixelFormat: stream.pix_fmt || 'yuv420p',
})),
audioStreams: results.streams
.filter((stream) => stream.codec_type === 'audio')

View File

@@ -1,7 +1,6 @@
import { BadRequestException } from '@nestjs/common';
import _ from 'lodash';
import { DateTime, Duration } from 'luxon';
import { OnJob } from 'src/decorators';
import {
AssetResponseDto,
MemoryLaneResponseDto,
@@ -22,13 +21,12 @@ import { MemoryLaneDto } from 'src/dtos/search.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetStatus, Permission } from 'src/enum';
import {
IAssetDeleteJob,
ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
JobOf,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
@@ -188,7 +186,6 @@ export class AssetService extends BaseService {
await this.assetRepository.updateAll(ids, options);
}
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
async handleAssetDeletionCheck(): Promise<JobStatus> {
const config = await this.getConfig({ withCache: false });
const trashedDays = config.trash.enabled ? config.trash.days : 0;
@@ -214,8 +211,7 @@ export class AssetService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.ASSET_DELETION, queue: QueueName.BACKGROUND_TASK })
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
const { id, deleteOnDisk } = job;
const asset = await this.assetRepository.getById(id, {

View File

@@ -3,7 +3,6 @@ import { DateTime } from 'luxon';
import { resolve } from 'node:path';
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import {
AuditDeletesDto,
AuditDeletesResponseDto,
@@ -22,14 +21,13 @@ import {
StorageFolder,
UserPathType,
} from 'src/enum';
import { JobName, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { usePagination } from 'src/utils/pagination';
@Injectable()
export class AuditService extends BaseService {
@OnJob({ name: JobName.CLEAN_OLD_AUDIT_LOGS, queue: QueueName.BACKGROUND_TASK })
async handleCleanup(): Promise<JobStatus> {
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
return JobStatus.SUCCESS;

View File

@@ -1,11 +1,11 @@
import { Injectable } from '@nestjs/common';
import { default as path } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { ImmichWorker, StorageFolder } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JobName, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { handlePromiseError } from 'src/utils/misc';
import { validateCronExpression } from 'src/validation';
@@ -75,7 +75,6 @@ export class BackupService extends BaseService {
this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`);
}
@OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE })
async handleBackupDatabase(): Promise<JobStatus> {
this.logger.debug(`Database Backup Started`);

View File

@@ -1,10 +1,9 @@
import { BadRequestException, Inject, Optional } from '@nestjs/common';
import { BadRequestException, Inject } from '@nestjs/common';
import sanitize from 'sanitize-filename';
import { SystemConfig } from 'src/config';
import { IWorker, SALT_ROUNDS } from 'src/constants';
import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { UserEntity } from 'src/entities/user.entity';
import { ImmichWorker } from 'src/enum';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IActivityRepository } from 'src/interfaces/activity.interface';
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
@@ -50,7 +49,6 @@ export class BaseService {
protected storageCore: StorageCore;
constructor(
@Inject(IWorker) @Optional() protected worker: ImmichWorker | undefined,
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
@Inject(IActivityRepository) protected activityRepository: IActivityRepository,

View File

@@ -31,22 +31,10 @@ describe(SearchService.name, () => {
describe('getDuplicates', () => {
it('should get duplicates', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe, assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{
duplicateId: assetStub.hasDupe.duplicateId,
assets: [
expect.objectContaining({ id: assetStub.hasDupe.id }),
expect.objectContaining({ id: assetStub.hasDupe.id }),
],
},
]);
});
it('should update assets with duplicateId', async () => {
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
]);
});
});

View File

@@ -1,11 +1,10 @@
import { Injectable } from '@nestjs/common';
import { OnJob } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { IBaseJob, IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { AssetDuplicateResult } from 'src/interfaces/search.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
@@ -16,28 +15,11 @@ import { usePagination } from 'src/utils/pagination';
export class DuplicateService extends BaseService {
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
const uniqueAssetIds: string[] = [];
const duplicates = mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))).filter(
(duplicate) => {
if (duplicate.assets.length === 1) {
uniqueAssetIds.push(duplicate.assets[0].id);
return false;
}
return true;
},
);
if (uniqueAssetIds.length > 0) {
try {
await this.assetRepository.updateAll(uniqueAssetIds, { duplicateId: null });
} catch (error: any) {
this.logger.error(`Failed to remove duplicateId from assets: ${error.message}`);
}
}
return duplicates;
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true })));
}
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
async handleQueueSearchDuplicates({ force }: JobOf<JobName.QUEUE_DUPLICATE_DETECTION>): Promise<JobStatus> {
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -58,8 +40,7 @@ export class DuplicateService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
async handleSearchDuplicates({ id }: JobOf<JobName.DUPLICATE_DETECTION>): Promise<JobStatus> {
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;

View File

@@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service';
import { MediaService } from 'src/services/media.service';
import { MemoryService } from 'src/services/memory.service';
import { MetadataService } from 'src/services/metadata.service';
import { MicroservicesService } from 'src/services/microservices.service';
import { NotificationService } from 'src/services/notification.service';
import { PartnerService } from 'src/services/partner.service';
import { PersonService } from 'src/services/person.service';
@@ -59,6 +60,7 @@ export const services = [
MediaService,
MemoryService,
MetadataService,
MicroservicesService,
NotificationService,
PartnerService,
PersonService,

View File

@@ -2,25 +2,37 @@ import { BadRequestException } from '@nestjs/common';
import { defaults } from 'src/config';
import { ImmichWorker } from 'src/enum';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
import {
IJobRepository,
JobCommand,
JobHandler,
JobItem,
JobName,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { JobService } from 'src/services/job.service';
import { assetStub } from 'test/fixtures/asset.stub';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';
import { Mocked, vitest } from 'vitest';
const makeMockHandlers = (status: JobStatus) => {
const mock = vitest.fn().mockResolvedValue(status);
return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
JobName,
JobHandler
>;
};
describe(JobService.name, () => {
let sut: JobService;
let assetMock: Mocked<IAssetRepository>;
let jobMock: Mocked<IJobRepository>;
let loggerMock: Mocked<ILoggerRepository>;
let telemetryMock: Mocked<ITelemetryRepository>;
let systemMock: Mocked<ISystemMetadataRepository>;
beforeEach(() => {
({ sut, assetMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {
worker: ImmichWorker.MICROSERVICES,
}));
({ sut, assetMock, jobMock, systemMock } = newTestService(JobService));
});
it('should work', () => {
@@ -29,6 +41,7 @@ describe(JobService.name, () => {
describe('onConfigUpdate', () => {
it('should update concurrency', () => {
sut.onBootstrap(ImmichWorker.MICROSERVICES);
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
@@ -212,19 +225,11 @@ describe(JobService.name, () => {
});
});
describe('onJobStart', () => {
it('should process a successful job', async () => {
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
await sut.onJobStart(QueueName.BACKGROUND_TASK, {
name: JobName.DELETE_FILES,
data: { files: ['path/to/file'] },
});
expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', 1);
expect(telemetryMock.jobs.addToGauge).toHaveBeenCalledWith('immich.queues.background_task.active', -1);
expect(telemetryMock.jobs.addToCounter).toHaveBeenCalledWith('immich.jobs.delete_files.success', 1);
expect(loggerMock.error).not.toHaveBeenCalled();
describe('init', () => {
it('should register a handler for each queue', async () => {
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
expect(systemMock.get).toHaveBeenCalled();
expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
});
const tests: Array<{ item: JobItem; jobs: JobName[] }> = [
@@ -292,9 +297,8 @@ describe(JobService.name, () => {
}
}
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
await jobMock.addHandler.mock.calls[0][2](item);
if (jobs.length > 1) {
expect(jobMock.queueAll).toHaveBeenCalledWith(
@@ -309,9 +313,8 @@ describe(JobService.name, () => {
});
it(`should not queue any jobs when ${item.name} fails`, async () => {
jobMock.run.mockResolvedValue(JobStatus.FAILED);
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
await sut.init(makeMockHandlers(JobStatus.FAILED));
await jobMock.addHandler.mock.calls[0][2](item);
expect(jobMock.queueAll).not.toHaveBeenCalled();
});

View File

@@ -4,10 +4,11 @@ import { OnEvent } from 'src/decorators';
import { mapAsset } from 'src/dtos/asset-response.dto';
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
import { AssetType, ImmichWorker, ManualJobName } from 'src/enum';
import { ArgOf, ArgsOf } from 'src/interfaces/event.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import {
ConcurrentQueueName,
JobCommand,
JobHandler,
JobItem,
JobName,
JobStatus,
@@ -38,9 +39,16 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
@Injectable()
export class JobService extends BaseService {
private isMicroservices = false;
@OnEvent({ name: 'app.bootstrap' })
onBootstrap(app: ArgOf<'app.bootstrap'>) {
this.isMicroservices = app === ImmichWorker.MICROSERVICES;
}
@OnEvent({ name: 'config.update', server: true })
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
if (this.worker !== ImmichWorker.MICROSERVICES) {
onConfigUpdate({ newConfig: config, oldConfig }: ArgOf<'config.update'>) {
if (!oldConfig || !this.isMicroservices) {
return;
}
@@ -169,21 +177,41 @@ export class JobService extends BaseService {
}
}
@OnEvent({ name: 'job.start' })
async onJobStart(...[queueName, job]: ArgsOf<'job.start'>) {
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
try {
const status = await this.jobRepository.run(job);
const jobMetric = `immich.jobs.${job.name.replaceAll('-', '_')}.${status}`;
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
await this.onDone(job);
async init(jobHandlers: Record<JobName, JobHandler>) {
const config = await this.getConfig({ withCache: false });
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;
if (this.isConcurrentQueue(queueName)) {
concurrency = config.job[queueName].concurrency;
}
} catch (error: Error | any) {
this.logger.error(`Unable to run job handler (${queueName}/${job.name}): ${error}`, error?.stack, job.data);
} finally {
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
this.logger.debug(`Registering ${queueName} with a concurrency of ${concurrency}`);
this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise<void> => {
const { name, data } = item;
const handler = jobHandlers[name];
if (!handler) {
this.logger.warn(`Skipping unknown job: "${name}"`);
return;
}
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
try {
const status = await handler(data);
const jobMetric = `immich.jobs.${name.replaceAll('-', '_')}.${status}`;
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
await this.onDone(item);
}
} catch (error: Error | any) {
this.logger.error(`Unable to run job handler (${queueName}/${name}): ${error}`, error?.stack, data);
} finally {
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
}
});
}
}

View File

@@ -3,7 +3,7 @@ import { R_OK } from 'node:constants';
import path, { basename, isAbsolute, parse } from 'node:path';
import picomatch from 'picomatch';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import {
CreateLibraryDto,
LibraryResponseDto,
@@ -19,7 +19,14 @@ import { LibraryEntity } from 'src/entities/library.entity';
import { AssetType, ImmichWorker } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobOf, JOBS_LIBRARY_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import {
IEntityJob,
ILibraryAssetJob,
ILibraryFileJob,
JobName,
JOBS_LIBRARY_PAGINATION_SIZE,
JobStatus,
} from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { mimeTypes } from 'src/utils/mime-types';
import { handlePromiseError } from 'src/utils/misc';
@@ -216,7 +223,6 @@ export class LibraryService extends BaseService {
return libraries.map((library) => mapLibrary(library));
}
@OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
async handleQueueCleanup(): Promise<JobStatus> {
this.logger.debug('Cleaning up any pending library deletions');
const pendingDeletion = await this.libraryRepository.getAllDeleted();
@@ -334,8 +340,7 @@ export class LibraryService extends BaseService {
await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
}
@OnJob({ name: JobName.LIBRARY_DELETE, queue: QueueName.LIBRARY })
async handleDeleteLibrary(job: JobOf<JobName.LIBRARY_DELETE>): Promise<JobStatus> {
async handleDeleteLibrary(job: IEntityJob): Promise<JobStatus> {
const libraryId = job.id;
const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
@@ -369,8 +374,7 @@ export class LibraryService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.LIBRARY_SYNC_FILE, queue: QueueName.LIBRARY })
async handleSyncFile(job: JobOf<JobName.LIBRARY_SYNC_FILE>): Promise<JobStatus> {
async handleSyncFile(job: ILibraryFileJob): Promise<JobStatus> {
// Only needs to handle new assets
const assetPath = path.normalize(job.assetPath);
@@ -454,7 +458,6 @@ export class LibraryService extends BaseService {
await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, data: { id } });
}
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ALL, queue: QueueName.LIBRARY })
async handleQueueSyncAll(): Promise<JobStatus> {
this.logger.debug(`Refreshing all external libraries`);
@@ -480,8 +483,7 @@ export class LibraryService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.LIBRARY_SYNC_ASSET, queue: QueueName.LIBRARY })
async handleSyncAsset(job: JobOf<JobName.LIBRARY_SYNC_ASSET>): Promise<JobStatus> {
async handleSyncAsset(job: ILibraryAssetJob): Promise<JobStatus> {
const asset = await this.assetRepository.getById(job.id);
if (!asset) {
return JobStatus.SKIPPED;
@@ -536,8 +538,7 @@ export class LibraryService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_FILES, queue: QueueName.LIBRARY })
async handleQueueSyncFiles(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_FILES>): Promise<JobStatus> {
async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> {
const library = await this.libraryRepository.get(job.id);
if (!library) {
this.logger.debug(`Library ${job.id} not found, skipping refresh`);
@@ -588,8 +589,7 @@ export class LibraryService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, queue: QueueName.LIBRARY })
async handleQueueSyncAssets(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_ASSETS>): Promise<JobStatus> {
async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> {
const library = await this.libraryRepository.get(job.id);
if (!library) {
return JobStatus.SKIPPED;

View File

@@ -9,6 +9,7 @@ import {
AudioCodec,
Colorspace,
ImageFormat,
ToneMapping,
TranscodeHWAccel,
TranscodePolicy,
VideoCodec,
@@ -409,7 +410,7 @@ describe(MediaService.name, () => {
'-frames:v 1',
'-update 1',
'-v verbose',
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc`,
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,scale=-2:1440:flags=lanczos+accurate_rnd+full_chroma_int:out_color_matrix=bt601:out_range=pc,format=yuv420p`,
],
twoPass: false,
}),
@@ -444,7 +445,7 @@ describe(MediaService.name, () => {
'-frames:v 1',
'-update 1',
'-v verbose',
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=601:m=470bg:range=pc,format=yuv420p`,
],
twoPass: false,
}),
@@ -481,7 +482,7 @@ describe(MediaService.name, () => {
'-frames:v 1',
'-update 1',
'-v verbose',
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p`,
String.raw`-vf fps=12:eof_action=pass:round=down,thumbnail=12,select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20),trim=end_frame=2,reverse,zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=601:m=470bg:range=pc,format=yuv420p`,
],
twoPass: false,
}),
@@ -1327,7 +1328,7 @@ describe(MediaService.name, () => {
'-map 0:0',
'-map 0:1',
'-v verbose',
'-vf scale=-2:720',
'-vf scale=-2:720,format=yuv420p',
'-preset 12',
'-crf 23',
]),
@@ -1453,7 +1454,7 @@ describe(MediaService.name, () => {
'-map 0:1',
'-g 256',
'-v verbose',
'-vf hwupload_cuda,scale_cuda=-2:720:format=nv12',
'-vf format=nv12,hwupload_cuda,scale_cuda=-2:720',
'-preset p1',
'-cq:v 23',
]),
@@ -1585,7 +1586,7 @@ describe(MediaService.name, () => {
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
outputOptions: expect.arrayContaining([
expect.stringContaining(
'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:tonemap_mode=lum:transfer=bt709:peak=100:format=nv12',
'tonemap_cuda=desat=0:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709:format=nv12',
),
]),
twoPass: false,
@@ -1593,24 +1594,6 @@ describe(MediaService.name, () => {
);
});
it('should set format to nv12 for nvenc if input is not yuv420p', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
systemMock.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHWAccel.NVENC, accelDecode: true },
});
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
twoPass: false,
}),
);
});
it('should set options for qsv', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
@@ -1633,7 +1616,7 @@ describe(MediaService.name, () => {
'-refs 5',
'-g 256',
'-v verbose',
'-vf hwupload=extra_hw_frames=64,scale_qsv=-1:720:mode=hq:format=nv12',
'-vf format=nv12,hwupload=extra_hw_frames=64,scale_qsv=-1:720:mode=hq',
'-preset 7',
'-global_quality:v 23',
'-maxrate 10000k',
@@ -1765,7 +1748,7 @@ describe(MediaService.name, () => {
]),
outputOptions: expect.arrayContaining([
expect.stringContaining(
'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=qsv:reverse=1,format=qsv',
'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709,hwmap=derive_device=qsv:reverse=1,format=qsv',
),
]),
twoPass: false,
@@ -1793,32 +1776,6 @@ describe(MediaService.name, () => {
);
});
it('should set format to nv12 for qsv if input is not yuv420p', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
systemMock.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHWAccel.QSV, accelDecode: true },
});
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining([
'-hwaccel qsv',
'-hwaccel_output_format qsv',
'-async_depth 4',
'-threads 1',
]),
outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
twoPass: false,
}),
);
});
it('should set options for vaapi', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
@@ -1842,7 +1799,7 @@ describe(MediaService.name, () => {
'-map 0:1',
'-g 256',
'-v verbose',
'-vf hwupload=extra_hw_frames=64,scale_vaapi=-2:720:mode=hq:out_range=pc:format=nv12',
'-vf format=nv12,hwupload,scale_vaapi=-2:720:mode=hq:out_range=pc',
'-compression_level 7',
'-rc_mode 1',
]),
@@ -2013,7 +1970,7 @@ describe(MediaService.name, () => {
);
});
it('should use hardware tone-mapping for vaapi if hardware decoding is enabled and should tone map', async () => {
it('should use hardware tone-mapping for qsv if hardware decoding is enabled and should tone map', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
systemMock.get.mockResolvedValue({
@@ -2030,7 +1987,7 @@ describe(MediaService.name, () => {
inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
outputOptions: expect.arrayContaining([
expect.stringContaining(
'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:transfer=bt709:range=pc:tonemap=hable:tonemap_mode=lum:peak=100,hwmap=derive_device=vaapi:reverse=1,format=vaapi',
'hwmap=derive_device=opencl,tonemap_opencl=desat=0:format=nv12:matrix=bt709:primaries=bt709:range=pc:tonemap=hable:transfer=bt709,hwmap=derive_device=vaapi:reverse=1,format=vaapi',
),
]),
twoPass: false,
@@ -2038,27 +1995,6 @@ describe(MediaService.name, () => {
);
});
it('should set format to nv12 for vaapi if input is not yuv420p', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
systemMock.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHWAccel.VAAPI, accelDecode: true },
});
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
outputOptions: expect.arrayContaining([expect.stringContaining('format=nv12')]),
twoPass: false,
}),
);
});
it('should use preferred device for vaapi when hardware decoding', async () => {
storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']);
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
@@ -2133,7 +2069,7 @@ describe(MediaService.name, () => {
'-map 0:1',
'-g 256',
'-v verbose',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4',
'-vf scale_rkrga=-2:720:format=nv12:afbc=1',
'-level 51',
'-rc_mode CQP',
'-qp_init 23',
@@ -2204,7 +2140,7 @@ describe(MediaService.name, () => {
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
outputOptions: expect.arrayContaining([
expect.stringContaining(
'scale_rkrga=-2:720:format=p010:afbc=1:async_depth=4,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0:tonemap_mode=lum:peak=100,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
'scale_rkrga=-2:720:format=p010:afbc=1,hwmap=derive_device=opencl:mode=read,tonemap_opencl=format=nv12:r=pc:p=bt709:t=bt709:m=bt709:tonemap=hable:desat=0,hwmap=derive_device=rkmpp:mode=write:reverse=1,format=drm_prime',
),
]),
twoPass: false,
@@ -2212,28 +2148,6 @@ describe(MediaService.name, () => {
);
});
it('should set hardware decoding options for rkmpp when hardware decoding is enabled with no OpenCL on non-HDR file', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
mediaMock.probe.mockResolvedValue(probeStub.noAudioStreams);
systemMock.get.mockResolvedValue({
ffmpeg: { accel: TranscodeHWAccel.RKMPP, accelDecode: true, crf: 30, maxBitrate: '0' },
});
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
outputOptions: expect.arrayContaining([
expect.stringContaining('scale_rkrga=-2:720:format=nv12:afbc=1:async_depth=4'),
]),
twoPass: false,
}),
);
});
it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats);
@@ -2250,7 +2164,7 @@ describe(MediaService.name, () => {
inputOptions: [],
outputOptions: expect.arrayContaining([
expect.stringContaining(
'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p',
),
]),
twoPass: false,
@@ -2258,7 +2172,7 @@ describe(MediaService.name, () => {
);
});
it('should use software tone-mapping if opencl is not available', async () => {
it('should use software decoding and tone-mapping if opencl is not available', async () => {
storageMock.readdir.mockResolvedValue(['renderD128']);
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
@@ -2271,10 +2185,10 @@ describe(MediaService.name, () => {
'/original/path.ext',
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.any(Array),
inputOptions: [],
outputOptions: expect.arrayContaining([
expect.stringContaining(
'tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
'zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p',
),
]),
twoPass: false,
@@ -2295,7 +2209,7 @@ describe(MediaService.name, () => {
outputOptions: expect.arrayContaining([
'-c:v h264',
'-c:a copy',
'-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
'-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p',
]),
twoPass: false,
}),
@@ -2315,16 +2229,16 @@ describe(MediaService.name, () => {
outputOptions: expect.arrayContaining([
'-c:v h264',
'-c:a copy',
'-vf tonemapx=tonemap=hable:desat=0:p=bt709:t=bt709:m=bt709:r=pc:peak=100:format=yuv420p',
'-vf zscale=t=linear:npl=100,tonemap=hable:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p',
]),
twoPass: false,
}),
);
});
it('should transcode when policy is required and video is not yuv420p', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
assetMock.getByIds.mockResolvedValue([assetStub.video]);
await sut.handleVideoConversion({ id: assetStub.video.id });
expect(mediaMock.transcode).toHaveBeenCalledWith(
@@ -2332,7 +2246,11 @@ describe(MediaService.name, () => {
'upload/encoded-video/user-id/as/se/asset-id.mp4',
expect.objectContaining({
inputOptions: expect.any(Array),
outputOptions: expect.arrayContaining(['-c:v h264', '-c:a copy', '-vf format=yuv420p']),
outputOptions: expect.arrayContaining([
'-c:v h264',
'-c:a copy',
'-vf zscale=t=linear:npl=250,tonemap=mobius:desat=0,zscale=p=709:t=709:m=709:range=pc,format=yuv420p',
]),
twoPass: false,
}),
);

View File

@@ -1,7 +1,6 @@
import { Injectable } from '@nestjs/common';
import { dirname } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import {
@@ -20,10 +19,11 @@ import {
} from 'src/enum';
import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
import {
IBaseJob,
IEntityJob,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
JobOf,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
@@ -39,8 +39,7 @@ export class MediaService extends BaseService {
private maliOpenCL?: boolean;
private devices?: string[];
@OnJob({ name: JobName.QUEUE_GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
async handleQueueGenerateThumbnails({ force }: JobOf<JobName.QUEUE_GENERATE_THUMBNAILS>): Promise<JobStatus> {
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
? this.assetRepository.getAll(pagination, {
@@ -91,7 +90,6 @@ export class MediaService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.QUEUE_MIGRATION, queue: QueueName.MIGRATION })
async handleQueueMigration(): Promise<JobStatus> {
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getAll(pagination),
@@ -122,8 +120,7 @@ export class MediaService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.MIGRATE_ASSET, queue: QueueName.MIGRATION })
async handleAssetMigration({ id }: JobOf<JobName.MIGRATE_ASSET>): Promise<JobStatus> {
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
const { image } = await this.getConfig({ withCache: true });
const [asset] = await this.assetRepository.getByIds([id], { files: true });
if (!asset) {
@@ -137,8 +134,7 @@ export class MediaService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
async handleGenerateThumbnails({ id }: JobOf<JobName.GENERATE_THUMBNAILS>): Promise<JobStatus> {
async handleGenerateThumbnails({ id }: IEntityJob): Promise<JobStatus> {
const asset = await this.assetRepository.getById(id, { exifInfo: true, files: true });
if (!asset) {
this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`);
@@ -261,8 +257,7 @@ export class MediaService extends BaseService {
return { previewPath, thumbnailPath, thumbhash };
}
@OnJob({ name: JobName.QUEUE_VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
async handleQueueVideoConversion(job: JobOf<JobName.QUEUE_VIDEO_CONVERSION>): Promise<JobStatus> {
async handleQueueVideoConversion(job: IBaseJob): Promise<JobStatus> {
const { force } = job;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
@@ -280,8 +275,7 @@ export class MediaService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
async handleVideoConversion({ id }: JobOf<JobName.VIDEO_CONVERSION>): Promise<JobStatus> {
async handleVideoConversion({ id }: IEntityJob): Promise<JobStatus> {
const [asset] = await this.assetRepository.getByIds([id]);
if (!asset || asset.type !== AssetType.VIDEO) {
return JobStatus.FAILED;
@@ -329,11 +323,9 @@ export class MediaService extends BaseService {
}
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
this.logger.log(`Transcoding video ${asset.id} without hardware acceleration`);
this.logger.log(`Encoding video ${asset.id} without hardware acceleration`);
} else {
this.logger.log(
`Transcoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and${ffmpeg.accelDecode ? '' : ' software'} decoding`,
);
this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`);
}
try {
@@ -343,26 +335,10 @@ export class MediaService extends BaseService {
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
return JobStatus.FAILED;
}
let partialFallbackSuccess = false;
if (ffmpeg.accelDecode) {
try {
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
const config = BaseConfig.create({ ...ffmpeg, accelDecode: false });
command = config.getCommand(target, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(input, output, command);
partialFallbackSuccess = true;
} catch (error: any) {
this.logger.error(`Error occurred during transcoding: ${error.message}`);
}
}
if (!partialFallbackSuccess) {
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
command = config.getCommand(target, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(input, output, command);
}
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
command = config.getCommand(target, mainVideoStream, mainAudioStream);
await this.mediaRepository.transcode(input, output, command);
}
this.logger.log(`Successfully encoded ${asset.id}`);
@@ -431,7 +407,7 @@ export class MediaService extends BaseService {
const isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p');
const isRequired = !isTargetVideoCodec || stream.isHDR;
switch (ffmpegConfig.transcode) {
case TranscodePolicy.DISABLED: {
@@ -520,7 +496,7 @@ export class MediaService extends BaseService {
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
} catch {
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU tonemapping');
this.logger.debug('OpenCL not available for transcoding, so RKMPP acceleration will use CPU decoding');
this.maliOpenCL = false;
}
}

View File

@@ -7,7 +7,7 @@ import { constants } from 'node:fs/promises';
import path from 'node:path';
import { SystemConfig } from 'src/config';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { ExifEntity } from 'src/entities/exif.entity';
@@ -16,7 +16,15 @@ import { AssetType, ImmichWorker, SourceType } from 'src/enum';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import {
IBaseJob,
IEntityJob,
ISidecarWriteJob,
JobName,
JOBS_ASSET_PAGINATION_SIZE,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { ReverseGeocodeResult } from 'src/interfaces/map.interface';
import { ImmichTags } from 'src/interfaces/metadata.interface';
import { BaseService } from 'src/services/base.service';
@@ -116,8 +124,7 @@ export class MetadataService extends BaseService {
}
}
@OnJob({ name: JobName.LINK_LIVE_PHOTOS, queue: QueueName.METADATA_EXTRACTION })
async handleLivePhotoLinking(job: JobOf<JobName.LINK_LIVE_PHOTOS>): Promise<JobStatus> {
async handleLivePhotoLinking(job: IEntityJob): Promise<JobStatus> {
const { id } = job;
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
if (!asset?.exifInfo) {
@@ -152,8 +159,7 @@ export class MetadataService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
async handleQueueMetadataExtraction(job: JobOf<JobName.QUEUE_METADATA_EXTRACTION>): Promise<JobStatus> {
async handleQueueMetadataExtraction(job: IBaseJob): Promise<JobStatus> {
const { force } = job;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
@@ -170,8 +176,7 @@ export class MetadataService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
async handleMetadataExtraction({ id }: JobOf<JobName.METADATA_EXTRACTION>): Promise<JobStatus> {
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
const [asset] = await this.assetRepository.getByIds([id], { faces: { person: false } });
if (!asset) {
@@ -187,8 +192,6 @@ export class MetadataService extends BaseService {
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
const { width, height } = this.getImageDimensions(exifTags);
const exifData: Partial<ExifEntity> = {
assetId: asset.id,
@@ -206,8 +209,8 @@ export class MetadataService extends BaseService {
// image/file
fileSizeInByte: stats.size,
exifImageHeight: validate(height),
exifImageWidth: validate(width),
exifImageHeight: validate(exifTags.ImageHeight),
exifImageWidth: validate(exifTags.ImageWidth),
orientation: validate(exifTags.Orientation)?.toString() ?? null,
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
bitsPerSample: this.getBitsPerSample(exifTags),
@@ -257,8 +260,7 @@ export class MetadataService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.QUEUE_SIDECAR, queue: QueueName.SIDECAR })
async handleQueueSidecar(job: JobOf<JobName.QUEUE_SIDECAR>): Promise<JobStatus> {
async handleQueueSidecar(job: IBaseJob): Promise<JobStatus> {
const { force } = job;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
return force
@@ -278,13 +280,11 @@ export class MetadataService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.SIDECAR_SYNC, queue: QueueName.SIDECAR })
handleSidecarSync({ id }: JobOf<JobName.SIDECAR_SYNC>): Promise<JobStatus> {
handleSidecarSync({ id }: IEntityJob): Promise<JobStatus> {
return this.processSidecar(id, true);
}
@OnJob({ name: JobName.SIDECAR_DISCOVERY, queue: QueueName.SIDECAR })
handleSidecarDiscovery({ id }: JobOf<JobName.SIDECAR_DISCOVERY>): Promise<JobStatus> {
handleSidecarDiscovery({ id }: IEntityJob): Promise<JobStatus> {
return this.processSidecar(id, false);
}
@@ -298,8 +298,7 @@ export class MetadataService extends BaseService {
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
}
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
if (!asset) {
@@ -335,19 +334,6 @@ export class MetadataService extends BaseService {
return JobStatus.SUCCESS;
}
private getImageDimensions(exifTags: ImmichTags): { width?: number; height?: number } {
/*
* The "true" values for width and height are a bit hidden, depending on the camera model and file format.
* For RAW images in the CR2 or RAF format, the "ImageSize" value seems to be correct,
* but ImageWidth and ImageHeight are not correct (they contain the dimensions of the preview image).
*/
let [width, height] = exifTags.ImageSize?.split('x').map((dim) => Number.parseInt(dim) || undefined) || [];
if (!width || !height) {
[width, height] = [exifTags.ImageWidth, exifTags.ImageHeight];
}
return { width, height };
}
private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};

View File

@@ -0,0 +1,106 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from 'src/decorators';
import { ImmichWorker } from 'src/enum';
import { ArgOf } from 'src/interfaces/event.interface';
import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface';
import { AssetService } from 'src/services/asset.service';
import { AuditService } from 'src/services/audit.service';
import { BackupService } from 'src/services/backup.service';
import { DuplicateService } from 'src/services/duplicate.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 { NotificationService } from 'src/services/notification.service';
import { PersonService } from 'src/services/person.service';
import { SessionService } from 'src/services/session.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 { TagService } from 'src/services/tag.service';
import { TrashService } from 'src/services/trash.service';
import { UserService } from 'src/services/user.service';
import { VersionService } from 'src/services/version.service';
@Injectable()
export class MicroservicesService {
constructor(
private auditService: AuditService,
private assetService: AssetService,
private backupService: BackupService,
private jobService: JobService,
private libraryService: LibraryService,
private mediaService: MediaService,
private metadataService: MetadataService,
private notificationService: NotificationService,
private personService: PersonService,
private smartInfoService: SmartInfoService,
private sessionService: SessionService,
private storageTemplateService: StorageTemplateService,
private storageService: StorageService,
private tagService: TagService,
private trashService: TrashService,
private userService: UserService,
private duplicateService: DuplicateService,
private versionService: VersionService,
) {}
@OnEvent({ name: 'app.bootstrap' })
async onBootstrap(app: ArgOf<'app.bootstrap'>) {
if (app !== ImmichWorker.MICROSERVICES) {
return;
}
await this.jobService.init({
[JobName.ASSET_DELETION]: (data) => this.assetService.handleAssetDeletion(data),
[JobName.ASSET_DELETION_CHECK]: () => this.assetService.handleAssetDeletionCheck(),
[JobName.BACKUP_DATABASE]: () => this.backupService.handleBackupDatabase(),
[JobName.DELETE_FILES]: (data: IDeleteFilesJob) => this.storageService.handleDeleteFiles(data),
[JobName.CLEAN_OLD_AUDIT_LOGS]: () => this.auditService.handleCleanup(),
[JobName.CLEAN_OLD_SESSION_TOKENS]: () => this.sessionService.handleCleanup(),
[JobName.USER_DELETE_CHECK]: () => this.userService.handleUserDeleteCheck(),
[JobName.USER_DELETION]: (data) => this.userService.handleUserDelete(data),
[JobName.USER_SYNC_USAGE]: () => this.userService.handleUserSyncUsage(),
[JobName.QUEUE_SMART_SEARCH]: (data) => this.smartInfoService.handleQueueEncodeClip(data),
[JobName.SMART_SEARCH]: (data) => this.smartInfoService.handleEncodeClip(data),
[JobName.QUEUE_DUPLICATE_DETECTION]: (data) => this.duplicateService.handleQueueSearchDuplicates(data),
[JobName.DUPLICATE_DETECTION]: (data) => this.duplicateService.handleSearchDuplicates(data),
[JobName.STORAGE_TEMPLATE_MIGRATION]: () => this.storageTemplateService.handleMigration(),
[JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE]: (data) => this.storageTemplateService.handleMigrationSingle(data),
[JobName.QUEUE_MIGRATION]: () => this.mediaService.handleQueueMigration(),
[JobName.MIGRATE_ASSET]: (data) => this.mediaService.handleAssetMigration(data),
[JobName.MIGRATE_PERSON]: (data) => this.personService.handlePersonMigration(data),
[JobName.QUEUE_GENERATE_THUMBNAILS]: (data) => this.mediaService.handleQueueGenerateThumbnails(data),
[JobName.GENERATE_THUMBNAILS]: (data) => this.mediaService.handleGenerateThumbnails(data),
[JobName.QUEUE_VIDEO_CONVERSION]: (data) => this.mediaService.handleQueueVideoConversion(data),
[JobName.VIDEO_CONVERSION]: (data) => this.mediaService.handleVideoConversion(data),
[JobName.QUEUE_METADATA_EXTRACTION]: (data) => this.metadataService.handleQueueMetadataExtraction(data),
[JobName.METADATA_EXTRACTION]: (data) => this.metadataService.handleMetadataExtraction(data),
[JobName.LINK_LIVE_PHOTOS]: (data) => this.metadataService.handleLivePhotoLinking(data),
[JobName.QUEUE_FACE_DETECTION]: (data) => this.personService.handleQueueDetectFaces(data),
[JobName.FACE_DETECTION]: (data) => this.personService.handleDetectFaces(data),
[JobName.QUEUE_FACIAL_RECOGNITION]: (data) => this.personService.handleQueueRecognizeFaces(data),
[JobName.FACIAL_RECOGNITION]: (data) => this.personService.handleRecognizeFaces(data),
[JobName.GENERATE_PERSON_THUMBNAIL]: (data) => this.personService.handleGeneratePersonThumbnail(data),
[JobName.PERSON_CLEANUP]: () => this.personService.handlePersonCleanup(),
[JobName.QUEUE_SIDECAR]: (data) => this.metadataService.handleQueueSidecar(data),
[JobName.SIDECAR_DISCOVERY]: (data) => this.metadataService.handleSidecarDiscovery(data),
[JobName.SIDECAR_SYNC]: (data) => this.metadataService.handleSidecarSync(data),
[JobName.SIDECAR_WRITE]: (data) => this.metadataService.handleSidecarWrite(data),
[JobName.LIBRARY_QUEUE_SYNC_ALL]: () => this.libraryService.handleQueueSyncAll(),
[JobName.LIBRARY_QUEUE_SYNC_FILES]: (data) => this.libraryService.handleQueueSyncFiles(data), //Queues all files paths on disk
[JobName.LIBRARY_SYNC_FILE]: (data) => this.libraryService.handleSyncFile(data), //Handles a single path on disk //Watcher calls for new files
[JobName.LIBRARY_QUEUE_SYNC_ASSETS]: (data) => this.libraryService.handleQueueSyncAssets(data), //Queues all library assets
[JobName.LIBRARY_SYNC_ASSET]: (data) => this.libraryService.handleSyncAsset(data), //Handles all library assets // Watcher calls for unlink and changed
[JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data),
[JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(),
[JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data),
[JobName.NOTIFY_ALBUM_INVITE]: (data) => this.notificationService.handleAlbumInvite(data),
[JobName.NOTIFY_ALBUM_UPDATE]: (data) => this.notificationService.handleAlbumUpdate(data),
[JobName.NOTIFY_SIGNUP]: (data) => this.notificationService.handleUserSignup(data),
[JobName.TAG_CLEANUP]: () => this.tagService.handleTagCleanup(),
[JobName.VERSION_CHECK]: () => this.versionService.handleVersionCheck(),
[JobName.QUEUE_TRASH_EMPTY]: () => this.trashService.handleQueueEmptyTrash(),
});
}
}

View File

@@ -1,16 +1,17 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
import { AlbumEntity } from 'src/entities/album.entity';
import { ArgOf } from 'src/interfaces/event.interface';
import {
IEmailJob,
IEntityJob,
INotifyAlbumInviteJob,
INotifyAlbumUpdateJob,
INotifySignupJob,
JobItem,
JobName,
JobOf,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface';
import { BaseService } from 'src/services/base.service';
@@ -175,8 +176,7 @@ export class NotificationService extends BaseService {
return { messageId };
}
@OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION })
async handleUserSignup({ id, tempPassword }: JobOf<JobName.NOTIFY_SIGNUP>) {
async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
const user = await this.userRepository.get(id, { withDeleted: false });
if (!user) {
return JobStatus.SKIPPED;
@@ -207,8 +207,7 @@ export class NotificationService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.NOTIFY_ALBUM_INVITE, queue: QueueName.NOTIFICATION })
async handleAlbumInvite({ id, recipientId }: JobOf<JobName.NOTIFY_ALBUM_INVITE>) {
async handleAlbumInvite({ id, recipientId }: INotifyAlbumInviteJob) {
const album = await this.albumRepository.getById(id, { withAssets: false });
if (!album) {
return JobStatus.SKIPPED;
@@ -255,8 +254,7 @@ export class NotificationService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.NOTIFY_ALBUM_UPDATE, queue: QueueName.NOTIFICATION })
async handleAlbumUpdate({ id, recipientIds }: JobOf<JobName.NOTIFY_ALBUM_UPDATE>) {
async handleAlbumUpdate({ id, recipientIds }: INotifyAlbumUpdateJob) {
const album = await this.albumRepository.getById(id, { withAssets: false });
if (!album) {
@@ -314,8 +312,7 @@ export class NotificationService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.SEND_EMAIL, queue: QueueName.NOTIFICATION })
async handleSendEmail(data: JobOf<JobName.SEND_EMAIL>): Promise<JobStatus> {
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
const { notifications } = await this.getConfig({ withCache: false });
if (!notifications.smtp.enabled) {
return JobStatus.SKIPPED;

View File

@@ -1,7 +1,6 @@
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
@@ -34,10 +33,13 @@ import {
} from 'src/enum';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import {
IBaseJob,
IDeferrableJob,
IEntityJob,
INightlyJob,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
JobOf,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
@@ -229,15 +231,13 @@ export class PersonService extends BaseService {
this.logger.debug(`Deleted ${people.length} people`);
}
@OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
async handlePersonCleanup(): Promise<JobStatus> {
const people = await this.personRepository.getAllWithoutFaces();
await this.delete(people);
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.QUEUE_FACE_DETECTION, queue: QueueName.FACE_DETECTION })
async handleQueueDetectFaces({ force }: JobOf<JobName.QUEUE_FACE_DETECTION>): Promise<JobStatus> {
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -272,8 +272,7 @@ export class PersonService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.FACE_DETECTION, queue: QueueName.FACE_DETECTION })
async handleDetectFaces({ id }: JobOf<JobName.FACE_DETECTION>): Promise<JobStatus> {
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -377,8 +376,7 @@ export class PersonService extends BaseService {
return intersection / union;
}
@OnJob({ name: JobName.QUEUE_FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
async handleQueueRecognizeFaces({ force, nightly }: JobOf<JobName.QUEUE_FACIAL_RECOGNITION>): Promise<JobStatus> {
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -428,8 +426,7 @@ export class PersonService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
async handleRecognizeFaces({ id, deferred }: JobOf<JobName.FACIAL_RECOGNITION>): Promise<JobStatus> {
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -512,8 +509,7 @@ export class PersonService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.MIGRATE_PERSON, queue: QueueName.MIGRATION })
async handlePersonMigration({ id }: JobOf<JobName.MIGRATE_PERSON>): Promise<JobStatus> {
async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> {
const person = await this.personRepository.getById(id);
if (!person) {
return JobStatus.FAILED;
@@ -524,8 +520,7 @@ export class PersonService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.GENERATE_PERSON_THUMBNAIL, queue: QueueName.THUMBNAIL_GENERATION })
async handleGeneratePersonThumbnail(data: JobOf<JobName.GENERATE_PERSON_THUMBNAIL>): Promise<JobStatus> {
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
return JobStatus.SKIPPED;

View File

@@ -1,16 +1,14 @@
import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import { OnJob } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
import { Permission } from 'src/enum';
import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
@Injectable()
export class SessionService extends BaseService {
@OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
async handleCleanup(): Promise<JobStatus> {
async handleCleanup() {
const sessions = await this.sessionRepository.search({
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
});

View File

@@ -1,11 +1,18 @@
import { Injectable } from '@nestjs/common';
import { SystemConfig } from 'src/config';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { ImmichWorker } from 'src/enum';
import { WithoutProperty } from 'src/interfaces/asset.interface';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
import {
IBaseJob,
IEntityJob,
JOBS_ASSET_PAGINATION_SIZE,
JobName,
JobStatus,
QueueName,
} from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { getAssetFiles } from 'src/utils/asset.util';
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
@@ -79,8 +86,7 @@ export class SmartInfoService extends BaseService {
});
}
@OnJob({ name: JobName.QUEUE_SMART_SEARCH, queue: QueueName.SMART_SEARCH })
async handleQueueEncodeClip({ force }: JobOf<JobName.QUEUE_SMART_SEARCH>): Promise<JobStatus> {
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: false });
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;
@@ -105,8 +111,7 @@ export class SmartInfoService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.SMART_SEARCH, queue: QueueName.SMART_SEARCH })
async handleEncodeClip({ id }: JobOf<JobName.SMART_SEARCH>): Promise<JobStatus> {
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.getConfig({ withCache: true });
if (!isSmartSearchEnabled(machineLearning)) {
return JobStatus.SKIPPED;

View File

@@ -4,13 +4,13 @@ import { DateTime } from 'luxon';
import path from 'node:path';
import sanitize from 'sanitize-filename';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
import { AssetEntity } from 'src/entities/asset.entity';
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobOf, JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { getLivePhotoMotionFilename } from 'src/utils/file';
import { usePagination } from 'src/utils/pagination';
@@ -108,8 +108,7 @@ export class StorageTemplateService extends BaseService {
return { ...storageTokens, presetOptions: storagePresets };
}
@OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
async handleMigrationSingle({ id }: JobOf<JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE>): Promise<JobStatus> {
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
const config = await this.getConfig({ withCache: true });
const storageTemplateEnabled = config.storageTemplate.enabled;
if (!storageTemplateEnabled) {
@@ -138,7 +137,6 @@ export class StorageTemplateService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
async handleMigration(): Promise<JobStatus> {
this.logger.log('Starting storage template migration');
const { storageTemplate } = await this.getConfig({ withCache: true });

View File

@@ -3,8 +3,7 @@ import { IConfigRepository } from 'src/interfaces/config.interface';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { IStorageRepository } from 'src/interfaces/storage.interface';
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
import { StorageService } from 'src/services/storage.service';
import { ImmichStartupError } from 'src/utils/misc';
import { ImmichStartupError, StorageService } from 'src/services/storage.service';
import { mockEnvData } from 'test/repositories/config.repository.mock';
import { newTestService } from 'test/utils';
import { Mocked } from 'vitest';

View File

@@ -1,13 +1,15 @@
import { Injectable } from '@nestjs/common';
import { join } from 'node:path';
import { StorageCore } from 'src/cores/storage.core';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { SystemFlags } from 'src/entities/system-metadata.entity';
import { StorageFolder, SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { ImmichStartupError } from 'src/utils/misc';
export class ImmichStartupError extends Error {}
export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
const docsMessage = `Please see https://immich.app/docs/administration/system-integrity#folder-checks for more information.`;
@@ -64,8 +66,7 @@ export class StorageService extends BaseService {
});
}
@OnJob({ name: JobName.DELETE_FILES, queue: QueueName.BACKGROUND_TASK })
async handleDeleteFiles(job: JobOf<JobName.DELETE_FILES>): Promise<JobStatus> {
async handleDeleteFiles(job: IDeleteFilesJob) {
const { files } = job;
// TODO: one job per file

View File

@@ -65,6 +65,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
bframes: -1,
refs: 0,
gopSize: 0,
npl: 0,
temporalAQ: false,
cqMode: CQMode.AUTO,
twoPass: false,

View File

@@ -1,5 +1,4 @@
import { BadRequestException, Injectable } from '@nestjs/common';
import { OnJob } from 'src/decorators';
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import {
@@ -13,7 +12,7 @@ import {
} from 'src/dtos/tag.dto';
import { TagEntity } from 'src/entities/tag.entity';
import { Permission } from 'src/enum';
import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JobStatus } from 'src/interfaces/job.interface';
import { AssetTagItem } from 'src/interfaces/tag.interface';
import { BaseService } from 'src/services/base.service';
import { addAssets, removeAssets } from 'src/utils/asset.util';
@@ -132,7 +131,6 @@ export class TagService extends BaseService {
return results;
}
@OnJob({ name: JobName.TAG_CLEANUP, queue: QueueName.BACKGROUND_TASK })
async handleTagCleanup() {
await this.tagRepository.deleteEmptyTags();
return JobStatus.SUCCESS;

View File

@@ -1,9 +1,9 @@
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
import { AuthDto } from 'src/dtos/auth.dto';
import { TrashResponseDto } from 'src/dtos/trash.dto';
import { Permission } from 'src/enum';
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JOBS_ASSET_PAGINATION_SIZE, JobName, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
import { usePagination } from 'src/utils/pagination';
@@ -44,7 +44,6 @@ export class TrashService extends BaseService {
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
}
@OnJob({ name: JobName.QUEUE_TRASH_EMPTY, queue: QueueName.BACKGROUND_TASK })
async handleQueueEmptyTrash() {
let count = 0;
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>

View File

@@ -2,7 +2,6 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
import { DateTime } from 'luxon';
import { SALT_ROUNDS } from 'src/constants';
import { StorageCore } from 'src/cores/storage.core';
import { OnJob } from 'src/decorators';
import { AuthDto } from 'src/dtos/auth.dto';
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto';
@@ -11,7 +10,7 @@ import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUse
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
import { UserEntity } from 'src/entities/user.entity';
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
import { JobName, JobOf, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { IEntityJob, JobName, JobStatus } from 'src/interfaces/job.interface';
import { UserFindOptions } from 'src/interfaces/user.interface';
import { BaseService } from 'src/services/base.service';
import { ImmichFileResponse } from 'src/utils/file';
@@ -164,13 +163,11 @@ export class UserService extends BaseService {
return licenseData;
}
@OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK })
async handleUserSyncUsage(): Promise<JobStatus> {
await this.userRepository.syncUsage();
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.USER_DELETE_CHECK, queue: QueueName.BACKGROUND_TASK })
async handleUserDeleteCheck(): Promise<JobStatus> {
const users = await this.userRepository.getDeletedUsers();
const config = await this.getConfig({ withCache: false });
@@ -184,8 +181,7 @@ export class UserService extends BaseService {
return JobStatus.SUCCESS;
}
@OnJob({ name: JobName.USER_DELETION, queue: QueueName.BACKGROUND_TASK })
async handleUserDelete({ id, force }: JobOf<JobName.USER_DELETION>): Promise<JobStatus> {
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
const config = await this.getConfig({ withCache: false });
const user = await this.userRepository.get(id, { withDeleted: true });
if (!user) {

View File

@@ -2,13 +2,13 @@ import { Injectable } from '@nestjs/common';
import { DateTime } from 'luxon';
import semver, { SemVer } from 'semver';
import { serverVersion } from 'src/constants';
import { OnEvent, OnJob } from 'src/decorators';
import { OnEvent } from 'src/decorators';
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
import { DatabaseLock } from 'src/interfaces/database.interface';
import { ArgOf } from 'src/interfaces/event.interface';
import { JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
import { JobName, JobStatus } from 'src/interfaces/job.interface';
import { BaseService } from 'src/services/base.service';
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
@@ -48,7 +48,6 @@ export class VersionService extends BaseService {
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
}
@OnJob({ name: JobName.VERSION_CHECK, queue: QueueName.BACKGROUND_TASK })
async handleVersionCheck(): Promise<JobStatus> {
try {
this.logger.debug('Running version check');

View File

@@ -16,7 +16,7 @@ export const logGlobalError = (logger: ILoggerRepository, error: Error) => {
}
if (error instanceof Error) {
logger.error(`Unknown error: ${error}`, error?.stack);
logger.error(`Unknown error: ${error}`);
return;
}
};

View File

@@ -58,9 +58,10 @@ export class BaseConfig implements VideoCodecSWConfig {
break;
}
case TranscodeHWAccel.RKMPP: {
handler = config.accelDecode
? new RkmppHwDecodeConfig(config, devices, hasMaliOpenCL)
: new RkmppSwDecodeConfig(config, devices);
handler =
config.accelDecode && hasMaliOpenCL
? new RkmppHwDecodeConfig(config, devices)
: new RkmppSwDecodeConfig(config, devices);
break;
}
default: {
@@ -148,11 +149,7 @@ export class BaseConfig implements VideoCodecSWConfig {
options.push(`scale=${this.getScaling(videoStream)}`);
}
options.push(...this.getToneMapping(videoStream));
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
options.push(`format=yuv420p`);
}
options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
return options;
}
@@ -274,20 +271,33 @@ export class BaseConfig implements VideoCodecSWConfig {
getColors() {
return {
primaries: 'bt709',
transfer: 'bt709',
matrix: 'bt709',
primaries: '709',
transfer: '709',
matrix: '709',
};
}
getNPL() {
if (this.config.npl <= 0) {
// since hable already outputs a darker image, we use a lower npl value for it
return this.config.tonemap === ToneMapping.HABLE ? 100 : 250;
} else {
return this.config.npl;
}
}
getToneMapping(videoStream: VideoStreamInfo) {
if (!this.shouldToneMap(videoStream)) {
return [];
}
const { primaries, transfer, matrix } = this.getColors();
const options = `tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`;
return [options];
const colors = this.getColors();
return [
`zscale=t=linear:npl=${this.getNPL()}`,
`tonemap=${this.config.tonemap}:desat=0`,
`zscale=p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:range=pc`,
];
}
getAudioCodec(): string {
@@ -326,32 +336,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
this.devices = this.validateDevices(devices);
}
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
return `scale_${this.config.accel}=${this.getScaling(videoStream)}:format=${format}`;
}
getHwDecodeFilterOptions(videoStream: VideoStreamInfo) {
const options = [];
const tonemapOptions = this.getToneMapping(videoStream);
if (this.shouldScale(videoStream) || tonemapOptions.length === 0) {
const format = tonemapOptions.length === 0 ? 'nv12' : 'p010';
options.push(this.getScalingFilter(videoStream, format));
} else if (tonemapOptions.length > 0) {
options.push('format=p010');
}
options.push(...tonemapOptions);
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
options.push('format=nv12');
}
return options;
}
getFilterOptions(videoStream: VideoStreamInfo) {
return this.config.accelDecode
? this.getHwDecodeFilterOptions(videoStream)
: [`hwupload_${this.config.accel}`, ...this.getHwDecodeFilterOptions(videoStream)];
}
getSupportedCodecs() {
return [VideoCodec.H264, VideoCodec.HEVC];
}
@@ -395,20 +379,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
return `/dev/dri/${deviceName}`;
}
getInputThreadOptions() {
if (this.config.accelDecode) {
return [`-threads 1`];
}
return [];
}
getOutputThreadOptions() {
if (this.config.accelDecode) {
return [];
}
return super.getOutputThreadOptions();
}
}
export class ThumbnailConfig extends BaseConfig {
@@ -425,14 +395,19 @@ export class ThumbnailConfig extends BaseConfig {
}
getFilterOptions(videoStream: VideoStreamInfo): string[] {
return [
const options = [
'fps=12:eof_action=pass:round=down',
'thumbnail=12',
String.raw`select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20)`,
'trim=end_frame=2',
'reverse',
...super.getFilterOptions(videoStream),
];
if (this.shouldScale(videoStream)) {
options.push(`scale=${this.getScaling(videoStream)}`);
}
options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
return options;
}
getPresetOptions() {
@@ -448,7 +423,19 @@ export class ThumbnailConfig extends BaseConfig {
}
getScaling(videoStream: VideoStreamInfo) {
return super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int:out_range=pc';
let options = super.getScaling(videoStream) + ':flags=lanczos+accurate_rnd+full_chroma_int';
if (!this.shouldToneMap(videoStream)) {
options += ':out_color_matrix=bt601:out_range=pc';
}
return options;
}
getColors() {
return {
primaries: '709',
transfer: '601',
matrix: '470bg',
};
}
}
@@ -543,11 +530,15 @@ export class AV1Config extends BaseConfig {
}
}
export class NvencConfig extends BaseHWConfig {
export class NvencSwDecodeConfig extends BaseHWConfig {
getSupportedCodecs() {
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
}
getBaseInputOptions() {
return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
}
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
const options = [
// below settings recommended from https://docs.nvidia.com/video-technologies/video-codec-sdk/12.0/ffmpeg-with-nvidia-gpu/index.html#command-line-for-latency-tolerant-high-quality-transcoding
@@ -566,6 +557,16 @@ export class NvencConfig extends BaseHWConfig {
return options;
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = this.getToneMapping(videoStream);
options.push('format=nv12', 'hwupload_cuda');
if (this.shouldScale(videoStream)) {
options.push(`scale_cuda=${this.getScaling(videoStream)}`);
}
return options;
}
getPresetOptions() {
let presetIndex = this.getPresetIndex();
if (presetIndex < 0) {
@@ -595,6 +596,10 @@ export class NvencConfig extends BaseHWConfig {
}
}
getThreadOptions() {
return [];
}
getRefs() {
const bframes = this.getBFrames();
if (bframes > 0 && bframes < 3 && this.config.refs < 3) {
@@ -602,59 +607,68 @@ export class NvencConfig extends BaseHWConfig {
}
return this.config.refs;
}
}
export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
getBaseInputOptions() {
if (!this.config.accelDecode) {
return ['-init_hw_device cuda=cuda:0', '-filter_hw_device cuda'];
}
return ['-hwaccel cuda', '-hwaccel_output_format cuda', '-noautorotate', ...this.getInputThreadOptions()];
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = [];
if (this.shouldScale(videoStream)) {
options.push(`scale_cuda=${this.getScaling(videoStream)}`);
}
options.push(...this.getToneMapping(videoStream));
if (options.length > 0) {
options[options.length - 1] += ':format=nv12';
}
return options;
}
getToneMapping(videoStream: VideoStreamInfo) {
if (!this.shouldToneMap(videoStream)) {
return [];
}
const { matrix, primaries, transfer } = this.getColors();
const colors = this.getColors();
const tonemapOptions = [
'desat=0',
`matrix=${matrix}`,
`primaries=${primaries}`,
`matrix=${colors.matrix}`,
`primaries=${colors.primaries}`,
'range=pc',
`tonemap=${this.config.tonemap}`,
'tonemap_mode=lum',
`transfer=${transfer}`,
'peak=100',
'format=nv12',
`transfer=${colors.transfer}`,
];
return [`tonemap_cuda=${tonemapOptions.join(':')}`];
}
getInputThreadOptions() {
return [`-threads 1`];
}
getOutputThreadOptions() {
return [];
}
getColors() {
return {
primaries: 'bt709',
transfer: 'bt709',
matrix: 'bt709',
};
}
}
export class QsvConfig extends BaseHWConfig {
export class QsvSwDecodeConfig extends BaseHWConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No QSV device found');
}
const hwDevice = this.getPreferredHardwareDevice();
if (this.config.accelDecode) {
const options = [
'-hwaccel qsv',
'-hwaccel_output_format qsv',
'-async_depth 4',
'-noautorotate',
...this.getInputThreadOptions(),
];
if (hwDevice) {
options.push(`-qsv_device ${hwDevice}`);
}
return options;
}
let qsvString = '';
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
qsvString = `,child_device=${hwDevice}`;
}
@@ -671,6 +685,15 @@ export class QsvConfig extends BaseHWConfig {
return options;
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = this.getToneMapping(videoStream);
options.push('format=nv12', 'hwupload=extra_hw_frames=64');
if (this.shouldScale(videoStream)) {
options.push(`scale_qsv=${this.getScaling(videoStream)}:mode=hq`);
}
return options;
}
getPresetOptions() {
let presetIndex = this.getPresetIndex();
if (presetIndex < 0) {
@@ -716,9 +739,41 @@ export class QsvConfig extends BaseHWConfig {
getScaling(videoStream: VideoStreamInfo): string {
return super.getScaling(videoStream, 1);
}
}
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
return `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq:format=${format}`;
export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No QSV device found');
}
const options = [
'-hwaccel qsv',
'-hwaccel_output_format qsv',
'-async_depth 4',
'-noautorotate',
...this.getInputThreadOptions(),
];
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
options.push(`-qsv_device ${hwDevice}`);
}
return options;
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = [];
if (this.shouldScale(videoStream) || !this.shouldToneMap(videoStream)) {
let scaling = `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq`;
if (!this.shouldToneMap(videoStream)) {
scaling += ':format=nv12';
}
options.push(scaling);
}
options.push(...this.getToneMapping(videoStream));
return options;
}
getToneMapping(videoStream: VideoStreamInfo): string[] {
@@ -726,17 +781,15 @@ export class QsvConfig extends BaseHWConfig {
return [];
}
const { matrix, primaries, transfer } = this.getColors();
const colors = this.getColors();
const tonemapOptions = [
'desat=0',
'format=nv12',
`matrix=${matrix}`,
`primaries=${primaries}`,
`transfer=${transfer}`,
`matrix=${colors.matrix}`,
`primaries=${colors.primaries}`,
'range=pc',
`tonemap=${this.config.tonemap}`,
'tonemap_mode=lum',
'peak=100',
`transfer=${colors.transfer}`,
];
return [
@@ -745,27 +798,27 @@ export class QsvConfig extends BaseHWConfig {
'hwmap=derive_device=qsv:reverse=1,format=qsv',
];
}
getInputThreadOptions() {
return [`-threads 1`];
}
getColors() {
return {
primaries: 'bt709',
transfer: 'bt709',
matrix: 'bt709',
};
}
}
export class VaapiConfig extends BaseHWConfig {
export class VaapiSwDecodeConfig extends BaseHWConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No VAAPI device found');
}
let hwDevice = this.getPreferredHardwareDevice();
if (this.config.accelDecode) {
const options = [
'-hwaccel vaapi',
'-hwaccel_output_format vaapi',
'-noautorotate',
...this.getInputThreadOptions(),
];
if (hwDevice) {
options.push(`-hwaccel_device ${hwDevice}`);
}
}
if (!hwDevice) {
hwDevice = `/dev/dri/${this.devices[0]}`;
}
@@ -773,6 +826,16 @@ export class VaapiConfig extends BaseHWConfig {
return [`-init_hw_device vaapi=accel:${hwDevice}`, '-filter_hw_device accel'];
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = this.getToneMapping(videoStream);
options.push('format=nv12', 'hwupload');
if (this.shouldScale(videoStream)) {
options.push(`scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`);
}
return options;
}
getPresetOptions() {
let presetIndex = this.getPresetIndex();
if (presetIndex < 0) {
@@ -814,9 +877,40 @@ export class VaapiConfig extends BaseHWConfig {
useCQP() {
return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9;
}
}
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
return `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=${format}`;
export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No VAAPI device found');
}
const options = [
'-hwaccel vaapi',
'-hwaccel_output_format vaapi',
'-noautorotate',
...this.getInputThreadOptions(),
];
const hwDevice = this.getPreferredHardwareDevice();
if (hwDevice) {
options.push(`-hwaccel_device ${hwDevice}`);
}
return options;
}
getFilterOptions(videoStream: VideoStreamInfo) {
const options = [];
if (this.shouldScale(videoStream) || !this.shouldToneMap(videoStream)) {
let scaling = `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc`;
if (!this.shouldToneMap(videoStream)) {
scaling += ':format=nv12';
}
options.push(scaling);
}
options.push(...this.getToneMapping(videoStream));
return options;
}
getToneMapping(videoStream: VideoStreamInfo): string[] {
@@ -824,17 +918,15 @@ export class VaapiConfig extends BaseHWConfig {
return [];
}
const { matrix, primaries, transfer } = this.getColors();
const colors = this.getColors();
const tonemapOptions = [
'desat=0',
'format=nv12',
`matrix=${matrix}`,
`primaries=${primaries}`,
`transfer=${transfer}`,
`matrix=${colors.matrix}`,
`primaries=${colors.primaries}`,
'range=pc',
`tonemap=${this.config.tonemap}`,
'tonemap_mode=lum',
'peak=100',
`transfer=${colors.transfer}`,
];
return [
@@ -843,33 +935,37 @@ export class VaapiConfig extends BaseHWConfig {
'hwmap=derive_device=vaapi:reverse=1,format=vaapi',
];
}
getInputThreadOptions() {
return [`-threads 1`];
}
getColors() {
return {
primaries: 'bt709',
transfer: 'bt709',
matrix: 'bt709',
};
}
}
export class RkmppConfig extends BaseHWConfig {
protected hasMaliOpenCL: boolean;
export class RkmppSwDecodeConfig extends BaseHWConfig {
constructor(
protected config: SystemConfigFFmpegDto,
devices: string[] = [],
hasMaliOpenCL = false,
) {
super(config, devices);
this.hasMaliOpenCL = hasMaliOpenCL;
}
eligibleForTwoPass(): boolean {
return false;
}
getBaseInputOptions() {
getBaseInputOptions(): string[] {
if (this.devices.length === 0) {
throw new Error('No RKMPP device found');
}
if (this.config.accelDecode) {
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
}
return ['-init_hw_device rkmpp=hw', '-filter_hw_device hw'];
return [];
}
getPresetOptions() {
@@ -902,30 +998,41 @@ export class RkmppConfig extends BaseHWConfig {
return [VideoCodec.H264, VideoCodec.HEVC];
}
getVideoCodec(): string {
return `${this.config.targetVideoCodec}_rkmpp`;
}
}
export class RkmppHwDecodeConfig extends RkmppSwDecodeConfig {
getBaseInputOptions() {
if (this.devices.length === 0) {
throw new Error('No RKMPP device found');
}
return ['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga', '-noautorotate'];
}
getFilterOptions(videoStream: VideoStreamInfo) {
if (this.shouldToneMap(videoStream)) {
const { primaries, transfer, matrix } = this.getColors();
if (this.hasMaliOpenCL) {
return [
// use RKMPP for scaling, OpenCL for tone mapping
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
'hwmap=derive_device=opencl:mode=read',
`tonemap_opencl=format=nv12:r=pc:p=${primaries}:t=${transfer}:m=${matrix}:tonemap=${this.config.tonemap}:desat=0:tonemap_mode=lum:peak=100`,
'hwmap=derive_device=rkmpp:mode=write:reverse=1',
'format=drm_prime',
];
}
const colors = this.getColors();
return [
// use RKMPP for scaling, CPU for tone mapping (only works on RK3588, which supports 10-bit output)
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
'hwdownload',
'format=p010',
`tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`,
'hwupload',
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1`,
'hwmap=derive_device=opencl:mode=read',
`tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`,
'hwmap=derive_device=rkmpp:mode=write:reverse=1',
'format=drm_prime',
];
} else if (this.shouldScale(videoStream)) {
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1:async_depth=4`];
return [`scale_rkrga=${this.getScaling(videoStream)}:format=nv12:afbc=1`];
}
return [];
}
getColors() {
return {
primaries: 'bt709',
transfer: 'bt709',
matrix: 'bt709',
};
}
}

View File

@@ -15,32 +15,6 @@ import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
export class ImmichStartupError extends Error {}
export const isStartUpError = (error: unknown): error is ImmichStartupError => error instanceof ImmichStartupError;
export const getKeyByValue = (object: Record<string, unknown>, value: unknown) =>
Object.keys(object).find((key) => object[key] === value);
export const getMethodNames = (instance: any) => {
const ctx = Object.getPrototypeOf(instance);
const methods: string[] = [];
for (const property of Object.getOwnPropertyNames(ctx)) {
const descriptor = Object.getOwnPropertyDescriptor(ctx, property);
if (!descriptor || descriptor.get || descriptor.set) {
continue;
}
const handler = instance[property];
if (typeof handler !== 'function') {
continue;
}
methods.push(property);
}
return methods;
};
export const getExternalDomain = (server: SystemConfig['server'], port: number) =>
server.externalDomain || `http://localhost:${port}`;

View File

@@ -13,7 +13,8 @@ import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { ConfigRepository } from 'src/repositories/config.repository';
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
import { ApiService } from 'src/services/api.service';
import { isStartUpError, useSwagger } from 'src/utils/misc';
import { isStartUpError } from 'src/services/storage.service';
import { useSwagger } from 'src/utils/misc';
async function bootstrap() {
process.title = 'immich-api';

View File

@@ -7,7 +7,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
import { ConfigRepository } from 'src/repositories/config.repository';
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
import { isStartUpError } from 'src/utils/misc';
import { isStartUpError } from 'src/services/storage.service';
export async function bootstrap() {
const { telemetry } = new ConfigRepository().getEnv();

View File

@@ -17,7 +17,6 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
];
@@ -44,7 +43,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
{
index: 1,
@@ -55,7 +53,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),
@@ -71,7 +68,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),
@@ -87,7 +83,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),
@@ -107,23 +102,6 @@ export const probeStub = {
rotation: 0,
isHDR: true,
bitrate: 0,
pixelFormat: 'yuv420p10le',
},
],
}),
videoStream10Bit: Object.freeze<VideoInfo>({
...probeStubDefault,
videoStreams: [
{
index: 0,
height: 480,
width: 480,
codecName: 'h264',
frameCount: 100,
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p10le',
},
],
}),
@@ -139,7 +117,6 @@ export const probeStub = {
rotation: 90,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),
@@ -155,7 +132,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),
@@ -171,7 +147,6 @@ export const probeStub = {
rotation: 0,
isHDR: false,
bitrate: 0,
pixelFormat: 'yuv420p',
},
],
}),

View File

@@ -3,9 +3,7 @@ import { Mocked, vitest } from 'vitest';
export const newJobRepositoryMock = (): Mocked<IJobRepository> => {
return {
setup: vitest.fn(),
startWorkers: vitest.fn(),
run: vitest.fn(),
addHandler: vitest.fn(),
addCronJob: vitest.fn(),
updateCronJob: vitest.fn(),
setConcurrency: vitest.fn(),

View File

@@ -1,7 +1,6 @@
import { ChildProcessWithoutNullStreams } from 'node:child_process';
import { Writable } from 'node:stream';
import { PNG } from 'pngjs';
import { ImmichWorker } from 'src/enum';
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
import { BaseService } from 'src/services/base.service';
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
@@ -45,9 +44,8 @@ import { newViewRepositoryMock } from 'test/repositories/view.repository.mock';
import { Readable } from 'typeorm/platform/PlatformTools';
import { Mocked, vitest } from 'vitest';
type Overrides = {
worker?: ImmichWorker;
metadataRepository?: IMetadataRepository;
type RepositoryOverrides = {
metadataRepository: IMetadataRepository;
};
type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
type Constructor<Type, Args extends Array<any>> = {
@@ -56,11 +54,9 @@ type Constructor<Type, Args extends Array<any>> = {
export const newTestService = <T extends BaseService>(
Service: Constructor<T, BaseServiceArgs>,
overrides?: Overrides,
overrides?: RepositoryOverrides,
) => {
const { metadataRepository, worker: workerOverride } = overrides || {};
const worker = workerOverride || ImmichWorker.API;
const { metadataRepository } = overrides || {};
const accessMock = newAccessRepositoryMock();
const loggerMock = newLoggerRepositoryMock();
@@ -102,7 +98,6 @@ export const newTestService = <T extends BaseService>(
const viewMock = newViewRepositoryMock();
const sut = new Service(
worker,
loggerMock,
accessMock,
activityMock,

View File

@@ -1,4 +1,4 @@
FROM node:22.11.0-alpine3.20@sha256:f265794478aa0b1a23d85a492c8311ed795bc527c3fe7e43453b3c872dcd71a3
FROM node:22.10.0-alpine3.20@sha256:fc95a044b87e95507c60c1f8c829e5d98ddf46401034932499db370c494ef0ff
RUN apk add --no-cache tini
USER node

250
web/package-lock.json generated
View File

@@ -23,9 +23,9 @@
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
"luxon": "^3.4.4",
"socket.io-client": "~4.7.5",
"socket.io-client": "^4.7.4",
"svelte-gestures": "^5.0.4",
"svelte-i18n": "^4.0.1",
"svelte-i18n": "^4.0.0",
"svelte-local-storage-store": "^0.6.4",
"svelte-maplibre": "^0.9.13",
"thumbhash": "^0.1.1"
@@ -35,12 +35,12 @@
"@eslint/js": "^9.8.0",
"@faker-js/faker": "^9.0.0",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.3.0",
"@sveltejs/kit": "^2.7.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/kit": "^2.5.18",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4",
"@testing-library/svelte": "^5.2.0",
"@testing-library/user-event": "^14.5.2",
"@types/dom-to-image": "^2.6.7",
"@types/justified-layout": "^4.1.4",
@@ -63,7 +63,7 @@
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.6",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^5.1.5",
"svelte": "^4.2.19",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
@@ -80,7 +80,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.8.1",
"@types/node": "^22.8.0",
"typescript": "^5.3.3"
}
},
@@ -1994,42 +1994,43 @@
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0.tgz",
"integrity": "sha512-kpVJwF+gNiMEsoHaw+FJL76IYiwBikkxYU83+BpqQLdVMff19KeRKLd2wisS8niNBMJ2omv5gG+iGDDwd8jzag==",
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz",
"integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
"debug": "^4.3.7",
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
"debug": "^4.3.4",
"deepmerge": "^4.3.1",
"kleur": "^4.1.5",
"magic-string": "^0.30.12",
"vitefu": "^1.0.3"
"magic-string": "^0.30.10",
"svelte-hmr": "^0.16.0",
"vitefu": "^0.2.5"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
"node": "^18.0.0 || >=20"
},
"peerDependencies": {
"svelte": "^5.0.0-next.96 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",
"integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.7"
"debug": "^4.3.4"
},
"engines": {
"node": "^18.0.0 || ^20.0.0 || >=22"
"node": "^18.0.0 || >=20"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0",
"svelte": "^5.0.0-next.96 || ^5.0.0",
"@sveltejs/vite-plugin-svelte": "^3.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.0"
}
},
@@ -2229,6 +2230,7 @@
"resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.4.tgz",
"integrity": "sha512-EFdy73+lULQgMJ1WolAymrxWWrPv9DWyDuDFKKlUip2PA/EXuHptzfYOKWljccFWDKhhGOu3dqNmoc2f/h/Ecg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@testing-library/dom": "^10.0.0"
},
@@ -2795,15 +2797,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/acorn-typescript": {
"version": "1.4.13",
"resolved": "https://registry.npmjs.org/acorn-typescript/-/acorn-typescript-1.4.13.tgz",
"integrity": "sha512-xsc9Xv0xlVfwp2o7sQ+GCQ1PgbkdcpWdTzrwXxO3xDMTAywVS3oXVOcOHuRjAPkS4P9b+yc/qNF15460v+jp4Q==",
"license": "MIT",
"peerDependencies": {
"acorn": ">=8.9.0"
}
},
"node_modules/agent-base": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
@@ -2890,7 +2883,6 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -2968,12 +2960,11 @@
}
},
"node_modules/axobject-query": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
"dependencies": {
"dequal": "^2.0.3"
}
},
"node_modules/balanced-match": {
@@ -3280,6 +3271,18 @@
"node": ">=6"
}
},
"node_modules/code-red": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/code-red/-/code-red-1.0.4.tgz",
"integrity": "sha512-7qJWqItLA8/VPVlKJlFXU+NBlo/qyfs39aJcuMT/2ere32ZqvF5OSxgdM5xOfJJ7O429gg2HM47y8v9P+9wrNw==",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@types/estree": "^1.0.1",
"acorn": "^8.10.0",
"estree-walker": "^3.0.3",
"periscopic": "^3.1.0"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
@@ -3401,6 +3404,18 @@
"node": ">= 8"
}
},
"node_modules/css-tree": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
"integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
"dependencies": {
"mdn-data": "2.0.30",
"source-map-js": "^1.0.1"
},
"engines": {
"node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
}
},
"node_modules/css.escape": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
@@ -3490,12 +3505,11 @@
}
},
"node_modules/debug": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
"license": "MIT",
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
"dependencies": {
"ms": "^2.1.3"
"ms": "2.1.2"
},
"engines": {
"node": ">=6.0"
@@ -3642,23 +3656,22 @@
"dev": true
},
"node_modules/engine.io-client": {
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
"version": "6.6.1",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
"xmlhttprequest-ssl": "~2.1.1"
}
},
"node_modules/engine.io-parser": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
"license": "MIT",
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
"engines": {
"node": ">=10.0.0"
}
@@ -4138,7 +4151,8 @@
"node_modules/esm-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA==",
"dev": true
},
"node_modules/esniff": {
"version": "2.0.1",
@@ -4183,16 +4197,6 @@
"node": ">=0.10"
}
},
"node_modules/esrap": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/esrap/-/esrap-1.2.2.tgz",
"integrity": "sha512-F2pSJklxx1BlQIQgooczXCPHmcWpn6EsP5oo73LQfonG9fIlIENQ8vMmfGXeojP9MrkzUNAfyU5vdFlR9shHAw==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.4.15",
"@types/estree": "^1.0.1"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
@@ -4218,7 +4222,6 @@
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
"dev": true,
"dependencies": {
"@types/estree": "^1.0.0"
}
@@ -4959,7 +4962,6 @@
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
"license": "MIT",
"dependencies": {
"@types/estree": "*"
}
@@ -5327,9 +5329,9 @@
}
},
"node_modules/magic-string": {
"version": "0.30.12",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
"version": "0.30.11",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0"
@@ -5473,6 +5475,11 @@
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
}
},
"node_modules/mdn-data": {
"version": "2.0.30",
"resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
"integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA=="
},
"node_modules/memoizee": {
"version": "0.4.17",
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
@@ -5594,10 +5601,9 @@
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/murmurhash-js": {
"version": "1.0.0",
@@ -5937,6 +5943,16 @@
"pbf": "bin/pbf"
}
},
"node_modules/periscopic": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/periscopic/-/periscopic-3.1.0.tgz",
"integrity": "sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==",
"dependencies": {
"@types/estree": "^1.0.0",
"estree-walker": "^3.0.0",
"is-reference": "^3.0.0"
}
},
"node_modules/picocolors": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
@@ -6844,14 +6860,14 @@
}
},
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
"version": "4.8.1",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
"license": "MIT",
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.2",
"engine.io-client": "~6.5.2",
"engine.io-client": "~6.6.1",
"socket.io-parser": "~4.2.4"
},
"engines": {
@@ -6914,7 +6930,6 @@
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -7161,27 +7176,28 @@
}
},
"node_modules/svelte": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.5.tgz",
"integrity": "sha512-AyYondx6wS0g8mmBMfwJVnOYYBswjBv6L4bc99awfbET2KozWvVwxe8NSN7fhx7Pgr7pOfOXIv7K8+Impc0OoQ==",
"version": "4.2.19",
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "^2.3.0",
"@jridgewell/sourcemap-codec": "^1.5.0",
"@types/estree": "^1.0.5",
"acorn": "^8.12.1",
"acorn-typescript": "^1.4.13",
"aria-query": "^5.3.1",
"axobject-query": "^4.1.0",
"esm-env": "^1.0.0",
"esrap": "^1.2.2",
"is-reference": "^3.0.2",
"@ampproject/remapping": "^2.2.1",
"@jridgewell/sourcemap-codec": "^1.4.15",
"@jridgewell/trace-mapping": "^0.3.18",
"@types/estree": "^1.0.1",
"acorn": "^8.9.0",
"aria-query": "^5.3.0",
"axobject-query": "^4.0.0",
"code-red": "^1.0.3",
"css-tree": "^2.3.1",
"estree-walker": "^3.0.3",
"is-reference": "^3.0.1",
"locate-character": "^3.0.0",
"magic-string": "^0.30.11",
"zimmerframe": "^1.1.2"
"magic-string": "^0.30.4",
"periscopic": "^3.1.0"
},
"engines": {
"node": ">=18"
"node": ">=16"
}
},
"node_modules/svelte-check": {
@@ -7302,6 +7318,18 @@
"integrity": "sha512-kElJnoZrQtlkXE0O/RcKioz9NP0Sxx05j31ohyosNkydo6NOEsZB85mhoaCxOQNjxN+QPumYWfmIUsznYFjihA==",
"license": "MIT"
},
"node_modules/svelte-hmr": {
"version": "0.16.0",
"resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz",
"integrity": "sha512-Gyc7cOS3VJzLlfj7wKS0ZnzDVdv3Pn2IuVeJPk9m2skfhcu5bq3wtIZyQGggr7/Iim5rH5cncyQft/kRLupcnA==",
"dev": true,
"engines": {
"node": "^12.20 || ^14.13.1 || >= 16"
},
"peerDependencies": {
"svelte": "^3.19.0 || ^4.0.0"
}
},
"node_modules/svelte-i18n": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz",
@@ -7793,15 +7821,6 @@
"svelte": "^3.0.0 || ^4.0.0 || ^5.0.0-next.1"
}
},
"node_modules/svelte/node_modules/aria-query": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz",
"integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==",
"license": "Apache-2.0",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/symbol-tree": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@@ -8401,17 +8420,12 @@
}
},
"node_modules/vitefu": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz",
"integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==",
"version": "0.2.5",
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
"dev": true,
"license": "MIT",
"workspaces": [
"tests/deps/*",
"tests/projects/*"
],
"peerDependencies": {
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0-beta.0"
"vite": "^3.0.0 || ^4.0.0 || ^5.0.0"
},
"peerDependenciesMeta": {
"vite": {
@@ -8743,9 +8757,9 @@
"peer": true
},
"node_modules/xmlhttprequest-ssl": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
"engines": {
"node": ">=0.4.0"
}
@@ -8806,12 +8820,6 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zimmerframe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/zimmerframe/-/zimmerframe-1.1.2.tgz",
"integrity": "sha512-rAbqEGa8ovJy4pyBxZM70hg4pE6gDgaQ0Sl9M3enG3I0d6H4XSAM3GeNGLKnsBpuijUow064sf7ww1nutC5/3w==",
"license": "MIT"
}
}
}

View File

@@ -8,7 +8,7 @@
"build:stats": "BUILD_STATS=true vite build",
"package": "svelte-kit package",
"preview": "vite preview",
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings --compiler-warnings 'reactive_declaration_non_reactive_property:ignore'",
"check:svelte": "svelte-check --no-tsconfig --fail-on-warnings",
"check:typescript": "tsc --noEmit",
"check:watch": "npm run check:svelte -- --watch",
"check:code": "npm run format && npm run lint && npm run check:svelte && npm run check:typescript",
@@ -27,12 +27,12 @@
"@eslint/js": "^9.8.0",
"@faker-js/faker": "^9.0.0",
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/adapter-static": "^3.0.1",
"@sveltejs/enhanced-img": "^0.3.0",
"@sveltejs/kit": "^2.7.2",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@sveltejs/kit": "^2.5.18",
"@sveltejs/vite-plugin-svelte": "^3.1.2",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4",
"@testing-library/svelte": "^5.2.0",
"@testing-library/user-event": "^14.5.2",
"@types/dom-to-image": "^2.6.7",
"@types/justified-layout": "^4.1.4",
@@ -55,7 +55,7 @@
"prettier-plugin-sort-json": "^4.0.0",
"prettier-plugin-svelte": "^3.2.6",
"rollup-plugin-visualizer": "^5.12.0",
"svelte": "^5.1.5",
"svelte": "^4.2.19",
"svelte-check": "^4.0.0",
"tailwindcss": "^3.4.1",
"tslib": "^2.6.2",
@@ -79,9 +79,9 @@
"justified-layout": "^4.1.0",
"lodash-es": "^4.17.21",
"luxon": "^3.4.4",
"socket.io-client": "~4.7.5",
"socket.io-client": "^4.7.4",
"svelte-gestures": "^5.0.4",
"svelte-i18n": "^4.0.1",
"svelte-i18n": "^4.0.0",
"svelte-local-storage-store": "^0.6.4",
"svelte-maplibre": "^0.9.13",
"thumbhash": "^0.1.1"

View File

@@ -1,17 +0,0 @@
import { tick } from 'svelte';
import { vi } from 'vitest';
export const getAnimateMock = () =>
vi.fn().mockImplementation(() => {
let onfinish: (() => void) | null = null;
void tick().then(() => onfinish?.());
return {
set onfinish(fn: () => void) {
onfinish = fn;
},
cancel() {
onfinish = null;
},
};
});

View File

@@ -14,14 +14,14 @@
mdiPlay,
mdiSelectionSearch,
} from '@mdi/js';
import { type Component } from 'svelte';
import { type ComponentType } from 'svelte';
import { t } from 'svelte-i18n';
import JobTileButton from './job-tile-button.svelte';
import JobTileStatus from './job-tile-status.svelte';
export let title: string;
export let subtitle: string | undefined;
export let description: Component | undefined;
export let description: ComponentType | undefined;
export let jobCounts: JobCountsDto;
export let queueStatus: QueueStatusDto;
export let icon: string;

View File

@@ -19,7 +19,7 @@
mdiTagFaces,
mdiVideo,
} from '@mdi/js';
import type { Component } from 'svelte';
import type { ComponentType } from 'svelte';
import JobTile from './job-tile.svelte';
import StorageMigrationDescription from './storage-migration-description.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
@@ -30,7 +30,7 @@
interface JobDetails {
title: string;
subtitle?: string;
description?: Component;
description?: ComponentType;
allText?: string;
refreshText?: string;
missingText: string;
@@ -56,7 +56,6 @@
await handleCommand(jobId, dto);
};
// svelte-ignore reactive_declaration_non_reactive_property
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
[JobName.ThumbnailGeneration]: {
icon: mdiFileJpgBox,

View File

@@ -343,6 +343,15 @@
subtitle={$t('admin.transcoding_advanced_options_description')}
>
<div class="ml-4 mt-4 flex flex-col gap-4">
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_tone_mapping_npl')}
desc={$t('admin.transcoding_tone_mapping_npl_description')}
bind:value={config.ffmpeg.npl}
isEdited={config.ffmpeg.npl !== savedConfig.ffmpeg.npl}
{disabled}
/>
<SettingInputField
inputType={SettingInputFieldType.NUMBER}
label={$t('admin.transcoding_max_b_frames')}

View File

@@ -87,7 +87,6 @@
}
}
// svelte-ignore reactive_declaration_non_reactive_property
$: {
if (selectedGroupOption.id === AlbumGroupBy.None) {
groupIcon = mdiFolderRemoveOutline;
@@ -97,10 +96,8 @@
}
}
// svelte-ignore reactive_declaration_non_reactive_property
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
// svelte-ignore reactive_declaration_non_reactive_property
$: albumFilterNames = ((): Record<AlbumFilter, string> => {
return {
[AlbumFilter.All]: $t('all'),
@@ -109,7 +106,6 @@
};
})();
// svelte-ignore reactive_declaration_non_reactive_property
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
return {
[AlbumSortBy.Title]: $t('sort_title'),
@@ -121,7 +117,6 @@
};
})();
// svelte-ignore reactive_declaration_non_reactive_property
$: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
return {
[AlbumGroupBy.None]: $t('group_no'),

View File

@@ -135,7 +135,6 @@
let isOpen = false;
// Step 1: Filter between Owned and Shared albums, or both.
// svelte-ignore reactive_declaration_non_reactive_property
$: {
switch (userSettings.filter) {
case AlbumFilter.Owned: {

View File

@@ -13,7 +13,7 @@
$albumViewSettings.sortOrder = option.defaultOrder;
}
};
// svelte-ignore reactive_declaration_non_reactive_property
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
return {
[AlbumSortBy.Title]: $t('sort_title'),

View File

@@ -293,7 +293,7 @@
class="h-[18px] {disabled
? 'cursor-not-allowed'
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
></textarea>
/>
</div>
{#if isSendingMessage}
<div class="flex items-end place-items-center pb-2 ml-0">

View File

@@ -15,31 +15,13 @@ describe('AssetViewerNavBar component', () => {
showShareButton: false,
onZoomImage: () => {},
onCopyImage: () => {},
onAction: () => {},
onRunJob: () => {},
onPlaySlideshow: () => {},
onShowDetail: () => {},
onClose: () => {},
};
beforeAll(() => {
Element.prototype.animate = vi.fn().mockImplementation(() => ({
cancel: () => {},
}));
vi.stubGlobal(
'ResizeObserver',
vi.fn(() => ({ observe: vi.fn(), unobserve: vi.fn(), disconnect: vi.fn() })),
);
});
afterEach(() => {
vi.resetAllMocks();
resetSavedUser();
});
afterAll(() => {
vi.restoreAllMocks();
});
it('shows back button', () => {
const asset = assetFactory.build({ isTrashed: false });
const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });

View File

@@ -61,7 +61,6 @@
const sharedLink = getSharedLink();
$: isOwner = $user && asset.ownerId === $user?.id;
// svelte-ignore reactive_declaration_non_reactive_property
$: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
// $: showEditorButton =
// isOwner &&

View File

@@ -598,7 +598,7 @@
{#if stackedAsset.id == asset.id}
<div class="w-full flex place-items-center place-content-center">
<div class="w-2 h-2 bg-white rounded-full flex mt-[2px]"></div>
<div class="w-2 h-2 bg-white rounded-full flex mt-[2px]" />
</div>
{/if}
</div>

View File

@@ -154,7 +154,7 @@
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
<p>
{#if $user?.isAdmin}
{$t('admin.asset_offline_description')}
<p>{$t('admin.asset_offline_description')}</p>
{:else}
{$t('asset_offline_description')}
{/if}
@@ -345,45 +345,43 @@
</Portal>
{/if}
<div class="flex gap-4 py-4">
<div><Icon path={mdiImageOutline} size="24" /></div>
{#if asset.exifInfo?.fileSizeInByte}
<div class="flex gap-4 py-4">
<div><Icon path={mdiImageOutline} size="24" /></div>
<div>
<p class="break-all flex place-items-center gap-2">
{asset.originalFileName}
{#if isOwner}
<CircleIconButton
icon={mdiInformationOutline}
title={$t('show_file_location')}
size="16"
padding="2"
on:click={toggleAssetPath}
/>
{/if}
</p>
{#if showAssetPath}
<p class="text-xs opacity-50 break-all pb-2" transition:slide={{ duration: 250 }}>
{asset.originalPath}
<div>
<p class="break-all flex place-items-center gap-2">
{asset.originalFileName}
{#if isOwner}
<CircleIconButton
icon={mdiInformationOutline}
title={$t('show_file_location')}
size="16"
padding="2"
on:click={toggleAssetPath}
/>
{/if}
</p>
{/if}
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
<div class="flex gap-2 text-sm">
{#if asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth}
{#if asset.exifInfo.exifImageHeight && asset.exifInfo.exifImageWidth}
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
<p>
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
</p>
{@const { width, height } = getDimensions(asset.exifInfo)}
<p>{width} x {height}</p>
{/if}
{@const { width, height } = getDimensions(asset.exifInfo)}
<p>{width} x {height}</p>
{/if}
{#if asset.exifInfo?.fileSizeInByte}
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
{/if}
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
</div>
{/if}
{#if showAssetPath}
<p class="text-xs opacity-50 break-all" transition:slide={{ duration: 250 }}>
{asset.originalPath}
</p>
{/if}
</div>
</div>
</div>
{/if}
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
<div class="flex gap-4 py-4">

View File

@@ -32,7 +32,7 @@
</div>
<div class="flex place-items-center gap-2">
<div class="h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full bg-immich-primary" style={`width: ${download.percentage}%`}></div>
<div class="h-[7px] rounded-full bg-immich-primary" style={`width: ${download.percentage}%`} />
</div>
<p class="min-w-[4em] whitespace-nowrap text-right">
<span class="text-immich-primary">

View File

@@ -23,7 +23,6 @@
export let onClose: () => void;
let selectedType: string = editTypes[0].name;
// svelte-ignore reactive_declaration_non_reactive_property
$: selectedTypeObj = editTypes.find((t) => t.name === selectedType) || editTypes[0];
setTimeout(() => {

Some files were not shown because too many files have changed in this diff Show More