Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9764326880 | |||
| 1ec5d89723 | |||
| 17181e3330 | |||
| 7eccf99797 |
+1
-1
@@ -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
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
Generated
+5
-5
@@ -24,7 +24,7 @@
|
|||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@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/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"@vitest/coverage-v8": "^2.0.5",
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1378,9 +1378,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "22.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@
|
|||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@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/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"@vitest/coverage-v8": "^2.0.5",
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ The default configuration looks like this:
|
|||||||
"bframes": -1,
|
"bframes": -1,
|
||||||
"refs": 0,
|
"refs": 0,
|
||||||
"gopSize": 0,
|
"gopSize": 0,
|
||||||
|
"npl": 0,
|
||||||
"temporalAQ": false,
|
"temporalAQ": false,
|
||||||
"cqMode": "auto",
|
"cqMode": "auto",
|
||||||
"twoPass": false,
|
"twoPass": false,
|
||||||
|
|||||||
@@ -83,12 +83,6 @@ const projects: CommunityProjectProps[] = [
|
|||||||
description: 'Power tools for organizing your immich library.',
|
description: 'Power tools for organizing your immich library.',
|
||||||
url: 'https://github.com/varun-raj/immich-power-tools',
|
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 {
|
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||||
|
|||||||
Generated
+17
-17
@@ -15,7 +15,7 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@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/eslint-plugin": "^8.0.0",
|
||||||
"@typescript-eslint/parser": "^8.0.0",
|
"@typescript-eslint/parser": "^8.0.0",
|
||||||
"@vitest/coverage-v8": "^2.0.5",
|
"@vitest/coverage-v8": "^2.0.5",
|
||||||
@@ -99,7 +99,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1613,9 +1613,9 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "22.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3294,9 +3294,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored": {
|
"node_modules/exiftool-vendored": {
|
||||||
"version": "28.7.0",
|
"version": "28.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -3307,14 +3307,14 @@
|
|||||||
"luxon": "^3.5.0"
|
"luxon": "^3.5.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"exiftool-vendored.exe": "12.99.0",
|
"exiftool-vendored.exe": "12.97.0",
|
||||||
"exiftool-vendored.pl": "12.99.0"
|
"exiftool-vendored.pl": "12.97.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored.exe": {
|
"node_modules/exiftool-vendored.exe": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
@@ -3323,9 +3323,9 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored.pl": {
|
"node_modules/exiftool-vendored.pl": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
|
|||||||
+1
-1
@@ -25,7 +25,7 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
|
|||||||
@@ -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 () => {
|
it(`should upload and generate a thumbnail for different file types`, async () => {
|
||||||
|
|||||||
+1
-1
Submodule e2e/test-assets updated: 99544a2004...3e057d2f58
@@ -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_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": "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_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": "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_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",
|
"transcoding_two_pass_encoding": "Two-pass encoding",
|
||||||
|
|||||||
Generated
+9
-9
@@ -946,13 +946,13 @@ tqdm = ["tqdm"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ftfy"
|
name = "ftfy"
|
||||||
version = "6.3.1"
|
version = "6.3.0"
|
||||||
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
description = "Fixes mojibake and other problems with Unicode, after the fact"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083"},
|
{file = "ftfy-6.3.0-py3-none-any.whl", hash = "sha256:17aca296801f44142e3ff2c16f93fbf6a87609ebb3704a9a41dd5d4903396caf"},
|
||||||
{file = "ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec"},
|
{file = "ftfy-6.3.0.tar.gz", hash = "sha256:1c7d6418e72b25a7760feb150acf574b86924dbb2e95b32c0b3abbd1ba3d7ad6"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -1609,13 +1609,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locust"
|
name = "locust"
|
||||||
version = "2.32.1"
|
version = "2.32.0"
|
||||||
description = "Developer-friendly load testing framework"
|
description = "Developer-friendly load testing framework"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "locust-2.32.1-py3-none-any.whl", hash = "sha256:3fb5548b4f2b6477fa5229ee55ac3dddbae56e86c3430bf2ba3fee358eb7e7bb"},
|
{file = "locust-2.32.0-py3-none-any.whl", hash = "sha256:e004514332b8631ca91382d11d224baee4ced040c5f5c8b2233800ebcbc73c0e"},
|
||||||
{file = "locust-2.32.1.tar.gz", hash = "sha256:8c3b1094dbf20860fd2f6e26b68f0c6064dc28054f4462664389d102fce1448b"},
|
{file = "locust-2.32.0.tar.gz", hash = "sha256:d8f7f5d9d4e801b2e7b0ee3f31109333673da744ccedf85e7da0151f2d263dd9"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -2749,13 +2749,13 @@ cli = ["click (>=5.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.17"
|
version = "0.0.12"
|
||||||
description = "A streaming multipart parser for Python"
|
description = "A streaming multipart parser for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "python_multipart-0.0.17-py3-none-any.whl", hash = "sha256:15dc4f487e0a9476cc1201261188ee0940165cffc94429b6fc565c4d3045cb5d"},
|
{file = "python_multipart-0.0.12-py3-none-any.whl", hash = "sha256:43dcf96cf65888a9cd3423544dd0d75ac10f7aa0c3c28a175bbcd00c9ce1aebf"},
|
||||||
{file = "python_multipart-0.0.17.tar.gz", hash = "sha256:41330d831cae6e2f22902704ead2826ea038d0419530eadff3ea80175aec5538"},
|
{file = "python_multipart-0.0.12.tar.gz", hash = "sha256:045e1f98d719c1ce085ed7f7e1ef9d8ccc8c02ba02b5566d5f7521410ced58cb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"flutter": "3.24.4"
|
"flutter": "3.24.3"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,13 @@ target 'Runner' do
|
|||||||
use_modular_headers!
|
use_modular_headers!
|
||||||
|
|
||||||
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
|
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
|
end
|
||||||
|
|
||||||
post_install do |installer|
|
post_install do |installer|
|
||||||
|
|||||||
+19
-3
@@ -48,7 +48,7 @@ PODS:
|
|||||||
- flutter_udid (0.0.1):
|
- flutter_udid (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- SAMKeychain
|
- SAMKeychain
|
||||||
- flutter_web_auth (0.6.0):
|
- flutter_web_auth (0.5.0):
|
||||||
- Flutter
|
- Flutter
|
||||||
- fluttertoast (0.0.2):
|
- fluttertoast (0.0.2):
|
||||||
- Flutter
|
- Flutter
|
||||||
@@ -81,6 +81,14 @@ PODS:
|
|||||||
- SDWebImage (5.19.4):
|
- SDWebImage (5.19.4):
|
||||||
- SDWebImage/Core (= 5.19.4)
|
- SDWebImage/Core (= 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):
|
- share_plus (0.0.1):
|
||||||
- Flutter
|
- Flutter
|
||||||
- shared_preferences_foundation (0.0.1):
|
- shared_preferences_foundation (0.0.1):
|
||||||
@@ -120,6 +128,8 @@ DEPENDENCIES:
|
|||||||
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
- path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`)
|
||||||
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
|
||||||
- photo_manager (from `.symlinks/plugins/photo_manager/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`)
|
- share_plus (from `.symlinks/plugins/share_plus/ios`)
|
||||||
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
|
||||||
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
- sqflite (from `.symlinks/plugins/sqflite/darwin`)
|
||||||
@@ -178,6 +188,10 @@ EXTERNAL SOURCES:
|
|||||||
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
:path: ".symlinks/plugins/permission_handler_apple/ios"
|
||||||
photo_manager:
|
photo_manager:
|
||||||
:path: ".symlinks/plugins/photo_manager/ios"
|
: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:
|
share_plus:
|
||||||
:path: ".symlinks/plugins/share_plus/ios"
|
:path: ".symlinks/plugins/share_plus/ios"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
@@ -202,7 +216,7 @@ SPEC CHECKSUMS:
|
|||||||
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086
|
||||||
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778
|
||||||
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04
|
||||||
flutter_web_auth: acc15a8fd7bba796a933c724a6dffc3d00f07c27
|
flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d
|
||||||
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c
|
||||||
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450
|
||||||
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1
|
||||||
@@ -217,6 +231,8 @@ SPEC CHECKSUMS:
|
|||||||
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
|
||||||
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
|
||||||
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d
|
||||||
|
share_handler_ios: 088138c17ce73333b4875a35860cd52604041521
|
||||||
|
share_handler_ios_models: fc638c9b4330dc7f082586c92aee9dfa0b87b871
|
||||||
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad
|
||||||
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
|
||||||
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec
|
||||||
@@ -226,6 +242,6 @@ SPEC CHECKSUMS:
|
|||||||
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
|
||||||
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1
|
||||||
|
|
||||||
PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d
|
PODFILE CHECKSUM: 1d6c11dc7539b1a7e0f9cc83a63904e0f8555f72
|
||||||
|
|
||||||
COCOAPODS: 1.15.2
|
COCOAPODS: 1.15.2
|
||||||
|
|||||||
@@ -16,8 +16,22 @@
|
|||||||
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
|
||||||
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
|
||||||
D218389C4A4C4693F141F7D1 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */; };
|
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 */
|
/* 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 */
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
isa = PBXCopyFilesBuildPhase;
|
isa = PBXCopyFilesBuildPhase;
|
||||||
@@ -29,6 +43,17 @@
|
|||||||
name = "Embed Frameworks";
|
name = "Embed Frameworks";
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
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 */
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXFileReference 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>"; };
|
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>"; };
|
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>"; };
|
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; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = RunnerProfile.entitlements; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
@@ -63,6 +98,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
FA87C4652C5AADE800109ACC /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FA74AF1DE1B65E796CC9A190 /* Pods_ShareExtension.framework in Frameworks */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXFrameworksBuildPhase section */
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXGroup section */
|
/* Begin PBXGroup section */
|
||||||
@@ -72,6 +115,9 @@
|
|||||||
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
|
2E3441B73560D0F6FD25E04F /* Pods-Runner.debug.xcconfig */,
|
||||||
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
|
E0E99CDC17B3EB7FA8BA2332 /* Pods-Runner.release.xcconfig */,
|
||||||
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
|
F7101BB0391A314774615E89 /* Pods-Runner.profile.xcconfig */,
|
||||||
|
B4CED4956FDE3EF1F786D7AD /* Pods-ShareExtension.debug.xcconfig */,
|
||||||
|
D1C2E3E4FBFF4719EF41D366 /* Pods-ShareExtension.release.xcconfig */,
|
||||||
|
7B82909F828DFBC0DC7A92BF /* Pods-ShareExtension.profile.xcconfig */,
|
||||||
);
|
);
|
||||||
path = Pods;
|
path = Pods;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -80,6 +126,7 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
|
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */,
|
||||||
|
F2F1F1634A89EAAC89566694 /* Pods_ShareExtension.framework */,
|
||||||
);
|
);
|
||||||
name = Frameworks;
|
name = Frameworks;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -109,6 +156,7 @@
|
|||||||
children = (
|
children = (
|
||||||
9740EEB11CF90186004384FC /* Flutter */,
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
97C146F01CF9000F007C117D /* Runner */,
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
FA87C4692C5AADE800109ACC /* ShareExtension */,
|
||||||
97C146EF1CF9000F007C117D /* Products */,
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
0FB772A5B9601143383626CA /* Pods */,
|
0FB772A5B9601143383626CA /* Pods */,
|
||||||
1754452DD81DA6620E279E51 /* Frameworks */,
|
1754452DD81DA6620E279E51 /* Frameworks */,
|
||||||
@@ -118,7 +166,8 @@
|
|||||||
97C146EF1CF9000F007C117D /* Products */ = {
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
97C146EE1CF9000F007C117D /* Immich-Debug.app */,
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
FA87C4682C5AADE800109ACC /* ShareExtension.appex */,
|
||||||
);
|
);
|
||||||
name = Products;
|
name = Products;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@@ -126,6 +175,7 @@
|
|||||||
97C146F01CF9000F007C117D /* Runner */ = {
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
FA87C4782C5AAE3500109ACC /* Runner.entitlements */,
|
||||||
65DD438629917FAD0047FFA8 /* BackgroundSync */,
|
65DD438629917FAD0047FFA8 /* BackgroundSync */,
|
||||||
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
FAC7416727DB9F5500C668D8 /* RunnerProfile.entitlements */,
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
@@ -140,6 +190,17 @@
|
|||||||
path = Runner;
|
path = Runner;
|
||||||
sourceTree = "<group>";
|
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 */
|
/* End PBXGroup section */
|
||||||
|
|
||||||
/* Begin PBXNativeTarget section */
|
/* Begin PBXNativeTarget section */
|
||||||
@@ -153,6 +214,7 @@
|
|||||||
97C146EB1CF9000F007C117D /* Frameworks */,
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
97C146EC1CF9000F007C117D /* Resources */,
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
FA87C4732C5AADE800109ACC /* Embed Foundation Extensions */,
|
||||||
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */,
|
||||||
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */,
|
6724EEB7D74949FA08581154 /* [CP] Copy Pods Resources */,
|
||||||
@@ -160,12 +222,31 @@
|
|||||||
buildRules = (
|
buildRules = (
|
||||||
);
|
);
|
||||||
dependencies = (
|
dependencies = (
|
||||||
|
FA87C4712C5AADE800109ACC /* PBXTargetDependency */,
|
||||||
);
|
);
|
||||||
name = Runner;
|
name = Runner;
|
||||||
productName = Runner;
|
productName = Runner;
|
||||||
productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
productType = "com.apple.product-type.application";
|
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 */
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
/* Begin PBXProject section */
|
/* Begin PBXProject section */
|
||||||
@@ -173,6 +254,7 @@
|
|||||||
isa = PBXProject;
|
isa = PBXProject;
|
||||||
attributes = {
|
attributes = {
|
||||||
BuildIndependentTargetsInParallel = YES;
|
BuildIndependentTargetsInParallel = YES;
|
||||||
|
LastSwiftUpdateCheck = 1510;
|
||||||
LastUpgradeCheck = 1510;
|
LastUpgradeCheck = 1510;
|
||||||
ORGANIZATIONNAME = "";
|
ORGANIZATIONNAME = "";
|
||||||
TargetAttributes = {
|
TargetAttributes = {
|
||||||
@@ -180,6 +262,9 @@
|
|||||||
CreatedOnToolsVersion = 7.3.1;
|
CreatedOnToolsVersion = 7.3.1;
|
||||||
LastSwiftMigration = 1100;
|
LastSwiftMigration = 1100;
|
||||||
};
|
};
|
||||||
|
FA87C4672C5AADE800109ACC = {
|
||||||
|
CreatedOnToolsVersion = 15.1;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
|
||||||
@@ -196,6 +281,7 @@
|
|||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
targets = (
|
targets = (
|
||||||
97C146ED1CF9000F007C117D /* Runner */,
|
97C146ED1CF9000F007C117D /* Runner */,
|
||||||
|
FA87C4672C5AADE800109ACC /* ShareExtension */,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
/* End PBXProject section */
|
/* End PBXProject section */
|
||||||
@@ -212,6 +298,14 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
FA87C4662C5AADE800109ACC /* Resources */ = {
|
||||||
|
isa = PBXResourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FA87C46E2C5AADE800109ACC /* MainInterface.storyboard in Resources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXResourcesBuildPhase section */
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
/* Begin PBXShellScriptBuildPhase section */
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
@@ -270,6 +364,28 @@
|
|||||||
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
|
||||||
showEnvVarsInLog = 0;
|
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 */ = {
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
isa = PBXShellScriptBuildPhase;
|
isa = PBXShellScriptBuildPhase;
|
||||||
alwaysOutOfDate = 1;
|
alwaysOutOfDate = 1;
|
||||||
@@ -316,8 +432,24 @@
|
|||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
FA87C4642C5AADE800109ACC /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
FA87C46B2C5AADE800109ACC /* ShareViewController.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
/* End PBXSourcesBuildPhase section */
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
FA87C4712C5AADE800109ACC /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = FA87C4672C5AADE800109ACC /* ShareExtension */;
|
||||||
|
targetProxy = FA87C4702C5AADE800109ACC /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
/* Begin PBXVariantGroup section */
|
/* Begin PBXVariantGroup section */
|
||||||
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
isa = PBXVariantGroup;
|
isa = PBXVariantGroup;
|
||||||
@@ -335,6 +467,14 @@
|
|||||||
name = LaunchScreen.storyboard;
|
name = LaunchScreen.storyboard;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
FA87C46C2C5AADE800109ACC /* MainInterface.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
FA87C46D2C5AADE800109ACC /* Base */,
|
||||||
|
);
|
||||||
|
name = MainInterface.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
/* End PBXVariantGroup section */
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
/* Begin XCBuildConfiguration section */
|
/* Begin XCBuildConfiguration section */
|
||||||
@@ -396,23 +536,25 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 181;
|
CURRENT_PROJECT_VERSION = 181;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.107.0;
|
MARKETING_VERSION = 1.107.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = "Immich-Profile";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -539,22 +681,25 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 181;
|
CURRENT_PROJECT_VERSION = 181;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.107.0;
|
MARKETING_VERSION = 1.107.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = "Immich-Debug";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||||
@@ -567,22 +712,25 @@
|
|||||||
isa = XCBuildConfiguration;
|
isa = XCBuildConfiguration;
|
||||||
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
|
||||||
buildSettings = {
|
buildSettings = {
|
||||||
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
|
||||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
|
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 181;
|
CURRENT_PROJECT_VERSION = 181;
|
||||||
|
CUSTOM_GROUP_ID = group.app.immich.share;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 13.0;
|
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||||
LD_RUNPATH_SEARCH_PATHS = (
|
LD_RUNPATH_SEARCH_PATHS = (
|
||||||
"$(inherited)",
|
"$(inherited)",
|
||||||
"@executable_path/Frameworks",
|
"@executable_path/Frameworks",
|
||||||
);
|
);
|
||||||
MARKETING_VERSION = 1.107.0;
|
MARKETING_VERSION = 1.107.0;
|
||||||
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
|
||||||
PRODUCT_NAME = Immich;
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||||
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
@@ -590,6 +738,129 @@
|
|||||||
};
|
};
|
||||||
name = Release;
|
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 */
|
/* End XCBuildConfiguration section */
|
||||||
|
|
||||||
/* Begin XCConfigurationList section */
|
/* Begin XCConfigurationList section */
|
||||||
@@ -613,6 +884,16 @@
|
|||||||
defaultConfigurationIsVisible = 0;
|
defaultConfigurationIsVisible = 0;
|
||||||
defaultConfigurationName = Release;
|
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 */
|
/* End XCConfigurationList section */
|
||||||
};
|
};
|
||||||
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
rootObject = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Immich.app"
|
BuildableName = "Runner.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Immich.app"
|
BuildableName = "Runner.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Immich.app"
|
BuildableName = "Runner.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
@@ -73,7 +73,7 @@
|
|||||||
<BuildableReference
|
<BuildableReference
|
||||||
BuildableIdentifier = "primary"
|
BuildableIdentifier = "primary"
|
||||||
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
BuildableName = "Immich.app"
|
BuildableName = "Runner.app"
|
||||||
BlueprintName = "Runner"
|
BlueprintName = "Runner"
|
||||||
ReferencedContainer = "container:Runner.xcodeproj">
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
</BuildableReference>
|
</BuildableReference>
|
||||||
|
|||||||
+160
-121
@@ -1,124 +1,163 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict>
|
<dict>
|
||||||
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
<key>BGTaskSchedulerPermittedIdentifiers</key>
|
||||||
<array>
|
<array>
|
||||||
<string>app.alextran.immich.backgroundFetch</string>
|
<string>app.alextran.immich.backgroundFetch</string>
|
||||||
<string>app.alextran.immich.backgroundProcessing</string>
|
<string>app.alextran.immich.backgroundProcessing</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CADisableMinimumFrameDurationOnPhone</key>
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
<true/>
|
<true />
|
||||||
<key>CFBundleDevelopmentRegion</key>
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
<key>CFBundleDisplayName</key>
|
<key>CFBundleDisplayName</key>
|
||||||
<string>${PRODUCT_NAME}</string>
|
<string>Immich</string>
|
||||||
<key>CFBundleExecutable</key>
|
<key>CFBundleExecutable</key>
|
||||||
<string>$(EXECUTABLE_NAME)</string>
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
<key>CFBundleIdentifier</key>
|
<key>CFBundleIdentifier</key>
|
||||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<key>CFBundleInfoDictionaryVersion</key>
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
<string>6.0</string>
|
<string>6.0</string>
|
||||||
<key>CFBundleLocalizations</key>
|
<key>CFBundleLocalizations</key>
|
||||||
<array>
|
<array>
|
||||||
<string>en</string>
|
<string>en</string>
|
||||||
<string>ar</string>
|
<string>ar</string>
|
||||||
<string>ca</string>
|
<string>ca</string>
|
||||||
<string>cs</string>
|
<string>cs</string>
|
||||||
<string>da</string>
|
<string>da</string>
|
||||||
<string>de</string>
|
<string>de</string>
|
||||||
<string>es</string>
|
<string>es</string>
|
||||||
<string>fi</string>
|
<string>fi</string>
|
||||||
<string>fr</string>
|
<string>fr</string>
|
||||||
<string>he</string>
|
<string>he</string>
|
||||||
<string>hi</string>
|
<string>hi</string>
|
||||||
<string>hu</string>
|
<string>hu</string>
|
||||||
<string>it</string>
|
<string>it</string>
|
||||||
<string>ja</string>
|
<string>ja</string>
|
||||||
<string>ko</string>
|
<string>ko</string>
|
||||||
<string>lv</string>
|
<string>lv</string>
|
||||||
<string>mn</string>
|
<string>mn</string>
|
||||||
<string>nb</string>
|
<string>nb</string>
|
||||||
<string>nl</string>
|
<string>nl</string>
|
||||||
<string>pl</string>
|
<string>pl</string>
|
||||||
<string>pt</string>
|
<string>pt</string>
|
||||||
<string>ro</string>
|
<string>ro</string>
|
||||||
<string>ru</string>
|
<string>ru</string>
|
||||||
<string>sk</string>
|
<string>sk</string>
|
||||||
<string>sl</string>
|
<string>sl</string>
|
||||||
<string>sr</string>
|
<string>sr</string>
|
||||||
<string>sv</string>
|
<string>sv</string>
|
||||||
<string>th</string>
|
<string>th</string>
|
||||||
<string>uk</string>
|
<string>uk</string>
|
||||||
<string>vi</string>
|
<string>vi</string>
|
||||||
<string>zh</string>
|
<string>zh</string>
|
||||||
</array>
|
</array>
|
||||||
<key>CFBundleName</key>
|
<key>CFBundleName</key>
|
||||||
<string>immich_mobile</string>
|
<string>immich_mobile</string>
|
||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.119.0</string>
|
<string>1.107.1</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleURLTypes</key>
|
||||||
<string>181</string>
|
<array>
|
||||||
<key>FLTEnableImpeller</key>
|
<dict>
|
||||||
<true/>
|
<key>CFBundleTypeRole</key>
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<string>Editor</string>
|
||||||
<false/>
|
<key>CFBundleURLSchemes</key>
|
||||||
<key>LSApplicationQueriesSchemes</key>
|
<array>
|
||||||
<array>
|
<string>ShareMedia-$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
<string>https</string>
|
</array>
|
||||||
</array>
|
</dict>
|
||||||
<key>LSRequiresIPhoneOS</key>
|
</array>
|
||||||
<true/>
|
<key>CFBundleVersion</key>
|
||||||
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
<string>162</string>
|
||||||
<true/>
|
<key>FLTEnableImpeller</key>
|
||||||
<key>NSAppTransportSecurity</key>
|
<true />
|
||||||
<dict>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
<key>NSAllowsArbitraryLoads</key>
|
<false />
|
||||||
<true/>
|
<key>LSApplicationQueriesSchemes</key>
|
||||||
</dict>
|
<array>
|
||||||
<key>NSCameraUsageDescription</key>
|
<string>https</string>
|
||||||
<string>We need to access the camera to let you take beautiful video using this app</string>
|
</array>
|
||||||
<key>NSLocationWhenInUseUsageDescription</key>
|
<key>LSRequiresIPhoneOS</key>
|
||||||
<string>Enable location setting to show position of assets on map</string>
|
<true />
|
||||||
<key>NSMicrophoneUsageDescription</key>
|
<key>MGLMapboxMetricsEnabledSettingShownInApp</key>
|
||||||
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
<true />
|
||||||
<key>NSPhotoLibraryAddUsageDescription</key>
|
<key>NSAppTransportSecurity</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<dict>
|
||||||
<key>NSPhotoLibraryUsageDescription</key>
|
<key>NSAllowsArbitraryLoads</key>
|
||||||
<string>We need to manage backup your photos album</string>
|
<true />
|
||||||
<key>UIApplicationSupportsIndirectInputEvents</key>
|
</dict>
|
||||||
<true/>
|
<key>NSCameraUsageDescription</key>
|
||||||
<key>UIBackgroundModes</key>
|
<string>We need to access the camera to let you take beautiful video using this app</string>
|
||||||
<array>
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
<string>fetch</string>
|
<string>Enable location setting to show position of assets on map</string>
|
||||||
<string>processing</string>
|
<key>NSMicrophoneUsageDescription</key>
|
||||||
</array>
|
<string>We need to access the microphone to let you take beautiful video using this app</string>
|
||||||
<key>UILaunchStoryboardName</key>
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
||||||
<string>LaunchScreen</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>UIMainStoryboardFile</key>
|
<key>NSPhotoLibraryUsageDescription</key>
|
||||||
<string>Main</string>
|
<string>We need to manage backup your photos album</string>
|
||||||
<key>UIStatusBarHidden</key>
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
<false/>
|
<true />
|
||||||
<key>UISupportedInterfaceOrientations</key>
|
<key>UIBackgroundModes</key>
|
||||||
<array>
|
<array>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>fetch</string>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<string>processing</string>
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
</array>
|
||||||
</array>
|
<key>UILaunchStoryboardName</key>
|
||||||
<key>UISupportedInterfaceOrientations~ipad</key>
|
<string>LaunchScreen</string>
|
||||||
<array>
|
<key>UIMainStoryboardFile</key>
|
||||||
<string>UIInterfaceOrientationPortrait</string>
|
<string>Main</string>
|
||||||
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
<key>UIStatusBarHidden</key>
|
||||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
<false />
|
||||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
</array>
|
<array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
<true/>
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
<key>io.flutter.embedded_views_preview</key>
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
<true/>
|
</array>
|
||||||
</dict>
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
</plist>
|
<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>
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?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">
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
<plist version="1.0">
|
<plist version="1.0">
|
||||||
<dict/>
|
<dict>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.app.immich.share</string>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -4,5 +4,9 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>aps-environment</key>
|
<key>aps-environment</key>
|
||||||
<string>development</string>
|
<string>development</string>
|
||||||
|
<key>com.apple.security.application-groups</key>
|
||||||
|
<array>
|
||||||
|
<string>group.app.immich.share</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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 > 0 ).@count
|
||||||
|
> 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>
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
//
|
||||||
|
// ShareViewController.swift
|
||||||
|
// ShareExtension
|
||||||
|
//
|
||||||
|
// Created by Alex Tran on 7/31/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import share_handler_ios_models
|
||||||
|
|
||||||
|
class ShareViewController: ShareHandlerIosViewController {}
|
||||||
+25
-1
@@ -10,6 +10,7 @@ import 'package:flutter/services.dart';
|
|||||||
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
import 'package:flutter_displaymode/flutter_displaymode.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
import 'package:immich_mobile/extensions/build_context_extensions.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:immich_mobile/utils/download.dart';
|
||||||
import 'package:timezone/data/latest.dart';
|
import 'package:timezone/data/latest.dart';
|
||||||
import 'package:immich_mobile/constants/locales.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
|
// needs to be delayed so that EasyLocalization is working
|
||||||
ref.read(backgroundServiceProvider).resumeServiceIfEnabled();
|
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
|
@override
|
||||||
@@ -214,7 +238,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
|
|||||||
localizationsDelegates: context.localizationDelegates,
|
localizationsDelegates: context.localizationDelegates,
|
||||||
supportedLocales: context.supportedLocales,
|
supportedLocales: context.supportedLocales,
|
||||||
locale: context.locale,
|
locale: context.locale,
|
||||||
debugShowCheckedModeBanner: true,
|
debugShowCheckedModeBanner: false,
|
||||||
home: MaterialApp.router(
|
home: MaterialApp.router(
|
||||||
title: 'Immich',
|
title: 'Immich',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
|||||||
@@ -84,48 +84,34 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
_deleteInProgress = true;
|
_deleteInProgress = true;
|
||||||
state = true;
|
state = true;
|
||||||
try {
|
try {
|
||||||
// Filter the assets based on the backed-up status
|
|
||||||
final assets = onlyBackedUp
|
final assets = onlyBackedUp
|
||||||
? deleteAssets.where((e) => e.storage == AssetState.merged)
|
? deleteAssets.where((e) => e.storage == AssetState.merged)
|
||||||
: deleteAssets;
|
: deleteAssets;
|
||||||
|
|
||||||
if (assets.isEmpty) {
|
|
||||||
return false; // No assets to delete
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with local deletion of the filtered assets
|
|
||||||
final localDeleted = await _deleteLocalAssets(assets);
|
final localDeleted = await _deleteLocalAssets(assets);
|
||||||
|
|
||||||
if (localDeleted.isNotEmpty) {
|
if (localDeleted.isNotEmpty) {
|
||||||
final localOnlyIds = assets
|
final localOnlyIds = deleteAssets
|
||||||
.where((e) => e.storage == AssetState.local)
|
.where((e) => e.storage == AssetState.local)
|
||||||
.map((e) => e.id)
|
.map((e) => e.id)
|
||||||
.toList();
|
.toList();
|
||||||
|
// Update merged assets to remote only
|
||||||
// Update merged assets to remote-only
|
|
||||||
final mergedAssets =
|
final mergedAssets =
|
||||||
assets.where((e) => e.storage == AssetState.merged).map((e) {
|
deleteAssets.where((e) => e.storage == AssetState.merged).map((e) {
|
||||||
e.localId = null;
|
e.localId = null;
|
||||||
return e;
|
return e;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
// Update the local database
|
|
||||||
await _db.writeTxn(() async {
|
await _db.writeTxn(() async {
|
||||||
if (mergedAssets.isNotEmpty) {
|
if (mergedAssets.isNotEmpty) {
|
||||||
await _db.assets
|
await _db.assets.putAll(mergedAssets);
|
||||||
.putAll(mergedAssets); // Use the filtered merged assets
|
|
||||||
}
|
}
|
||||||
await _db.exifInfos.deleteAll(localOnlyIds);
|
await _db.exifInfos.deleteAll(localOnlyIds);
|
||||||
await _db.assets.deleteAll(localOnlyIds);
|
await _db.assets.deleteAll(localOnlyIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
_deleteInProgress = false;
|
_deleteInProgress = false;
|
||||||
state = false;
|
state = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -203,30 +203,18 @@ class MultiselectGrid extends HookConsumerWidget {
|
|||||||
void onDeleteLocal(bool onlyBackedUp) async {
|
void onDeleteLocal(bool onlyBackedUp) async {
|
||||||
processing.value = true;
|
processing.value = true;
|
||||||
try {
|
try {
|
||||||
// Select only the local assets from the selection
|
|
||||||
final localIds = selection.value.where((a) => a.isLocal).toList();
|
final localIds = selection.value.where((a) => a.isLocal).toList();
|
||||||
|
|
||||||
// Delete only the backed-up assets if 'onlyBackedUp' is true
|
|
||||||
final isDeleted = await ref
|
final isDeleted = await ref
|
||||||
.read(assetProvider.notifier)
|
.read(assetProvider.notifier)
|
||||||
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
|
.deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp);
|
||||||
|
|
||||||
if (isDeleted) {
|
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(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'assets_removed_permanently_from_device'
|
msg: 'assets_removed_permanently_from_device'
|
||||||
.tr(args: ["$deletedCount"]),
|
.tr(args: ["${localIds.length}"]),
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reset the selection
|
|
||||||
selectionEnabledHook.value = false;
|
selectionEnabledHook.value = false;
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+10
-1
@@ -23,6 +23,7 @@ class SystemConfigFFmpegDto {
|
|||||||
required this.crf,
|
required this.crf,
|
||||||
required this.gopSize,
|
required this.gopSize,
|
||||||
required this.maxBitrate,
|
required this.maxBitrate,
|
||||||
|
required this.npl,
|
||||||
required this.preferredHwDevice,
|
required this.preferredHwDevice,
|
||||||
required this.preset,
|
required this.preset,
|
||||||
required this.refs,
|
required this.refs,
|
||||||
@@ -61,6 +62,9 @@ class SystemConfigFFmpegDto {
|
|||||||
|
|
||||||
String maxBitrate;
|
String maxBitrate;
|
||||||
|
|
||||||
|
/// Minimum value: 0
|
||||||
|
int npl;
|
||||||
|
|
||||||
String preferredHwDevice;
|
String preferredHwDevice;
|
||||||
|
|
||||||
String preset;
|
String preset;
|
||||||
@@ -98,6 +102,7 @@ class SystemConfigFFmpegDto {
|
|||||||
other.crf == crf &&
|
other.crf == crf &&
|
||||||
other.gopSize == gopSize &&
|
other.gopSize == gopSize &&
|
||||||
other.maxBitrate == maxBitrate &&
|
other.maxBitrate == maxBitrate &&
|
||||||
|
other.npl == npl &&
|
||||||
other.preferredHwDevice == preferredHwDevice &&
|
other.preferredHwDevice == preferredHwDevice &&
|
||||||
other.preset == preset &&
|
other.preset == preset &&
|
||||||
other.refs == refs &&
|
other.refs == refs &&
|
||||||
@@ -123,6 +128,7 @@ class SystemConfigFFmpegDto {
|
|||||||
(crf.hashCode) +
|
(crf.hashCode) +
|
||||||
(gopSize.hashCode) +
|
(gopSize.hashCode) +
|
||||||
(maxBitrate.hashCode) +
|
(maxBitrate.hashCode) +
|
||||||
|
(npl.hashCode) +
|
||||||
(preferredHwDevice.hashCode) +
|
(preferredHwDevice.hashCode) +
|
||||||
(preset.hashCode) +
|
(preset.hashCode) +
|
||||||
(refs.hashCode) +
|
(refs.hashCode) +
|
||||||
@@ -136,7 +142,7 @@ class SystemConfigFFmpegDto {
|
|||||||
(twoPass.hashCode);
|
(twoPass.hashCode);
|
||||||
|
|
||||||
@override
|
@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() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@@ -150,6 +156,7 @@ class SystemConfigFFmpegDto {
|
|||||||
json[r'crf'] = this.crf;
|
json[r'crf'] = this.crf;
|
||||||
json[r'gopSize'] = this.gopSize;
|
json[r'gopSize'] = this.gopSize;
|
||||||
json[r'maxBitrate'] = this.maxBitrate;
|
json[r'maxBitrate'] = this.maxBitrate;
|
||||||
|
json[r'npl'] = this.npl;
|
||||||
json[r'preferredHwDevice'] = this.preferredHwDevice;
|
json[r'preferredHwDevice'] = this.preferredHwDevice;
|
||||||
json[r'preset'] = this.preset;
|
json[r'preset'] = this.preset;
|
||||||
json[r'refs'] = this.refs;
|
json[r'refs'] = this.refs;
|
||||||
@@ -183,6 +190,7 @@ class SystemConfigFFmpegDto {
|
|||||||
crf: mapValueOfType<int>(json, r'crf')!,
|
crf: mapValueOfType<int>(json, r'crf')!,
|
||||||
gopSize: mapValueOfType<int>(json, r'gopSize')!,
|
gopSize: mapValueOfType<int>(json, r'gopSize')!,
|
||||||
maxBitrate: mapValueOfType<String>(json, r'maxBitrate')!,
|
maxBitrate: mapValueOfType<String>(json, r'maxBitrate')!,
|
||||||
|
npl: mapValueOfType<int>(json, r'npl')!,
|
||||||
preferredHwDevice: mapValueOfType<String>(json, r'preferredHwDevice')!,
|
preferredHwDevice: mapValueOfType<String>(json, r'preferredHwDevice')!,
|
||||||
preset: mapValueOfType<String>(json, r'preset')!,
|
preset: mapValueOfType<String>(json, r'preset')!,
|
||||||
refs: mapValueOfType<int>(json, r'refs')!,
|
refs: mapValueOfType<int>(json, r'refs')!,
|
||||||
@@ -251,6 +259,7 @@ class SystemConfigFFmpegDto {
|
|||||||
'crf',
|
'crf',
|
||||||
'gopSize',
|
'gopSize',
|
||||||
'maxBitrate',
|
'maxBitrate',
|
||||||
|
'npl',
|
||||||
'preferredHwDevice',
|
'preferredHwDevice',
|
||||||
'preset',
|
'preset',
|
||||||
'refs',
|
'refs',
|
||||||
|
|||||||
+232
-229
File diff suppressed because it is too large
Load Diff
+4
-2
@@ -6,7 +6,7 @@ version: 1.119.1+164
|
|||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
flutter: 3.24.4
|
flutter: 3.24.3
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
@@ -42,7 +42,7 @@ dependencies:
|
|||||||
path_provider: ^2.1.2
|
path_provider: ^2.1.2
|
||||||
collection: ^1.18.0
|
collection: ^1.18.0
|
||||||
http_parser: ^4.0.2
|
http_parser: ^4.0.2
|
||||||
flutter_web_auth: ^0.6.0
|
flutter_web_auth: ^0.5.0
|
||||||
easy_image_viewer: ^1.4.0
|
easy_image_viewer: ^1.4.0
|
||||||
isar: ^3.1.0+1
|
isar: ^3.1.0+1
|
||||||
isar_flutter_libs: ^3.1.0+1
|
isar_flutter_libs: ^3.1.0+1
|
||||||
@@ -57,6 +57,8 @@ dependencies:
|
|||||||
async: ^2.11.0
|
async: ^2.11.0
|
||||||
dynamic_color: ^1.7.0 #package to apply system theme
|
dynamic_color: ^1.7.0 #package to apply system theme
|
||||||
background_downloader: ^8.5.5
|
background_downloader: ^8.5.5
|
||||||
|
share_handler: ^0.0.21
|
||||||
|
share_handler_ios:
|
||||||
|
|
||||||
#image editing packages
|
#image editing packages
|
||||||
crop_image: ^1.0.13
|
crop_image: ^1.0.13
|
||||||
|
|||||||
@@ -11621,6 +11621,10 @@
|
|||||||
"maxBitrate": {
|
"maxBitrate": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
|
"npl": {
|
||||||
|
"minimum": 0,
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"preferredHwDevice": {
|
"preferredHwDevice": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
@@ -11669,6 +11673,7 @@
|
|||||||
"crf",
|
"crf",
|
||||||
"gopSize",
|
"gopSize",
|
||||||
"maxBitrate",
|
"maxBitrate",
|
||||||
|
"npl",
|
||||||
"preferredHwDevice",
|
"preferredHwDevice",
|
||||||
"preset",
|
"preset",
|
||||||
"refs",
|
"refs",
|
||||||
|
|||||||
+14
-15
@@ -1,18 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.119.1",
|
"version": "1.111.0",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.119.1",
|
"version": "1.111.0",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^20.14.12",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -22,19 +22,19 @@
|
|||||||
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "20.14.12",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.8"
|
"undici-types": "~5.26.4"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.6.3",
|
"version": "5.5.4",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
|
||||||
"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
|
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -46,11 +46,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/undici-types": {
|
"node_modules/undici-types": {
|
||||||
"version": "6.19.8",
|
"version": "5.26.5",
|
||||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
|
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
|
"integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
|
||||||
"dev": true,
|
"dev": true
|
||||||
"license": "MIT"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
|
|||||||
@@ -1104,6 +1104,7 @@ export type SystemConfigFFmpegDto = {
|
|||||||
crf: number;
|
crf: number;
|
||||||
gopSize: number;
|
gopSize: number;
|
||||||
maxBitrate: string;
|
maxBitrate: string;
|
||||||
|
npl: number;
|
||||||
preferredHwDevice: string;
|
preferredHwDevice: string;
|
||||||
preset: string;
|
preset: string;
|
||||||
refs: number;
|
refs: number;
|
||||||
|
|||||||
+1
-1
@@ -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
|
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
||||||
|
|
||||||
# web build
|
# 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
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
Generated
+29
-29
@@ -83,7 +83,7 @@
|
|||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/picomatch": "^3.0.0",
|
"@types/picomatch": "^3.0.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
@@ -5110,9 +5110,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "22.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.19.8"
|
"undici-types": "~6.19.8"
|
||||||
}
|
}
|
||||||
@@ -8236,9 +8236,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored": {
|
"node_modules/exiftool-vendored": {
|
||||||
"version": "28.7.0",
|
"version": "28.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@photostructure/tz-lookup": "^11.0.0",
|
"@photostructure/tz-lookup": "^11.0.0",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
@@ -8247,23 +8247,23 @@
|
|||||||
"luxon": "^3.5.0"
|
"luxon": "^3.5.0"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"exiftool-vendored.exe": "12.99.0",
|
"exiftool-vendored.exe": "12.97.0",
|
||||||
"exiftool-vendored.pl": "12.99.0"
|
"exiftool-vendored.pl": "12.97.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored.exe": {
|
"node_modules/exiftool-vendored.exe": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"win32"
|
"win32"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"node_modules/exiftool-vendored.pl": {
|
"node_modules/exiftool-vendored.pl": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
"!win32"
|
"!win32"
|
||||||
@@ -18258,9 +18258,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@types/node": {
|
"@types/node": {
|
||||||
"version": "22.9.0",
|
"version": "22.8.5",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.8.5.tgz",
|
||||||
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
|
"integrity": "sha512-5iYk6AMPtsMbkZqCO1UGF9W5L38twq11S2pYWkybGHH2ogPUvXWNlQqJBzuEZWKj/WRH+QTeiv6ySWqJtvIEgA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"undici-types": "~6.19.8"
|
"undici-types": "~6.19.8"
|
||||||
}
|
}
|
||||||
@@ -20579,29 +20579,29 @@
|
|||||||
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
|
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="
|
||||||
},
|
},
|
||||||
"exiftool-vendored": {
|
"exiftool-vendored": {
|
||||||
"version": "28.7.0",
|
"version": "28.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.6.0.tgz",
|
||||||
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
|
"integrity": "sha512-Cx8/8ov1tKEacHhsi7FNYdisIhKq/SeQfprYSpYzwBuJwkPmCV8w7tTIvUJRQX9rvopXhBA4eBf1FPXqTZW5vA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@photostructure/tz-lookup": "^11.0.0",
|
"@photostructure/tz-lookup": "^11.0.0",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"batch-cluster": "^13.0.0",
|
"batch-cluster": "^13.0.0",
|
||||||
"exiftool-vendored.exe": "12.99.0",
|
"exiftool-vendored.exe": "12.97.0",
|
||||||
"exiftool-vendored.pl": "12.99.0",
|
"exiftool-vendored.pl": "12.97.0",
|
||||||
"he": "^1.2.0",
|
"he": "^1.2.0",
|
||||||
"luxon": "^3.5.0"
|
"luxon": "^3.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"exiftool-vendored.exe": {
|
"exiftool-vendored.exe": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.97.0.tgz",
|
||||||
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
|
"integrity": "sha512-+HxyFigEJOtwRjP7PhEslhZKuVW2V0hvmHPHtbVtNKGfAUGcfc95xNTjASQfKJvc+9ZuvzdEBPkEQmyA/ZYdIw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"exiftool-vendored.pl": {
|
"exiftool-vendored.pl": {
|
||||||
"version": "12.99.0",
|
"version": "12.97.0",
|
||||||
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
|
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.97.0.tgz",
|
||||||
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
|
"integrity": "sha512-mXe9JEH3csfyPWcC7+H6IpfaokDMMr4S45n7MtiobGPdeeh+kFnf1SQ9cxg4sF403P6IKVeYYPbzgKMlpro9eQ==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
"express": {
|
"express": {
|
||||||
|
|||||||
+1
-1
@@ -108,7 +108,7 @@
|
|||||||
"@types/lodash": "^4.14.197",
|
"@types/lodash": "^4.14.197",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^22.8.5",
|
"@types/node": "^22.8.1",
|
||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/picomatch": "^3.0.0",
|
"@types/picomatch": "^3.0.0",
|
||||||
"@types/pngjs": "^6.0.5",
|
"@types/pngjs": "^6.0.5",
|
||||||
|
|||||||
+19
-15
@@ -6,12 +6,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
|||||||
import { ClsModule } from 'nestjs-cls';
|
import { ClsModule } from 'nestjs-cls';
|
||||||
import { OpenTelemetryModule } from 'nestjs-otel';
|
import { OpenTelemetryModule } from 'nestjs-otel';
|
||||||
import { commands } from 'src/commands';
|
import { commands } from 'src/commands';
|
||||||
import { IWorker } from 'src/constants';
|
|
||||||
import { controllers } from 'src/controllers';
|
import { controllers } from 'src/controllers';
|
||||||
import { entities } from 'src/entities';
|
import { entities } from 'src/entities';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
import { IEventRepository } from 'src/interfaces/event.interface';
|
||||||
import { IJobRepository } from 'src/interfaces/job.interface';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
||||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||||
@@ -58,25 +56,23 @@ const imports = [
|
|||||||
TypeOrmModule.forFeature(entities),
|
TypeOrmModule.forFeature(entities),
|
||||||
];
|
];
|
||||||
|
|
||||||
class BaseModule implements OnModuleInit, OnModuleDestroy {
|
abstract class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||||
|
private get worker() {
|
||||||
|
return this.getWorker();
|
||||||
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IWorker) private worker: ImmichWorker,
|
|
||||||
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
@Inject(ILoggerRepository) logger: ILoggerRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
|
||||||
@Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
|
@Inject(ITelemetryRepository) private telemetryRepository: ITelemetryRepository,
|
||||||
) {
|
) {
|
||||||
logger.setAppName(this.worker);
|
logger.setAppName(this.worker);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
abstract getWorker(): ImmichWorker;
|
||||||
|
|
||||||
async onModuleInit() {
|
async onModuleInit() {
|
||||||
this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) });
|
this.telemetryRepository.setup({ repositories: repositories.map(({ useClass }) => useClass) });
|
||||||
|
|
||||||
this.jobRepository.setup({ services });
|
|
||||||
if (this.worker === ImmichWorker.MICROSERVICES) {
|
|
||||||
this.jobRepository.startWorkers();
|
|
||||||
}
|
|
||||||
|
|
||||||
this.eventRepository.setup({ services });
|
this.eventRepository.setup({ services });
|
||||||
await this.eventRepository.emit('app.bootstrap', this.worker);
|
await this.eventRepository.emit('app.bootstrap', this.worker);
|
||||||
}
|
}
|
||||||
@@ -90,15 +86,23 @@ class BaseModule implements OnModuleInit, OnModuleDestroy {
|
|||||||
@Module({
|
@Module({
|
||||||
imports: [...imports, ScheduleModule.forRoot()],
|
imports: [...imports, ScheduleModule.forRoot()],
|
||||||
controllers: [...controllers],
|
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({
|
@Module({
|
||||||
imports: [...imports],
|
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({
|
@Module({
|
||||||
imports: [...imports],
|
imports: [...imports],
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ImmichWorker } from 'src/enum';
|
|||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
const { host, workers, port } = new ConfigRepository().getEnv();
|
const { workers, port } = new ConfigRepository().getEnv();
|
||||||
if (!workers.includes(ImmichWorker.API)) {
|
if (!workers.includes(ImmichWorker.API)) {
|
||||||
process.exit();
|
process.exit();
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ const main = async () => {
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||||
try {
|
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,
|
signal: controller.signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ export interface SystemConfig {
|
|||||||
bframes: number;
|
bframes: number;
|
||||||
refs: number;
|
refs: number;
|
||||||
gopSize: number;
|
gopSize: number;
|
||||||
|
npl: number;
|
||||||
temporalAQ: boolean;
|
temporalAQ: boolean;
|
||||||
cqMode: CQMode;
|
cqMode: CQMode;
|
||||||
twoPass: boolean;
|
twoPass: boolean;
|
||||||
@@ -177,6 +178,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
bframes: -1,
|
bframes: -1,
|
||||||
refs: 0,
|
refs: 0,
|
||||||
gopSize: 0,
|
gopSize: 0,
|
||||||
|
npl: 0,
|
||||||
temporalAQ: false,
|
temporalAQ: false,
|
||||||
cqMode: CQMode.AUTO,
|
cqMode: CQMode.AUTO,
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
|
|||||||
|
|
||||||
export const SALT_ROUNDS = 10;
|
export const SALT_ROUNDS = 10;
|
||||||
|
|
||||||
export const IWorker = 'IWorker';
|
|
||||||
|
|
||||||
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||||
export const serverVersion = new SemVer(version);
|
export const serverVersion = new SemVer(version);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import _ from 'lodash';
|
|||||||
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants';
|
||||||
import { MetadataKey } from 'src/enum';
|
import { MetadataKey } from 'src/enum';
|
||||||
import { EmitEvent } from 'src/interfaces/event.interface';
|
import { EmitEvent } from 'src/interfaces/event.interface';
|
||||||
import { JobName, QueueName } from 'src/interfaces/job.interface';
|
|
||||||
import { setUnion } from 'src/utils/set';
|
import { setUnion } from 'src/utils/set';
|
||||||
|
|
||||||
// PostgreSQL uses a 16-bit integer to indicate the number of bound parameters. This means that the
|
// 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 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 LifecycleRelease = 'NEXT_RELEASE' | string;
|
||||||
type LifecycleMetadata = {
|
type LifecycleMetadata = {
|
||||||
addedAt?: LifecycleRelease;
|
addedAt?: LifecycleRelease;
|
||||||
|
|||||||
@@ -134,6 +134,12 @@ export class SystemConfigFFmpegDto {
|
|||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
gopSize!: number;
|
gopSize!: number;
|
||||||
|
|
||||||
|
@IsInt()
|
||||||
|
@Min(0)
|
||||||
|
@Type(() => Number)
|
||||||
|
@ApiProperty({ type: 'integer' })
|
||||||
|
npl!: number;
|
||||||
|
|
||||||
@ValidateBoolean()
|
@ValidateBoolean()
|
||||||
temporalAQ!: boolean;
|
temporalAQ!: boolean;
|
||||||
|
|
||||||
|
|||||||
@@ -335,7 +335,6 @@ export enum MetadataKey {
|
|||||||
SHARED_ROUTE = 'shared_route',
|
SHARED_ROUTE = 'shared_route',
|
||||||
API_KEY_SECURITY = 'api_key',
|
API_KEY_SECURITY = 'api_key',
|
||||||
EVENT_CONFIG = 'event_config',
|
EVENT_CONFIG = 'event_config',
|
||||||
JOB_CONFIG = 'job_config',
|
|
||||||
TELEMETRY_ENABLED = 'telemetry_enabled',
|
TELEMETRY_ENABLED = 'telemetry_enabled',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { SystemConfig } from 'src/config';
|
|||||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { JobItem, QueueName } from 'src/interfaces/job.interface';
|
|
||||||
|
|
||||||
export const IEventRepository = 'IEventRepository';
|
export const IEventRepository = 'IEventRepository';
|
||||||
|
|
||||||
@@ -39,8 +38,6 @@ type EventMap = {
|
|||||||
'assets.delete': [{ assetIds: string[]; userId: string }];
|
'assets.delete': [{ assetIds: string[]; userId: string }];
|
||||||
'assets.restore': [{ assetIds: string[]; userId: string }];
|
'assets.restore': [{ assetIds: string[]; userId: string }];
|
||||||
|
|
||||||
'job.start': [QueueName, JobItem];
|
|
||||||
|
|
||||||
// session events
|
// session events
|
||||||
'session.delete': [{ sessionId: string }];
|
'session.delete': [{ sessionId: string }];
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { ClassConstructor } from 'class-transformer';
|
|
||||||
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
|
import { EmailImageAttachment } from 'src/interfaces/notification.interface';
|
||||||
|
|
||||||
export enum QueueName {
|
export enum QueueName {
|
||||||
@@ -239,8 +238,8 @@ export type JobItem =
|
|||||||
|
|
||||||
// Migration
|
// Migration
|
||||||
| { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
|
| { name: JobName.QUEUE_MIGRATION; data?: IBaseJob }
|
||||||
| { name: JobName.MIGRATE_ASSET; data: IEntityJob }
|
| { name: JobName.MIGRATE_ASSET; data?: IEntityJob }
|
||||||
| { name: JobName.MIGRATE_PERSON; data: IEntityJob }
|
| { name: JobName.MIGRATE_PERSON; data?: IEntityJob }
|
||||||
|
|
||||||
// Metadata Extraction
|
// Metadata Extraction
|
||||||
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
| { name: JobName.QUEUE_METADATA_EXTRACTION; data: IBaseJob }
|
||||||
@@ -287,7 +286,7 @@ export type JobItem =
|
|||||||
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
|
| { name: JobName.LIBRARY_SYNC_FILE; data: ILibraryFileJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
|
| { name: JobName.LIBRARY_QUEUE_SYNC_FILES; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ASSETS; 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_DELETE; data: IEntityJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_SYNC_ALL; data?: IBaseJob }
|
||||||
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
| { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob }
|
||||||
@@ -306,15 +305,14 @@ export enum JobStatus {
|
|||||||
FAILED = 'failed',
|
FAILED = 'failed',
|
||||||
SKIPPED = 'skipped',
|
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 const IJobRepository = 'IJobRepository';
|
||||||
|
|
||||||
export interface IJobRepository {
|
export interface IJobRepository {
|
||||||
setup(options: { services: ClassConstructor<unknown>[] }): void;
|
addHandler(queueName: QueueName, concurrency: number, handler: JobItemHandler): void;
|
||||||
startWorkers(): void;
|
|
||||||
run(job: JobItem): Promise<JobStatus>;
|
|
||||||
addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void;
|
addCronJob(name: string, expression: string, onTick: () => void, start?: boolean): void;
|
||||||
updateCronJob(name: string, expression?: string, start?: boolean): void;
|
updateCronJob(name: string, expression?: string, start?: boolean): void;
|
||||||
setConcurrency(queueName: QueueName, concurrency: number): void;
|
setConcurrency(queueName: QueueName, concurrency: number): void;
|
||||||
|
|||||||
@@ -59,7 +59,6 @@ export interface VideoStreamInfo {
|
|||||||
frameCount: number;
|
frameCount: number;
|
||||||
isHDR: boolean;
|
isHDR: boolean;
|
||||||
bitrate: number;
|
bitrate: number;
|
||||||
pixelFormat: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AudioStreamInfo {
|
export interface AudioStreamInfo {
|
||||||
|
|||||||
@@ -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> {}
|
|
||||||
}
|
|
||||||
@@ -1,122 +1,124 @@
|
|||||||
import { getQueueToken } from '@nestjs/bullmq';
|
import { getQueueToken } from '@nestjs/bullmq';
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { ModuleRef, Reflector } from '@nestjs/core';
|
import { ModuleRef } from '@nestjs/core';
|
||||||
import { SchedulerRegistry } from '@nestjs/schedule';
|
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||||
import { JobsOptions, Queue, Worker } from 'bullmq';
|
import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
|
||||||
import { ClassConstructor } from 'class-transformer';
|
|
||||||
import { CronJob, CronTime } from 'cron';
|
import { CronJob, CronTime } from 'cron';
|
||||||
import { setTimeout } from 'node:timers/promises';
|
import { setTimeout } from 'node:timers/promises';
|
||||||
import { JobConfig } from 'src/decorators';
|
|
||||||
import { MetadataKey } from 'src/enum';
|
|
||||||
import { IConfigRepository } from 'src/interfaces/config.interface';
|
import { IConfigRepository } from 'src/interfaces/config.interface';
|
||||||
import { IEventRepository } from 'src/interfaces/event.interface';
|
|
||||||
import {
|
import {
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
IJobRepository,
|
IJobRepository,
|
||||||
JobCounts,
|
JobCounts,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobOf,
|
|
||||||
JobStatus,
|
|
||||||
QueueCleanType,
|
QueueCleanType,
|
||||||
QueueName,
|
QueueName,
|
||||||
QueueStatus,
|
QueueStatus,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { getKeyByValue, getMethodNames, ImmichStartupError } from 'src/utils/misc';
|
|
||||||
|
|
||||||
type JobMapItem = {
|
export const JOBS_TO_QUEUE: Record<JobName, QueueName> = {
|
||||||
jobName: JobName;
|
// misc
|
||||||
queueName: QueueName;
|
[JobName.ASSET_DELETION]: QueueName.BACKGROUND_TASK,
|
||||||
handler: (job: JobOf<any>) => Promise<JobStatus>;
|
[JobName.ASSET_DELETION_CHECK]: QueueName.BACKGROUND_TASK,
|
||||||
label: string;
|
[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()
|
@Injectable()
|
||||||
export class JobRepository implements IJobRepository {
|
export class JobRepository implements IJobRepository {
|
||||||
private workers: Partial<Record<QueueName, Worker>> = {};
|
private workers: Partial<Record<QueueName, Worker>> = {};
|
||||||
private handlers: Partial<Record<JobName, JobMapItem>> = {};
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private moduleRef: ModuleRef,
|
private moduleReference: ModuleRef,
|
||||||
private schedulerRegistry: SchedulerRegistry,
|
private schedulerReqistry: SchedulerRegistry,
|
||||||
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
@Inject(IConfigRepository) private configRepository: IConfigRepository,
|
||||||
@Inject(IEventRepository) private eventRepository: IEventRepository,
|
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
) {
|
) {
|
||||||
this.logger.setContext(JobRepository.name);
|
this.logger.setContext(JobRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
setup({ services }: { services: ClassConstructor<unknown>[] }) {
|
addHandler(queueName: QueueName, concurrency: number, handler: (item: JobItem) => Promise<void>) {
|
||||||
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() {
|
|
||||||
const { bull } = this.configRepository.getEnv();
|
const { bull } = this.configRepository.getEnv();
|
||||||
for (const queueName of Object.values(QueueName)) {
|
const workerHandler: Processor = async (job: Job) => handler(job as JobItem);
|
||||||
this.logger.debug(`Starting worker for queue: ${queueName}`);
|
const workerOptions: WorkerOptions = { ...bull.config, concurrency };
|
||||||
this.workers[queueName] = new Worker(
|
this.workers[queueName] = new Worker(queueName, workerHandler, workerOptions);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
addCronJob(name: string, expression: string, onTick: () => void, start = true): void {
|
addCronJob(name: string, expression: string, onTick: () => void, start = true): void {
|
||||||
@@ -139,11 +141,11 @@ export class JobRepository implements IJobRepository {
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.schedulerRegistry.addCronJob(name, job);
|
this.schedulerReqistry.addCronJob(name, job);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateCronJob(name: string, expression?: string, start?: boolean): void {
|
updateCronJob(name: string, expression?: string, start?: boolean): void {
|
||||||
const job = this.schedulerRegistry.getCronJob(name);
|
const job = this.schedulerReqistry.getCronJob(name);
|
||||||
if (expression) {
|
if (expression) {
|
||||||
job.setTime(new CronTime(expression));
|
job.setTime(new CronTime(expression));
|
||||||
}
|
}
|
||||||
@@ -202,10 +204,6 @@ export class JobRepository implements IJobRepository {
|
|||||||
) as unknown as Promise<JobCounts>;
|
) as unknown as Promise<JobCounts>;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getQueueName(name: JobName) {
|
|
||||||
return (this.handlers[name] as JobMapItem).queueName;
|
|
||||||
}
|
|
||||||
|
|
||||||
async queueAll(items: JobItem[]): Promise<void> {
|
async queueAll(items: JobItem[]): Promise<void> {
|
||||||
if (items.length === 0) {
|
if (items.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -214,7 +212,7 @@ export class JobRepository implements IJobRepository {
|
|||||||
const promises = [];
|
const promises = [];
|
||||||
const itemsByQueue = {} as Record<string, (JobItem & { data: any; options: JobsOptions | undefined })[]>;
|
const itemsByQueue = {} as Record<string, (JobItem & { data: any; options: JobsOptions | undefined })[]>;
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
const queueName = this.getQueueName(item.name);
|
const queueName = JOBS_TO_QUEUE[item.name];
|
||||||
const job = {
|
const job = {
|
||||||
name: item.name,
|
name: item.name,
|
||||||
data: item.data || {},
|
data: item.data || {},
|
||||||
@@ -275,11 +273,11 @@ export class JobRepository implements IJobRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getQueue(queue: QueueName): Queue {
|
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> {
|
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) {
|
if (!existingJob) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,6 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
rotation: this.parseInt(stream.rotation),
|
rotation: this.parseInt(stream.rotation),
|
||||||
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
|
isHDR: stream.color_transfer === 'smpte2084' || stream.color_transfer === 'arib-std-b67',
|
||||||
bitrate: this.parseInt(stream.bit_rate),
|
bitrate: this.parseInt(stream.bit_rate),
|
||||||
pixelFormat: stream.pix_fmt || 'yuv420p',
|
|
||||||
})),
|
})),
|
||||||
audioStreams: results.streams
|
audioStreams: results.streams
|
||||||
.filter((stream) => stream.codec_type === 'audio')
|
.filter((stream) => stream.codec_type === 'audio')
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { BadRequestException } from '@nestjs/common';
|
import { BadRequestException } from '@nestjs/common';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DateTime, Duration } from 'luxon';
|
import { DateTime, Duration } from 'luxon';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import {
|
import {
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
MemoryLaneResponseDto,
|
MemoryLaneResponseDto,
|
||||||
@@ -22,13 +21,12 @@ import { MemoryLaneDto } from 'src/dtos/search.dto';
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetStatus, Permission } from 'src/enum';
|
import { AssetStatus, Permission } from 'src/enum';
|
||||||
import {
|
import {
|
||||||
|
IAssetDeleteJob,
|
||||||
ISidecarWriteJob,
|
ISidecarWriteJob,
|
||||||
JOBS_ASSET_PAGINATION_SIZE,
|
JOBS_ASSET_PAGINATION_SIZE,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobOf,
|
|
||||||
JobStatus,
|
JobStatus,
|
||||||
QueueName,
|
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles, getMyPartnerIds, onAfterUnlink, onBeforeLink, onBeforeUnlink } from 'src/utils/asset.util';
|
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);
|
await this.assetRepository.updateAll(ids, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.ASSET_DELETION_CHECK, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
||||||
@@ -214,8 +211,7 @@ export class AssetService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.ASSET_DELETION, queue: QueueName.BACKGROUND_TASK })
|
async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
|
||||||
async handleAssetDeletion(job: JobOf<JobName.ASSET_DELETION>): Promise<JobStatus> {
|
|
||||||
const { id, deleteOnDisk } = job;
|
const { id, deleteOnDisk } = job;
|
||||||
|
|
||||||
const asset = await this.assetRepository.getById(id, {
|
const asset = await this.assetRepository.getById(id, {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { DateTime } from 'luxon';
|
|||||||
import { resolve } from 'node:path';
|
import { resolve } from 'node:path';
|
||||||
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
import { AUDIT_LOG_MAX_DURATION } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import {
|
import {
|
||||||
AuditDeletesDto,
|
AuditDeletesDto,
|
||||||
AuditDeletesResponseDto,
|
AuditDeletesResponseDto,
|
||||||
@@ -22,14 +21,13 @@ import {
|
|||||||
StorageFolder,
|
StorageFolder,
|
||||||
UserPathType,
|
UserPathType,
|
||||||
} from 'src/enum';
|
} 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 { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuditService extends BaseService {
|
export class AuditService extends BaseService {
|
||||||
@OnJob({ name: JobName.CLEAN_OLD_AUDIT_LOGS, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleCleanup(): Promise<JobStatus> {
|
async handleCleanup(): Promise<JobStatus> {
|
||||||
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
await this.auditRepository.removeBefore(DateTime.now().minus(AUDIT_LOG_MAX_DURATION).toJSDate());
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { default as path } from 'node:path';
|
import { default as path } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 { ImmichWorker, StorageFolder } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
import { validateCronExpression } from 'src/validation';
|
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`);
|
this.logger.debug(`Database Backup Cleanup Finished, deleted ${toDelete.length} backups`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.BACKUP_DATABASE, queue: QueueName.BACKUP_DATABASE })
|
|
||||||
async handleBackupDatabase(): Promise<JobStatus> {
|
async handleBackupDatabase(): Promise<JobStatus> {
|
||||||
this.logger.debug(`Database Backup Started`);
|
this.logger.debug(`Database Backup Started`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { BadRequestException, Inject, Optional } from '@nestjs/common';
|
import { BadRequestException, Inject } from '@nestjs/common';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { SystemConfig } from 'src/config';
|
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 { StorageCore } from 'src/cores/storage.core';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { ImmichWorker } from 'src/enum';
|
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
import { IActivityRepository } from 'src/interfaces/activity.interface';
|
||||||
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
import { IAlbumUserRepository } from 'src/interfaces/album-user.interface';
|
||||||
@@ -50,7 +49,6 @@ export class BaseService {
|
|||||||
protected storageCore: StorageCore;
|
protected storageCore: StorageCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IWorker) @Optional() protected worker: ImmichWorker | undefined,
|
|
||||||
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
@Inject(ILoggerRepository) protected logger: ILoggerRepository,
|
||||||
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
|
@Inject(IAccessRepository) protected accessRepository: IAccessRepository,
|
||||||
@Inject(IActivityRepository) protected activityRepository: IActivityRepository,
|
@Inject(IActivityRepository) protected activityRepository: IActivityRepository,
|
||||||
|
|||||||
@@ -31,22 +31,10 @@ describe(SearchService.name, () => {
|
|||||||
|
|
||||||
describe('getDuplicates', () => {
|
describe('getDuplicates', () => {
|
||||||
it('should get duplicates', async () => {
|
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]);
|
assetMock.getDuplicates.mockResolvedValue([assetStub.hasDupe]);
|
||||||
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([]);
|
await expect(sut.getDuplicates(authStub.admin)).resolves.toEqual([
|
||||||
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.hasDupe.id], { duplicateId: null });
|
{ duplicateId: assetStub.hasDupe.duplicateId, assets: [expect.objectContaining({ id: assetStub.hasDupe.id })] },
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
import { DuplicateResponseDto, mapDuplicateResponse } from 'src/dtos/duplicate.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
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 { AssetDuplicateResult } from 'src/interfaces/search.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
@@ -16,28 +15,11 @@ import { usePagination } from 'src/utils/pagination';
|
|||||||
export class DuplicateService extends BaseService {
|
export class DuplicateService extends BaseService {
|
||||||
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
async getDuplicates(auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||||
const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] });
|
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(
|
return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true })));
|
||||||
(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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
|
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueSearchDuplicates({ force }: JobOf<JobName.QUEUE_DUPLICATE_DETECTION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -58,8 +40,7 @@ export class DuplicateService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.DUPLICATE_DETECTION, queue: QueueName.DUPLICATE_DETECTION })
|
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleSearchDuplicates({ id }: JobOf<JobName.DUPLICATE_DETECTION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { MapService } from 'src/services/map.service';
|
|||||||
import { MediaService } from 'src/services/media.service';
|
import { MediaService } from 'src/services/media.service';
|
||||||
import { MemoryService } from 'src/services/memory.service';
|
import { MemoryService } from 'src/services/memory.service';
|
||||||
import { MetadataService } from 'src/services/metadata.service';
|
import { MetadataService } from 'src/services/metadata.service';
|
||||||
|
import { MicroservicesService } from 'src/services/microservices.service';
|
||||||
import { NotificationService } from 'src/services/notification.service';
|
import { NotificationService } from 'src/services/notification.service';
|
||||||
import { PartnerService } from 'src/services/partner.service';
|
import { PartnerService } from 'src/services/partner.service';
|
||||||
import { PersonService } from 'src/services/person.service';
|
import { PersonService } from 'src/services/person.service';
|
||||||
@@ -59,6 +60,7 @@ export const services = [
|
|||||||
MediaService,
|
MediaService,
|
||||||
MemoryService,
|
MemoryService,
|
||||||
MetadataService,
|
MetadataService,
|
||||||
|
MicroservicesService,
|
||||||
NotificationService,
|
NotificationService,
|
||||||
PartnerService,
|
PartnerService,
|
||||||
PersonService,
|
PersonService,
|
||||||
|
|||||||
@@ -2,25 +2,37 @@ import { BadRequestException } from '@nestjs/common';
|
|||||||
import { defaults } from 'src/config';
|
import { defaults } from 'src/config';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||||
import { IJobRepository, JobCommand, JobItem, JobName, JobStatus, QueueName } from 'src/interfaces/job.interface';
|
import {
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
IJobRepository,
|
||||||
import { ITelemetryRepository } from 'src/interfaces/telemetry.interface';
|
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 { JobService } from 'src/services/job.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { newTestService } from 'test/utils';
|
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, () => {
|
describe(JobService.name, () => {
|
||||||
let sut: JobService;
|
let sut: JobService;
|
||||||
let assetMock: Mocked<IAssetRepository>;
|
let assetMock: Mocked<IAssetRepository>;
|
||||||
let jobMock: Mocked<IJobRepository>;
|
let jobMock: Mocked<IJobRepository>;
|
||||||
let loggerMock: Mocked<ILoggerRepository>;
|
let systemMock: Mocked<ISystemMetadataRepository>;
|
||||||
let telemetryMock: Mocked<ITelemetryRepository>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
({ sut, assetMock, jobMock, loggerMock, telemetryMock } = newTestService(JobService, {
|
({ sut, assetMock, jobMock, systemMock } = newTestService(JobService));
|
||||||
worker: ImmichWorker.MICROSERVICES,
|
|
||||||
}));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work', () => {
|
it('should work', () => {
|
||||||
@@ -29,6 +41,7 @@ describe(JobService.name, () => {
|
|||||||
|
|
||||||
describe('onConfigUpdate', () => {
|
describe('onConfigUpdate', () => {
|
||||||
it('should update concurrency', () => {
|
it('should update concurrency', () => {
|
||||||
|
sut.onBootstrap(ImmichWorker.MICROSERVICES);
|
||||||
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
|
sut.onConfigUpdate({ oldConfig: defaults, newConfig: defaults });
|
||||||
|
|
||||||
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
|
expect(jobMock.setConcurrency).toHaveBeenCalledTimes(15);
|
||||||
@@ -212,19 +225,11 @@ describe(JobService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('onJobStart', () => {
|
describe('init', () => {
|
||||||
it('should process a successful job', async () => {
|
it('should register a handler for each queue', async () => {
|
||||||
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
|
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
|
||||||
|
expect(systemMock.get).toHaveBeenCalled();
|
||||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, {
|
expect(jobMock.addHandler).toHaveBeenCalledTimes(Object.keys(QueueName).length);
|
||||||
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();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const tests: Array<{ item: JobItem; jobs: JobName[] }> = [
|
const tests: Array<{ item: JobItem; jobs: JobName[] }> = [
|
||||||
@@ -292,9 +297,8 @@ describe(JobService.name, () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
jobMock.run.mockResolvedValue(JobStatus.SUCCESS);
|
await sut.init(makeMockHandlers(JobStatus.SUCCESS));
|
||||||
|
await jobMock.addHandler.mock.calls[0][2](item);
|
||||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
|
|
||||||
|
|
||||||
if (jobs.length > 1) {
|
if (jobs.length > 1) {
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith(
|
expect(jobMock.queueAll).toHaveBeenCalledWith(
|
||||||
@@ -309,9 +313,8 @@ describe(JobService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it(`should not queue any jobs when ${item.name} fails`, async () => {
|
it(`should not queue any jobs when ${item.name} fails`, async () => {
|
||||||
jobMock.run.mockResolvedValue(JobStatus.FAILED);
|
await sut.init(makeMockHandlers(JobStatus.FAILED));
|
||||||
|
await jobMock.addHandler.mock.calls[0][2](item);
|
||||||
await sut.onJobStart(QueueName.BACKGROUND_TASK, item);
|
|
||||||
|
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ import { OnEvent } from 'src/decorators';
|
|||||||
import { mapAsset } from 'src/dtos/asset-response.dto';
|
import { mapAsset } from 'src/dtos/asset-response.dto';
|
||||||
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
import { AllJobStatusResponseDto, JobCommandDto, JobCreateDto, JobStatusDto } from 'src/dtos/job.dto';
|
||||||
import { AssetType, ImmichWorker, ManualJobName } from 'src/enum';
|
import { AssetType, ImmichWorker, ManualJobName } from 'src/enum';
|
||||||
import { ArgOf, ArgsOf } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
ConcurrentQueueName,
|
ConcurrentQueueName,
|
||||||
JobCommand,
|
JobCommand,
|
||||||
|
JobHandler,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobStatus,
|
JobStatus,
|
||||||
@@ -38,9 +39,16 @@ const asJobItem = (dto: JobCreateDto): JobItem => {
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class JobService extends BaseService {
|
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 })
|
@OnEvent({ name: 'config.update', server: true })
|
||||||
onConfigUpdate({ newConfig: config }: ArgOf<'config.update'>) {
|
onConfigUpdate({ newConfig: config, oldConfig }: ArgOf<'config.update'>) {
|
||||||
if (this.worker !== ImmichWorker.MICROSERVICES) {
|
if (!oldConfig || !this.isMicroservices) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -169,21 +177,41 @@ export class JobService extends BaseService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnEvent({ name: 'job.start' })
|
async init(jobHandlers: Record<JobName, JobHandler>) {
|
||||||
async onJobStart(...[queueName, job]: ArgsOf<'job.start'>) {
|
const config = await this.getConfig({ withCache: false });
|
||||||
const queueMetric = `immich.queues.${snakeCase(queueName)}.active`;
|
for (const queueName of Object.values(QueueName)) {
|
||||||
this.telemetryRepository.jobs.addToGauge(queueMetric, 1);
|
let concurrency = 1;
|
||||||
try {
|
|
||||||
const status = await this.jobRepository.run(job);
|
if (this.isConcurrentQueue(queueName)) {
|
||||||
const jobMetric = `immich.jobs.${job.name.replaceAll('-', '_')}.${status}`;
|
concurrency = config.job[queueName].concurrency;
|
||||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
|
||||||
if (status === JobStatus.SUCCESS || status == JobStatus.SKIPPED) {
|
|
||||||
await this.onDone(job);
|
|
||||||
}
|
}
|
||||||
} catch (error: Error | any) {
|
|
||||||
this.logger.error(`Unable to run job handler (${queueName}/${job.name}): ${error}`, error?.stack, job.data);
|
this.logger.debug(`Registering ${queueName} with a concurrency of ${concurrency}`);
|
||||||
} finally {
|
this.jobRepository.addHandler(queueName, concurrency, async (item: JobItem): Promise<void> => {
|
||||||
this.telemetryRepository.jobs.addToGauge(queueMetric, -1);
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { R_OK } from 'node:constants';
|
|||||||
import path, { basename, isAbsolute, parse } from 'node:path';
|
import path, { basename, isAbsolute, parse } from 'node:path';
|
||||||
import picomatch from 'picomatch';
|
import picomatch from 'picomatch';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import {
|
import {
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
LibraryResponseDto,
|
LibraryResponseDto,
|
||||||
@@ -19,7 +19,14 @@ import { LibraryEntity } from 'src/entities/library.entity';
|
|||||||
import { AssetType, ImmichWorker } from 'src/enum';
|
import { AssetType, ImmichWorker } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { handlePromiseError } from 'src/utils/misc';
|
import { handlePromiseError } from 'src/utils/misc';
|
||||||
@@ -216,7 +223,6 @@ export class LibraryService extends BaseService {
|
|||||||
return libraries.map((library) => mapLibrary(library));
|
return libraries.map((library) => mapLibrary(library));
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_QUEUE_CLEANUP, queue: QueueName.LIBRARY })
|
|
||||||
async handleQueueCleanup(): Promise<JobStatus> {
|
async handleQueueCleanup(): Promise<JobStatus> {
|
||||||
this.logger.debug('Cleaning up any pending library deletions');
|
this.logger.debug('Cleaning up any pending library deletions');
|
||||||
const pendingDeletion = await this.libraryRepository.getAllDeleted();
|
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 } });
|
await this.jobRepository.queue({ name: JobName.LIBRARY_DELETE, data: { id } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_DELETE, queue: QueueName.LIBRARY })
|
async handleDeleteLibrary(job: IEntityJob): Promise<JobStatus> {
|
||||||
async handleDeleteLibrary(job: JobOf<JobName.LIBRARY_DELETE>): Promise<JobStatus> {
|
|
||||||
const libraryId = job.id;
|
const libraryId = job.id;
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) =>
|
||||||
@@ -369,8 +374,7 @@ export class LibraryService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_SYNC_FILE, queue: QueueName.LIBRARY })
|
async handleSyncFile(job: ILibraryFileJob): Promise<JobStatus> {
|
||||||
async handleSyncFile(job: JobOf<JobName.LIBRARY_SYNC_FILE>): Promise<JobStatus> {
|
|
||||||
// Only needs to handle new assets
|
// Only needs to handle new assets
|
||||||
const assetPath = path.normalize(job.assetPath);
|
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 } });
|
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> {
|
async handleQueueSyncAll(): Promise<JobStatus> {
|
||||||
this.logger.debug(`Refreshing all external libraries`);
|
this.logger.debug(`Refreshing all external libraries`);
|
||||||
|
|
||||||
@@ -480,8 +483,7 @@ export class LibraryService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_SYNC_ASSET, queue: QueueName.LIBRARY })
|
async handleSyncAsset(job: ILibraryAssetJob): Promise<JobStatus> {
|
||||||
async handleSyncAsset(job: JobOf<JobName.LIBRARY_SYNC_ASSET>): Promise<JobStatus> {
|
|
||||||
const asset = await this.assetRepository.getById(job.id);
|
const asset = await this.assetRepository.getById(job.id);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -536,8 +538,7 @@ export class LibraryService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_FILES, queue: QueueName.LIBRARY })
|
async handleQueueSyncFiles(job: IEntityJob): Promise<JobStatus> {
|
||||||
async handleQueueSyncFiles(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_FILES>): Promise<JobStatus> {
|
|
||||||
const library = await this.libraryRepository.get(job.id);
|
const library = await this.libraryRepository.get(job.id);
|
||||||
if (!library) {
|
if (!library) {
|
||||||
this.logger.debug(`Library ${job.id} not found, skipping refresh`);
|
this.logger.debug(`Library ${job.id} not found, skipping refresh`);
|
||||||
@@ -588,8 +589,7 @@ export class LibraryService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.LIBRARY_QUEUE_SYNC_ASSETS, queue: QueueName.LIBRARY })
|
async handleQueueSyncAssets(job: IEntityJob): Promise<JobStatus> {
|
||||||
async handleQueueSyncAssets(job: JobOf<JobName.LIBRARY_QUEUE_SYNC_ASSETS>): Promise<JobStatus> {
|
|
||||||
const library = await this.libraryRepository.get(job.id);
|
const library = await this.libraryRepository.get(job.id);
|
||||||
if (!library) {
|
if (!library) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
AudioCodec,
|
AudioCodec,
|
||||||
Colorspace,
|
Colorspace,
|
||||||
ImageFormat,
|
ImageFormat,
|
||||||
|
ToneMapping,
|
||||||
TranscodeHWAccel,
|
TranscodeHWAccel,
|
||||||
TranscodePolicy,
|
TranscodePolicy,
|
||||||
VideoCodec,
|
VideoCodec,
|
||||||
@@ -409,7 +410,7 @@ describe(MediaService.name, () => {
|
|||||||
'-frames:v 1',
|
'-frames:v 1',
|
||||||
'-update 1',
|
'-update 1',
|
||||||
'-v verbose',
|
'-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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
@@ -444,7 +445,7 @@ describe(MediaService.name, () => {
|
|||||||
'-frames:v 1',
|
'-frames:v 1',
|
||||||
'-update 1',
|
'-update 1',
|
||||||
'-v verbose',
|
'-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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
@@ -481,7 +482,7 @@ describe(MediaService.name, () => {
|
|||||||
'-frames:v 1',
|
'-frames:v 1',
|
||||||
'-update 1',
|
'-update 1',
|
||||||
'-v verbose',
|
'-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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
@@ -1327,7 +1328,7 @@ describe(MediaService.name, () => {
|
|||||||
'-map 0:0',
|
'-map 0:0',
|
||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf scale=-2:720',
|
'-vf scale=-2:720,format=yuv420p',
|
||||||
'-preset 12',
|
'-preset 12',
|
||||||
'-crf 23',
|
'-crf 23',
|
||||||
]),
|
]),
|
||||||
@@ -1453,7 +1454,7 @@ describe(MediaService.name, () => {
|
|||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-v verbose',
|
||||||
'-vf hwupload_cuda,scale_cuda=-2:720:format=nv12',
|
'-vf format=nv12,hwupload_cuda,scale_cuda=-2:720',
|
||||||
'-preset p1',
|
'-preset p1',
|
||||||
'-cq:v 23',
|
'-cq:v 23',
|
||||||
]),
|
]),
|
||||||
@@ -1585,7 +1586,7 @@ describe(MediaService.name, () => {
|
|||||||
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
|
inputOptions: expect.arrayContaining(['-hwaccel cuda', '-hwaccel_output_format cuda']),
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
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 () => {
|
it('should set options for qsv', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
@@ -1633,7 +1616,7 @@ describe(MediaService.name, () => {
|
|||||||
'-refs 5',
|
'-refs 5',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-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',
|
'-preset 7',
|
||||||
'-global_quality:v 23',
|
'-global_quality:v 23',
|
||||||
'-maxrate 10000k',
|
'-maxrate 10000k',
|
||||||
@@ -1765,7 +1748,7 @@ describe(MediaService.name, () => {
|
|||||||
]),
|
]),
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
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 () => {
|
it('should set options for vaapi', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
@@ -1842,7 +1799,7 @@ describe(MediaService.name, () => {
|
|||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-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',
|
'-compression_level 7',
|
||||||
'-rc_mode 1',
|
'-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']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
systemMock.get.mockResolvedValue({
|
systemMock.get.mockResolvedValue({
|
||||||
@@ -2030,7 +1987,7 @@ describe(MediaService.name, () => {
|
|||||||
inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
|
inputOptions: expect.arrayContaining(['-hwaccel vaapi', '-hwaccel_output_format vaapi', '-threads 1']),
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
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 () => {
|
it('should use preferred device for vaapi when hardware decoding', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']);
|
storageMock.readdir.mockResolvedValue(['renderD128', 'renderD129', 'renderD130']);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
@@ -2133,7 +2069,7 @@ describe(MediaService.name, () => {
|
|||||||
'-map 0:1',
|
'-map 0:1',
|
||||||
'-g 256',
|
'-g 256',
|
||||||
'-v verbose',
|
'-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',
|
'-level 51',
|
||||||
'-rc_mode CQP',
|
'-rc_mode CQP',
|
||||||
'-qp_init 23',
|
'-qp_init 23',
|
||||||
@@ -2204,7 +2140,7 @@ describe(MediaService.name, () => {
|
|||||||
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
|
inputOptions: expect.arrayContaining(['-hwaccel rkmpp', '-hwaccel_output_format drm_prime', '-afbc rga']),
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
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 () => {
|
it('should use software decoding and tone-mapping if hardware decoding is disabled', async () => {
|
||||||
storageMock.readdir.mockResolvedValue(['renderD128']);
|
storageMock.readdir.mockResolvedValue(['renderD128']);
|
||||||
storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats);
|
storageMock.stat.mockResolvedValue({ isFile: () => true, isCharacterDevice: () => true } as Stats);
|
||||||
@@ -2250,7 +2164,7 @@ describe(MediaService.name, () => {
|
|||||||
inputOptions: [],
|
inputOptions: [],
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
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.readdir.mockResolvedValue(['renderD128']);
|
||||||
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
|
storageMock.stat.mockResolvedValue({ isFile: () => false, isCharacterDevice: () => false } as Stats);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
@@ -2271,10 +2185,10 @@ describe(MediaService.name, () => {
|
|||||||
'/original/path.ext',
|
'/original/path.ext',
|
||||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
inputOptions: expect.any(Array),
|
inputOptions: [],
|
||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
expect.stringContaining(
|
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,
|
twoPass: false,
|
||||||
@@ -2295,7 +2209,7 @@ describe(MediaService.name, () => {
|
|||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
'-c:v h264',
|
'-c:v h264',
|
||||||
'-c:a copy',
|
'-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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
@@ -2315,16 +2229,16 @@ describe(MediaService.name, () => {
|
|||||||
outputOptions: expect.arrayContaining([
|
outputOptions: expect.arrayContaining([
|
||||||
'-c:v h264',
|
'-c:v h264',
|
||||||
'-c:a copy',
|
'-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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should transcode when policy is required and video is not yuv420p', async () => {
|
it('should set npl to 250 for reinhard and mobius tone-mapping algorithms', async () => {
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.videoStream10Bit);
|
mediaMock.probe.mockResolvedValue(probeStub.videoStreamHDR);
|
||||||
systemMock.get.mockResolvedValue({ ffmpeg: { transcode: TranscodePolicy.REQUIRED } });
|
systemMock.get.mockResolvedValue({ ffmpeg: { tonemap: ToneMapping.MOBIUS } });
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
assetMock.getByIds.mockResolvedValue([assetStub.video]);
|
||||||
await sut.handleVideoConversion({ id: assetStub.video.id });
|
await sut.handleVideoConversion({ id: assetStub.video.id });
|
||||||
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
expect(mediaMock.transcode).toHaveBeenCalledWith(
|
||||||
@@ -2332,7 +2246,11 @@ describe(MediaService.name, () => {
|
|||||||
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
'upload/encoded-video/user-id/as/se/asset-id.mp4',
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
inputOptions: expect.any(Array),
|
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,
|
twoPass: false,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { dirname } from 'node:path';
|
import { dirname } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import {
|
import {
|
||||||
@@ -20,10 +19,11 @@ import {
|
|||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { UpsertFileOptions, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import {
|
import {
|
||||||
|
IBaseJob,
|
||||||
|
IEntityJob,
|
||||||
JOBS_ASSET_PAGINATION_SIZE,
|
JOBS_ASSET_PAGINATION_SIZE,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobOf,
|
|
||||||
JobStatus,
|
JobStatus,
|
||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
@@ -39,8 +39,7 @@ export class MediaService extends BaseService {
|
|||||||
private maliOpenCL?: boolean;
|
private maliOpenCL?: boolean;
|
||||||
private devices?: string[];
|
private devices?: string[];
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
|
async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueGenerateThumbnails({ force }: JobOf<JobName.QUEUE_GENERATE_THUMBNAILS>): Promise<JobStatus> {
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
? this.assetRepository.getAll(pagination, {
|
? this.assetRepository.getAll(pagination, {
|
||||||
@@ -91,7 +90,6 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_MIGRATION, queue: QueueName.MIGRATION })
|
|
||||||
async handleQueueMigration(): Promise<JobStatus> {
|
async handleQueueMigration(): Promise<JobStatus> {
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
this.assetRepository.getAll(pagination),
|
this.assetRepository.getAll(pagination),
|
||||||
@@ -122,8 +120,7 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.MIGRATE_ASSET, queue: QueueName.MIGRATION })
|
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleAssetMigration({ id }: JobOf<JobName.MIGRATE_ASSET>): Promise<JobStatus> {
|
|
||||||
const { image } = await this.getConfig({ withCache: true });
|
const { image } = await this.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { files: true });
|
const [asset] = await this.assetRepository.getByIds([id], { files: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -137,8 +134,7 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.GENERATE_THUMBNAILS, queue: QueueName.THUMBNAIL_GENERATION })
|
async handleGenerateThumbnails({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleGenerateThumbnails({ id }: JobOf<JobName.GENERATE_THUMBNAILS>): Promise<JobStatus> {
|
|
||||||
const asset = await this.assetRepository.getById(id, { exifInfo: true, files: true });
|
const asset = await this.assetRepository.getById(id, { exifInfo: true, files: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`);
|
this.logger.warn(`Thumbnail generation failed for asset ${id}: not found`);
|
||||||
@@ -261,8 +257,7 @@ export class MediaService extends BaseService {
|
|||||||
return { previewPath, thumbnailPath, thumbhash };
|
return { previewPath, thumbnailPath, thumbhash };
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
|
async handleQueueVideoConversion(job: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueVideoConversion(job: JobOf<JobName.QUEUE_VIDEO_CONVERSION>): Promise<JobStatus> {
|
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
@@ -280,8 +275,7 @@ export class MediaService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.VIDEO_CONVERSION, queue: QueueName.VIDEO_CONVERSION })
|
async handleVideoConversion({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleVideoConversion({ id }: JobOf<JobName.VIDEO_CONVERSION>): Promise<JobStatus> {
|
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset || asset.type !== AssetType.VIDEO) {
|
if (!asset || asset.type !== AssetType.VIDEO) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@@ -329,11 +323,9 @@ export class MediaService extends BaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
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 {
|
} else {
|
||||||
this.logger.log(
|
this.logger.log(`Encoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()} acceleration`);
|
||||||
`Transcoding video ${asset.id} with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and${ffmpeg.accelDecode ? '' : ' software'} decoding`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -343,26 +335,10 @@ export class MediaService extends BaseService {
|
|||||||
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
if (ffmpeg.accel === TranscodeHWAccel.DISABLED) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||||
let partialFallbackSuccess = false;
|
const config = BaseConfig.create({ ...ffmpeg, accel: TranscodeHWAccel.DISABLED });
|
||||||
if (ffmpeg.accelDecode) {
|
command = config.getCommand(target, mainVideoStream, mainAudioStream);
|
||||||
try {
|
await this.mediaRepository.transcode(input, output, command);
|
||||||
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.log(`Successfully encoded ${asset.id}`);
|
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 isLargerThanTargetBitrate = stream.bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||||
|
|
||||||
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
|
const isTargetVideoCodec = ffmpegConfig.acceptedVideoCodecs.includes(stream.codecName as VideoCodec);
|
||||||
const isRequired = !isTargetVideoCodec || !stream.pixelFormat.endsWith('420p');
|
const isRequired = !isTargetVideoCodec || stream.isHDR;
|
||||||
|
|
||||||
switch (ffmpegConfig.transcode) {
|
switch (ffmpegConfig.transcode) {
|
||||||
case TranscodePolicy.DISABLED: {
|
case TranscodePolicy.DISABLED: {
|
||||||
@@ -520,7 +496,7 @@ export class MediaService extends BaseService {
|
|||||||
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
|
const maliDeviceStat = await this.storageRepository.stat('/dev/mali0');
|
||||||
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
this.maliOpenCL = maliIcdStat.isFile() && maliDeviceStat.isCharacterDevice();
|
||||||
} catch {
|
} 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;
|
this.maliOpenCL = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { constants } from 'node:fs/promises';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.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 { WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { ReverseGeocodeResult } from 'src/interfaces/map.interface';
|
||||||
import { ImmichTags } from 'src/interfaces/metadata.interface';
|
import { ImmichTags } from 'src/interfaces/metadata.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
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: IEntityJob): Promise<JobStatus> {
|
||||||
async handleLivePhotoLinking(job: JobOf<JobName.LINK_LIVE_PHOTOS>): Promise<JobStatus> {
|
|
||||||
const { id } = job;
|
const { id } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
const [asset] = await this.assetRepository.getByIds([id], { exifInfo: true });
|
||||||
if (!asset?.exifInfo) {
|
if (!asset?.exifInfo) {
|
||||||
@@ -152,8 +159,7 @@ export class MetadataService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
async handleQueueMetadataExtraction(job: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueMetadataExtraction(job: JobOf<JobName.QUEUE_METADATA_EXTRACTION>): Promise<JobStatus> {
|
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
@@ -170,8 +176,7 @@ export class MetadataService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.METADATA_EXTRACTION, queue: QueueName.METADATA_EXTRACTION })
|
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleMetadataExtraction({ id }: JobOf<JobName.METADATA_EXTRACTION>): Promise<JobStatus> {
|
|
||||||
const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
|
const { metadata, reverseGeocoding } = await this.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { faces: { person: false } });
|
const [asset] = await this.assetRepository.getByIds([id], { faces: { person: false } });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -187,8 +192,6 @@ export class MetadataService extends BaseService {
|
|||||||
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
const { dateTimeOriginal, localDateTime, timeZone, modifyDate } = this.getDates(asset, exifTags);
|
||||||
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
const { latitude, longitude, country, state, city } = await this.getGeo(exifTags, reverseGeocoding);
|
||||||
|
|
||||||
const { width, height } = this.getImageDimensions(exifTags);
|
|
||||||
|
|
||||||
const exifData: Partial<ExifEntity> = {
|
const exifData: Partial<ExifEntity> = {
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
|
|
||||||
@@ -206,8 +209,8 @@ export class MetadataService extends BaseService {
|
|||||||
|
|
||||||
// image/file
|
// image/file
|
||||||
fileSizeInByte: stats.size,
|
fileSizeInByte: stats.size,
|
||||||
exifImageHeight: validate(height),
|
exifImageHeight: validate(exifTags.ImageHeight),
|
||||||
exifImageWidth: validate(width),
|
exifImageWidth: validate(exifTags.ImageWidth),
|
||||||
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
||||||
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
||||||
bitsPerSample: this.getBitsPerSample(exifTags),
|
bitsPerSample: this.getBitsPerSample(exifTags),
|
||||||
@@ -257,8 +260,7 @@ export class MetadataService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_SIDECAR, queue: QueueName.SIDECAR })
|
async handleQueueSidecar(job: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueSidecar(job: JobOf<JobName.QUEUE_SIDECAR>): Promise<JobStatus> {
|
|
||||||
const { force } = job;
|
const { force } = job;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
return force
|
return force
|
||||||
@@ -278,13 +280,11 @@ export class MetadataService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.SIDECAR_SYNC, queue: QueueName.SIDECAR })
|
handleSidecarSync({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
handleSidecarSync({ id }: JobOf<JobName.SIDECAR_SYNC>): Promise<JobStatus> {
|
|
||||||
return this.processSidecar(id, true);
|
return this.processSidecar(id, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.SIDECAR_DISCOVERY, queue: QueueName.SIDECAR })
|
handleSidecarDiscovery({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
handleSidecarDiscovery({ id }: JobOf<JobName.SIDECAR_DISCOVERY>): Promise<JobStatus> {
|
|
||||||
return this.processSidecar(id, false);
|
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 } });
|
await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.SIDECAR_WRITE, queue: QueueName.SIDECAR })
|
async handleSidecarWrite(job: ISidecarWriteJob): Promise<JobStatus> {
|
||||||
async handleSidecarWrite(job: JobOf<JobName.SIDECAR_WRITE>): Promise<JobStatus> {
|
|
||||||
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job;
|
||||||
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
const [asset] = await this.assetRepository.getByIds([id], { tags: true });
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -335,19 +334,6 @@ export class MetadataService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
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> {
|
private async getExifTags(asset: AssetEntity): Promise<ImmichTags> {
|
||||||
const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
|
const mediaTags = await this.metadataRepository.readTags(asset.originalPath);
|
||||||
const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};
|
const sidecarTags = asset.sidecarPath ? await this.metadataRepository.readTags(asset.sidecarPath) : {};
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
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 { SystemConfigSmtpDto } from 'src/dtos/system-config.dto';
|
||||||
import { AlbumEntity } from 'src/entities/album.entity';
|
import { AlbumEntity } from 'src/entities/album.entity';
|
||||||
import { ArgOf } from 'src/interfaces/event.interface';
|
import { ArgOf } from 'src/interfaces/event.interface';
|
||||||
import {
|
import {
|
||||||
|
IEmailJob,
|
||||||
IEntityJob,
|
IEntityJob,
|
||||||
|
INotifyAlbumInviteJob,
|
||||||
INotifyAlbumUpdateJob,
|
INotifyAlbumUpdateJob,
|
||||||
|
INotifySignupJob,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobOf,
|
|
||||||
JobStatus,
|
JobStatus,
|
||||||
QueueName,
|
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface';
|
import { EmailImageAttachment, EmailTemplate } from 'src/interfaces/notification.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
@@ -175,8 +176,7 @@ export class NotificationService extends BaseService {
|
|||||||
return { messageId };
|
return { messageId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.NOTIFY_SIGNUP, queue: QueueName.NOTIFICATION })
|
async handleUserSignup({ id, tempPassword }: INotifySignupJob) {
|
||||||
async handleUserSignup({ id, tempPassword }: JobOf<JobName.NOTIFY_SIGNUP>) {
|
|
||||||
const user = await this.userRepository.get(id, { withDeleted: false });
|
const user = await this.userRepository.get(id, { withDeleted: false });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -207,8 +207,7 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.NOTIFY_ALBUM_INVITE, queue: QueueName.NOTIFICATION })
|
async handleAlbumInvite({ id, recipientId }: INotifyAlbumInviteJob) {
|
||||||
async handleAlbumInvite({ id, recipientId }: JobOf<JobName.NOTIFY_ALBUM_INVITE>) {
|
|
||||||
const album = await this.albumRepository.getById(id, { withAssets: false });
|
const album = await this.albumRepository.getById(id, { withAssets: false });
|
||||||
if (!album) {
|
if (!album) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -255,8 +254,7 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.NOTIFY_ALBUM_UPDATE, queue: QueueName.NOTIFICATION })
|
async handleAlbumUpdate({ id, recipientIds }: INotifyAlbumUpdateJob) {
|
||||||
async handleAlbumUpdate({ id, recipientIds }: JobOf<JobName.NOTIFY_ALBUM_UPDATE>) {
|
|
||||||
const album = await this.albumRepository.getById(id, { withAssets: false });
|
const album = await this.albumRepository.getById(id, { withAssets: false });
|
||||||
|
|
||||||
if (!album) {
|
if (!album) {
|
||||||
@@ -314,8 +312,7 @@ export class NotificationService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.SEND_EMAIL, queue: QueueName.NOTIFICATION })
|
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
||||||
async handleSendEmail(data: JobOf<JobName.SEND_EMAIL>): Promise<JobStatus> {
|
|
||||||
const { notifications } = await this.getConfig({ withCache: false });
|
const { notifications } = await this.getConfig({ withCache: false });
|
||||||
if (!notifications.smtp.enabled) {
|
if (!notifications.smtp.enabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
import { BadRequestException, Injectable, NotFoundException } from '@nestjs/common';
|
||||||
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
import { FACE_THUMBNAIL_SIZE } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
@@ -34,10 +33,13 @@ import {
|
|||||||
} from 'src/enum';
|
} from 'src/enum';
|
||||||
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import {
|
import {
|
||||||
|
IBaseJob,
|
||||||
|
IDeferrableJob,
|
||||||
|
IEntityJob,
|
||||||
|
INightlyJob,
|
||||||
JOBS_ASSET_PAGINATION_SIZE,
|
JOBS_ASSET_PAGINATION_SIZE,
|
||||||
JobItem,
|
JobItem,
|
||||||
JobName,
|
JobName,
|
||||||
JobOf,
|
|
||||||
JobStatus,
|
JobStatus,
|
||||||
QueueName,
|
QueueName,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
@@ -229,15 +231,13 @@ export class PersonService extends BaseService {
|
|||||||
this.logger.debug(`Deleted ${people.length} people`);
|
this.logger.debug(`Deleted ${people.length} people`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.PERSON_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handlePersonCleanup(): Promise<JobStatus> {
|
async handlePersonCleanup(): Promise<JobStatus> {
|
||||||
const people = await this.personRepository.getAllWithoutFaces();
|
const people = await this.personRepository.getAllWithoutFaces();
|
||||||
await this.delete(people);
|
await this.delete(people);
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_FACE_DETECTION, queue: QueueName.FACE_DETECTION })
|
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueDetectFaces({ force }: JobOf<JobName.QUEUE_FACE_DETECTION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -272,8 +272,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.FACE_DETECTION, queue: QueueName.FACE_DETECTION })
|
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleDetectFaces({ id }: JobOf<JobName.FACE_DETECTION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -377,8 +376,7 @@ export class PersonService extends BaseService {
|
|||||||
return intersection / union;
|
return intersection / union;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
|
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
|
||||||
async handleQueueRecognizeFaces({ force, nightly }: JobOf<JobName.QUEUE_FACIAL_RECOGNITION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -428,8 +426,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.FACIAL_RECOGNITION, queue: QueueName.FACIAL_RECOGNITION })
|
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
||||||
async handleRecognizeFaces({ id, deferred }: JobOf<JobName.FACIAL_RECOGNITION>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -512,8 +509,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.MIGRATE_PERSON, queue: QueueName.MIGRATION })
|
async handlePersonMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handlePersonMigration({ id }: JobOf<JobName.MIGRATE_PERSON>): Promise<JobStatus> {
|
|
||||||
const person = await this.personRepository.getById(id);
|
const person = await this.personRepository.getById(id);
|
||||||
if (!person) {
|
if (!person) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@@ -524,8 +520,7 @@ export class PersonService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.GENERATE_PERSON_THUMBNAIL, queue: QueueName.THUMBNAIL_GENERATION })
|
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
||||||
async handleGeneratePersonThumbnail(data: JobOf<JobName.GENERATE_PERSON_THUMBNAIL>): Promise<JobStatus> {
|
|
||||||
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
|
const { machineLearning, metadata, image } = await this.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
import { SessionResponseDto, mapSession } from 'src/dtos/session.dto';
|
||||||
import { Permission } from 'src/enum';
|
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';
|
import { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SessionService extends BaseService {
|
export class SessionService extends BaseService {
|
||||||
@OnJob({ name: JobName.CLEAN_OLD_SESSION_TOKENS, queue: QueueName.BACKGROUND_TASK })
|
async handleCleanup() {
|
||||||
async handleCleanup(): Promise<JobStatus> {
|
|
||||||
const sessions = await this.sessionRepository.search({
|
const sessions = await this.sessionRepository.search({
|
||||||
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
|
updatedBefore: DateTime.now().minus({ days: 90 }).toJSDate(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,18 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { SystemConfig } from 'src/config';
|
import { SystemConfig } from 'src/config';
|
||||||
import { OnEvent, OnJob } from 'src/decorators';
|
import { OnEvent } from 'src/decorators';
|
||||||
import { ImmichWorker } from 'src/enum';
|
import { ImmichWorker } from 'src/enum';
|
||||||
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
import { WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service';
|
||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc';
|
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 }: IBaseJob): Promise<JobStatus> {
|
||||||
async handleQueueEncodeClip({ force }: JobOf<JobName.QUEUE_SMART_SEARCH>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: false });
|
const { machineLearning } = await this.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -105,8 +111,7 @@ export class SmartInfoService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.SMART_SEARCH, queue: QueueName.SMART_SEARCH })
|
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleEncodeClip({ id }: JobOf<JobName.SMART_SEARCH>): Promise<JobStatus> {
|
|
||||||
const { machineLearning } = await this.getConfig({ withCache: true });
|
const { machineLearning } = await this.getConfig({ withCache: true });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
|
|||||||
@@ -4,13 +4,13 @@ import { DateTime } from 'luxon';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import sanitize from 'sanitize-filename';
|
import sanitize from 'sanitize-filename';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 { SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
import { AssetPathType, AssetType, StorageFolder } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service';
|
||||||
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
import { getLivePhotoMotionFilename } from 'src/utils/file';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
@@ -108,8 +108,7 @@ export class StorageTemplateService extends BaseService {
|
|||||||
return { ...storageTokens, presetOptions: storagePresets };
|
return { ...storageTokens, presetOptions: storagePresets };
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
|
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleMigrationSingle({ id }: JobOf<JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE>): Promise<JobStatus> {
|
|
||||||
const config = await this.getConfig({ withCache: true });
|
const config = await this.getConfig({ withCache: true });
|
||||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||||
if (!storageTemplateEnabled) {
|
if (!storageTemplateEnabled) {
|
||||||
@@ -138,7 +137,6 @@ export class StorageTemplateService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.STORAGE_TEMPLATE_MIGRATION, queue: QueueName.STORAGE_TEMPLATE_MIGRATION })
|
|
||||||
async handleMigration(): Promise<JobStatus> {
|
async handleMigration(): Promise<JobStatus> {
|
||||||
this.logger.log('Starting storage template migration');
|
this.logger.log('Starting storage template migration');
|
||||||
const { storageTemplate } = await this.getConfig({ withCache: true });
|
const { storageTemplate } = await this.getConfig({ withCache: true });
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { IConfigRepository } from 'src/interfaces/config.interface';
|
|||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { StorageService } from 'src/services/storage.service';
|
import { ImmichStartupError, StorageService } from 'src/services/storage.service';
|
||||||
import { ImmichStartupError } from 'src/utils/misc';
|
|
||||||
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
import { mockEnvData } from 'test/repositories/config.repository.mock';
|
||||||
import { newTestService } from 'test/utils';
|
import { newTestService } from 'test/utils';
|
||||||
import { Mocked } from 'vitest';
|
import { Mocked } from 'vitest';
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 { SystemFlags } from 'src/entities/system-metadata.entity';
|
||||||
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
import { StorageFolder, SystemMetadataKey } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
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 { 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.`;
|
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: IDeleteFilesJob) {
|
||||||
async handleDeleteFiles(job: JobOf<JobName.DELETE_FILES>): Promise<JobStatus> {
|
|
||||||
const { files } = job;
|
const { files } = job;
|
||||||
|
|
||||||
// TODO: one job per file
|
// TODO: one job per file
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
bframes: -1,
|
bframes: -1,
|
||||||
refs: 0,
|
refs: 0,
|
||||||
gopSize: 0,
|
gopSize: 0,
|
||||||
|
npl: 0,
|
||||||
temporalAQ: false,
|
temporalAQ: false,
|
||||||
cqMode: CQMode.AUTO,
|
cqMode: CQMode.AUTO,
|
||||||
twoPass: false,
|
twoPass: false,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import {
|
import {
|
||||||
@@ -13,7 +12,7 @@ import {
|
|||||||
} from 'src/dtos/tag.dto';
|
} from 'src/dtos/tag.dto';
|
||||||
import { TagEntity } from 'src/entities/tag.entity';
|
import { TagEntity } from 'src/entities/tag.entity';
|
||||||
import { Permission } from 'src/enum';
|
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 { AssetTagItem } from 'src/interfaces/tag.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
import { addAssets, removeAssets } from 'src/utils/asset.util';
|
||||||
@@ -132,7 +131,6 @@ export class TagService extends BaseService {
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.TAG_CLEANUP, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleTagCleanup() {
|
async handleTagCleanup() {
|
||||||
await this.tagRepository.deleteEmptyTags();
|
await this.tagRepository.deleteEmptyTags();
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
|
|||||||
@@ -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 { BulkIdsDto } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { TrashResponseDto } from 'src/dtos/trash.dto';
|
import { TrashResponseDto } from 'src/dtos/trash.dto';
|
||||||
import { Permission } from 'src/enum';
|
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 { BaseService } from 'src/services/base.service';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
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: {} });
|
await this.jobRepository.queue({ name: JobName.QUEUE_TRASH_EMPTY, data: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.QUEUE_TRASH_EMPTY, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleQueueEmptyTrash() {
|
async handleQueueEmptyTrash() {
|
||||||
let count = 0;
|
let count = 0;
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { BadRequestException, Injectable, NotFoundException } from '@nestjs/comm
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { SALT_ROUNDS } from 'src/constants';
|
import { SALT_ROUNDS } from 'src/constants';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { OnJob } from 'src/decorators';
|
|
||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto';
|
||||||
import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.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 { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||||
import { UserEntity } from 'src/entities/user.entity';
|
import { UserEntity } from 'src/entities/user.entity';
|
||||||
import { CacheControl, StorageFolder, UserMetadataKey } from 'src/enum';
|
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 { UserFindOptions } from 'src/interfaces/user.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { ImmichFileResponse } from 'src/utils/file';
|
import { ImmichFileResponse } from 'src/utils/file';
|
||||||
@@ -164,13 +163,11 @@ export class UserService extends BaseService {
|
|||||||
return licenseData;
|
return licenseData;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.USER_SYNC_USAGE, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleUserSyncUsage(): Promise<JobStatus> {
|
async handleUserSyncUsage(): Promise<JobStatus> {
|
||||||
await this.userRepository.syncUsage();
|
await this.userRepository.syncUsage();
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.USER_DELETE_CHECK, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleUserDeleteCheck(): Promise<JobStatus> {
|
async handleUserDeleteCheck(): Promise<JobStatus> {
|
||||||
const users = await this.userRepository.getDeletedUsers();
|
const users = await this.userRepository.getDeletedUsers();
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
@@ -184,8 +181,7 @@ export class UserService extends BaseService {
|
|||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.USER_DELETION, queue: QueueName.BACKGROUND_TASK })
|
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
||||||
async handleUserDelete({ id, force }: JobOf<JobName.USER_DELETION>): Promise<JobStatus> {
|
|
||||||
const config = await this.getConfig({ withCache: false });
|
const config = await this.getConfig({ withCache: false });
|
||||||
const user = await this.userRepository.get(id, { withDeleted: true });
|
const user = await this.userRepository.get(id, { withDeleted: true });
|
||||||
if (!user) {
|
if (!user) {
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import semver, { SemVer } from 'semver';
|
import semver, { SemVer } from 'semver';
|
||||||
import { serverVersion } from 'src/constants';
|
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 { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto';
|
||||||
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
import { VersionCheckMetadata } from 'src/entities/system-metadata.entity';
|
||||||
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
|
import { ImmichEnvironment, SystemMetadataKey } from 'src/enum';
|
||||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||||
import { ArgOf } from 'src/interfaces/event.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 { BaseService } from 'src/services/base.service';
|
||||||
|
|
||||||
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): ReleaseNotification => {
|
||||||
@@ -48,7 +48,6 @@ export class VersionService extends BaseService {
|
|||||||
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
|
await this.jobRepository.queue({ name: JobName.VERSION_CHECK, data: {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
@OnJob({ name: JobName.VERSION_CHECK, queue: QueueName.BACKGROUND_TASK })
|
|
||||||
async handleVersionCheck(): Promise<JobStatus> {
|
async handleVersionCheck(): Promise<JobStatus> {
|
||||||
try {
|
try {
|
||||||
this.logger.debug('Running version check');
|
this.logger.debug('Running version check');
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const logGlobalError = (logger: ILoggerRepository, error: Error) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (error instanceof Error) {
|
if (error instanceof Error) {
|
||||||
logger.error(`Unknown error: ${error}`, error?.stack);
|
logger.error(`Unknown error: ${error}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
+250
-143
@@ -58,9 +58,10 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case TranscodeHWAccel.RKMPP: {
|
case TranscodeHWAccel.RKMPP: {
|
||||||
handler = config.accelDecode
|
handler =
|
||||||
? new RkmppHwDecodeConfig(config, devices, hasMaliOpenCL)
|
config.accelDecode && hasMaliOpenCL
|
||||||
: new RkmppSwDecodeConfig(config, devices);
|
? new RkmppHwDecodeConfig(config, devices)
|
||||||
|
: new RkmppSwDecodeConfig(config, devices);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
@@ -148,11 +149,7 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
options.push(`scale=${this.getScaling(videoStream)}`);
|
options.push(`scale=${this.getScaling(videoStream)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
options.push(...this.getToneMapping(videoStream));
|
options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
|
||||||
if (options.length === 0 && !videoStream.pixelFormat.endsWith('420p')) {
|
|
||||||
options.push(`format=yuv420p`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,20 +271,33 @@ export class BaseConfig implements VideoCodecSWConfig {
|
|||||||
|
|
||||||
getColors() {
|
getColors() {
|
||||||
return {
|
return {
|
||||||
primaries: 'bt709',
|
primaries: '709',
|
||||||
transfer: 'bt709',
|
transfer: '709',
|
||||||
matrix: 'bt709',
|
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) {
|
getToneMapping(videoStream: VideoStreamInfo) {
|
||||||
if (!this.shouldToneMap(videoStream)) {
|
if (!this.shouldToneMap(videoStream)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { primaries, transfer, matrix } = this.getColors();
|
const colors = 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];
|
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 {
|
getAudioCodec(): string {
|
||||||
@@ -326,32 +336,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
|
|||||||
this.devices = this.validateDevices(devices);
|
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() {
|
getSupportedCodecs() {
|
||||||
return [VideoCodec.H264, VideoCodec.HEVC];
|
return [VideoCodec.H264, VideoCodec.HEVC];
|
||||||
}
|
}
|
||||||
@@ -395,20 +379,6 @@ export class BaseHWConfig extends BaseConfig implements VideoCodecHWConfig {
|
|||||||
|
|
||||||
return `/dev/dri/${deviceName}`;
|
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 {
|
export class ThumbnailConfig extends BaseConfig {
|
||||||
@@ -425,14 +395,19 @@ export class ThumbnailConfig extends BaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFilterOptions(videoStream: VideoStreamInfo): string[] {
|
getFilterOptions(videoStream: VideoStreamInfo): string[] {
|
||||||
return [
|
const options = [
|
||||||
'fps=12:eof_action=pass:round=down',
|
'fps=12:eof_action=pass:round=down',
|
||||||
'thumbnail=12',
|
'thumbnail=12',
|
||||||
String.raw`select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20)`,
|
String.raw`select=gt(scene\,0.1)-eq(prev_selected_n\,n)+isnan(prev_selected_n)+gt(n\,20)`,
|
||||||
'trim=end_frame=2',
|
'trim=end_frame=2',
|
||||||
'reverse',
|
'reverse',
|
||||||
...super.getFilterOptions(videoStream),
|
|
||||||
];
|
];
|
||||||
|
if (this.shouldScale(videoStream)) {
|
||||||
|
options.push(`scale=${this.getScaling(videoStream)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.push(...this.getToneMapping(videoStream), 'format=yuv420p');
|
||||||
|
return options;
|
||||||
}
|
}
|
||||||
|
|
||||||
getPresetOptions() {
|
getPresetOptions() {
|
||||||
@@ -448,7 +423,19 @@ export class ThumbnailConfig extends BaseConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getScaling(videoStream: VideoStreamInfo) {
|
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() {
|
getSupportedCodecs() {
|
||||||
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.AV1];
|
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) {
|
getBaseOutputOptions(target: TranscodeTarget, videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||||
const options = [
|
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
|
// 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;
|
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() {
|
getPresetOptions() {
|
||||||
let presetIndex = this.getPresetIndex();
|
let presetIndex = this.getPresetIndex();
|
||||||
if (presetIndex < 0) {
|
if (presetIndex < 0) {
|
||||||
@@ -595,6 +596,10 @@ export class NvencConfig extends BaseHWConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getThreadOptions() {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
getRefs() {
|
getRefs() {
|
||||||
const bframes = this.getBFrames();
|
const bframes = this.getBFrames();
|
||||||
if (bframes > 0 && bframes < 3 && this.config.refs < 3) {
|
if (bframes > 0 && bframes < 3 && this.config.refs < 3) {
|
||||||
@@ -602,59 +607,68 @@ export class NvencConfig extends BaseHWConfig {
|
|||||||
}
|
}
|
||||||
return this.config.refs;
|
return this.config.refs;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class NvencHwDecodeConfig extends NvencSwDecodeConfig {
|
||||||
getBaseInputOptions() {
|
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()];
|
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) {
|
getToneMapping(videoStream: VideoStreamInfo) {
|
||||||
if (!this.shouldToneMap(videoStream)) {
|
if (!this.shouldToneMap(videoStream)) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { matrix, primaries, transfer } = this.getColors();
|
const colors = this.getColors();
|
||||||
const tonemapOptions = [
|
const tonemapOptions = [
|
||||||
'desat=0',
|
'desat=0',
|
||||||
`matrix=${matrix}`,
|
`matrix=${colors.matrix}`,
|
||||||
`primaries=${primaries}`,
|
`primaries=${colors.primaries}`,
|
||||||
'range=pc',
|
'range=pc',
|
||||||
`tonemap=${this.config.tonemap}`,
|
`tonemap=${this.config.tonemap}`,
|
||||||
'tonemap_mode=lum',
|
`transfer=${colors.transfer}`,
|
||||||
`transfer=${transfer}`,
|
|
||||||
'peak=100',
|
|
||||||
'format=nv12',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [`tonemap_cuda=${tonemapOptions.join(':')}`];
|
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() {
|
getBaseInputOptions() {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
throw new Error('No QSV device found');
|
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 = '';
|
let qsvString = '';
|
||||||
|
const hwDevice = this.getPreferredHardwareDevice();
|
||||||
if (hwDevice) {
|
if (hwDevice) {
|
||||||
qsvString = `,child_device=${hwDevice}`;
|
qsvString = `,child_device=${hwDevice}`;
|
||||||
}
|
}
|
||||||
@@ -671,6 +685,15 @@ export class QsvConfig extends BaseHWConfig {
|
|||||||
return options;
|
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() {
|
getPresetOptions() {
|
||||||
let presetIndex = this.getPresetIndex();
|
let presetIndex = this.getPresetIndex();
|
||||||
if (presetIndex < 0) {
|
if (presetIndex < 0) {
|
||||||
@@ -716,9 +739,41 @@ export class QsvConfig extends BaseHWConfig {
|
|||||||
getScaling(videoStream: VideoStreamInfo): string {
|
getScaling(videoStream: VideoStreamInfo): string {
|
||||||
return super.getScaling(videoStream, 1);
|
return super.getScaling(videoStream, 1);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
|
export class QsvHwDecodeConfig extends QsvSwDecodeConfig {
|
||||||
return `scale_qsv=${this.getScaling(videoStream)}:async_depth=4:mode=hq:format=${format}`;
|
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[] {
|
getToneMapping(videoStream: VideoStreamInfo): string[] {
|
||||||
@@ -726,17 +781,15 @@ export class QsvConfig extends BaseHWConfig {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { matrix, primaries, transfer } = this.getColors();
|
const colors = this.getColors();
|
||||||
const tonemapOptions = [
|
const tonemapOptions = [
|
||||||
'desat=0',
|
'desat=0',
|
||||||
'format=nv12',
|
'format=nv12',
|
||||||
`matrix=${matrix}`,
|
`matrix=${colors.matrix}`,
|
||||||
`primaries=${primaries}`,
|
`primaries=${colors.primaries}`,
|
||||||
`transfer=${transfer}`,
|
|
||||||
'range=pc',
|
'range=pc',
|
||||||
`tonemap=${this.config.tonemap}`,
|
`tonemap=${this.config.tonemap}`,
|
||||||
'tonemap_mode=lum',
|
`transfer=${colors.transfer}`,
|
||||||
'peak=100',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -745,27 +798,27 @@ export class QsvConfig extends BaseHWConfig {
|
|||||||
'hwmap=derive_device=qsv:reverse=1,format=qsv',
|
'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() {
|
getBaseInputOptions() {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
throw new Error('No VAAPI device found');
|
throw new Error('No VAAPI device found');
|
||||||
}
|
}
|
||||||
|
|
||||||
let hwDevice = this.getPreferredHardwareDevice();
|
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) {
|
if (!hwDevice) {
|
||||||
hwDevice = `/dev/dri/${this.devices[0]}`;
|
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'];
|
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() {
|
getPresetOptions() {
|
||||||
let presetIndex = this.getPresetIndex();
|
let presetIndex = this.getPresetIndex();
|
||||||
if (presetIndex < 0) {
|
if (presetIndex < 0) {
|
||||||
@@ -814,9 +877,40 @@ export class VaapiConfig extends BaseHWConfig {
|
|||||||
useCQP() {
|
useCQP() {
|
||||||
return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9;
|
return this.config.cqMode !== CQMode.ICQ || this.config.targetVideoCodec === VideoCodec.VP9;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getScalingFilter(videoStream: VideoStreamInfo, format: string) {
|
export class VaapiHwDecodeConfig extends VaapiSwDecodeConfig {
|
||||||
return `scale_vaapi=${this.getScaling(videoStream)}:mode=hq:out_range=pc:format=${format}`;
|
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[] {
|
getToneMapping(videoStream: VideoStreamInfo): string[] {
|
||||||
@@ -824,17 +918,15 @@ export class VaapiConfig extends BaseHWConfig {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const { matrix, primaries, transfer } = this.getColors();
|
const colors = this.getColors();
|
||||||
const tonemapOptions = [
|
const tonemapOptions = [
|
||||||
'desat=0',
|
'desat=0',
|
||||||
'format=nv12',
|
'format=nv12',
|
||||||
`matrix=${matrix}`,
|
`matrix=${colors.matrix}`,
|
||||||
`primaries=${primaries}`,
|
`primaries=${colors.primaries}`,
|
||||||
`transfer=${transfer}`,
|
|
||||||
'range=pc',
|
'range=pc',
|
||||||
`tonemap=${this.config.tonemap}`,
|
`tonemap=${this.config.tonemap}`,
|
||||||
'tonemap_mode=lum',
|
`transfer=${colors.transfer}`,
|
||||||
'peak=100',
|
|
||||||
];
|
];
|
||||||
|
|
||||||
return [
|
return [
|
||||||
@@ -843,33 +935,37 @@ export class VaapiConfig extends BaseHWConfig {
|
|||||||
'hwmap=derive_device=vaapi:reverse=1,format=vaapi',
|
'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 {
|
export class RkmppSwDecodeConfig extends BaseHWConfig {
|
||||||
protected hasMaliOpenCL: boolean;
|
|
||||||
constructor(
|
constructor(
|
||||||
protected config: SystemConfigFFmpegDto,
|
protected config: SystemConfigFFmpegDto,
|
||||||
devices: string[] = [],
|
devices: string[] = [],
|
||||||
hasMaliOpenCL = false,
|
|
||||||
) {
|
) {
|
||||||
super(config, devices);
|
super(config, devices);
|
||||||
this.hasMaliOpenCL = hasMaliOpenCL;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
eligibleForTwoPass(): boolean {
|
eligibleForTwoPass(): boolean {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
getBaseInputOptions() {
|
getBaseInputOptions(): string[] {
|
||||||
if (this.devices.length === 0) {
|
if (this.devices.length === 0) {
|
||||||
throw new Error('No RKMPP device found');
|
throw new Error('No RKMPP device found');
|
||||||
}
|
}
|
||||||
|
return [];
|
||||||
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'];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getPresetOptions() {
|
getPresetOptions() {
|
||||||
@@ -902,30 +998,41 @@ export class RkmppConfig extends BaseHWConfig {
|
|||||||
return [VideoCodec.H264, VideoCodec.HEVC];
|
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) {
|
getFilterOptions(videoStream: VideoStreamInfo) {
|
||||||
if (this.shouldToneMap(videoStream)) {
|
if (this.shouldToneMap(videoStream)) {
|
||||||
const { primaries, transfer, matrix } = this.getColors();
|
const colors = 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',
|
|
||||||
];
|
|
||||||
}
|
|
||||||
return [
|
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`,
|
||||||
`scale_rkrga=${this.getScaling(videoStream)}:format=p010:afbc=1:async_depth=4`,
|
'hwmap=derive_device=opencl:mode=read',
|
||||||
'hwdownload',
|
`tonemap_opencl=format=nv12:r=pc:p=${colors.primaries}:t=${colors.transfer}:m=${colors.matrix}:tonemap=${this.config.tonemap}:desat=0`,
|
||||||
'format=p010',
|
'hwmap=derive_device=rkmpp:mode=write:reverse=1',
|
||||||
`tonemapx=tonemap=${this.config.tonemap}:desat=0:p=${primaries}:t=${transfer}:m=${matrix}:r=pc:peak=100:format=yuv420p`,
|
'format=drm_prime',
|
||||||
'hwupload',
|
|
||||||
];
|
];
|
||||||
} else if (this.shouldScale(videoStream)) {
|
} 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 [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getColors() {
|
||||||
|
return {
|
||||||
|
primaries: 'bt709',
|
||||||
|
transfer: 'bt709',
|
||||||
|
matrix: 'bt709',
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,32 +15,6 @@ import { CLIP_MODEL_INFO, serverVersion } from 'src/constants';
|
|||||||
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
import { ImmichCookie, ImmichHeader, MetadataKey } from 'src/enum';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
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) =>
|
export const getExternalDomain = (server: SystemConfig['server'], port: number) =>
|
||||||
server.externalDomain || `http://localhost:${port}`;
|
server.externalDomain || `http://localhost:${port}`;
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
|||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
import { bootstrapTelemetry } from 'src/repositories/telemetry.repository';
|
||||||
import { ApiService } from 'src/services/api.service';
|
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() {
|
async function bootstrap() {
|
||||||
process.title = 'immich-api';
|
process.title = 'immich-api';
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||||
import { ConfigRepository } from 'src/repositories/config.repository';
|
import { ConfigRepository } from 'src/repositories/config.repository';
|
||||||
import { bootstrapTelemetry } from 'src/repositories/telemetry.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() {
|
export async function bootstrap() {
|
||||||
const { telemetry } = new ConfigRepository().getEnv();
|
const { telemetry } = new ConfigRepository().getEnv();
|
||||||
|
|||||||
Vendored
-25
@@ -17,7 +17,6 @@ const probeStubDefaultVideoStream: VideoStreamInfo[] = [
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -44,7 +43,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -55,7 +53,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -71,7 +68,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -87,7 +83,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -107,23 +102,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: true,
|
isHDR: true,
|
||||||
bitrate: 0,
|
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,
|
rotation: 90,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -155,7 +132,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
@@ -171,7 +147,6 @@ export const probeStub = {
|
|||||||
rotation: 0,
|
rotation: 0,
|
||||||
isHDR: false,
|
isHDR: false,
|
||||||
bitrate: 0,
|
bitrate: 0,
|
||||||
pixelFormat: 'yuv420p',
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { Mocked, vitest } from 'vitest';
|
|||||||
|
|
||||||
export const newJobRepositoryMock = (): Mocked<IJobRepository> => {
|
export const newJobRepositoryMock = (): Mocked<IJobRepository> => {
|
||||||
return {
|
return {
|
||||||
setup: vitest.fn(),
|
addHandler: vitest.fn(),
|
||||||
startWorkers: vitest.fn(),
|
|
||||||
run: vitest.fn(),
|
|
||||||
addCronJob: vitest.fn(),
|
addCronJob: vitest.fn(),
|
||||||
updateCronJob: vitest.fn(),
|
updateCronJob: vitest.fn(),
|
||||||
setConcurrency: vitest.fn(),
|
setConcurrency: vitest.fn(),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
import { ChildProcessWithoutNullStreams } from 'node:child_process';
|
||||||
import { Writable } from 'node:stream';
|
import { Writable } from 'node:stream';
|
||||||
import { PNG } from 'pngjs';
|
import { PNG } from 'pngjs';
|
||||||
import { ImmichWorker } from 'src/enum';
|
|
||||||
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
import { IMetadataRepository } from 'src/interfaces/metadata.interface';
|
||||||
import { BaseService } from 'src/services/base.service';
|
import { BaseService } from 'src/services/base.service';
|
||||||
import { newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
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 { Readable } from 'typeorm/platform/PlatformTools';
|
||||||
import { Mocked, vitest } from 'vitest';
|
import { Mocked, vitest } from 'vitest';
|
||||||
|
|
||||||
type Overrides = {
|
type RepositoryOverrides = {
|
||||||
worker?: ImmichWorker;
|
metadataRepository: IMetadataRepository;
|
||||||
metadataRepository?: IMetadataRepository;
|
|
||||||
};
|
};
|
||||||
type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
|
type BaseServiceArgs = ConstructorParameters<typeof BaseService>;
|
||||||
type Constructor<Type, Args extends Array<any>> = {
|
type Constructor<Type, Args extends Array<any>> = {
|
||||||
@@ -56,11 +54,9 @@ type Constructor<Type, Args extends Array<any>> = {
|
|||||||
|
|
||||||
export const newTestService = <T extends BaseService>(
|
export const newTestService = <T extends BaseService>(
|
||||||
Service: Constructor<T, BaseServiceArgs>,
|
Service: Constructor<T, BaseServiceArgs>,
|
||||||
overrides?: Overrides,
|
overrides?: RepositoryOverrides,
|
||||||
) => {
|
) => {
|
||||||
const { metadataRepository, worker: workerOverride } = overrides || {};
|
const { metadataRepository } = overrides || {};
|
||||||
|
|
||||||
const worker = workerOverride || ImmichWorker.API;
|
|
||||||
|
|
||||||
const accessMock = newAccessRepositoryMock();
|
const accessMock = newAccessRepositoryMock();
|
||||||
const loggerMock = newLoggerRepositoryMock();
|
const loggerMock = newLoggerRepositoryMock();
|
||||||
@@ -102,7 +98,6 @@ export const newTestService = <T extends BaseService>(
|
|||||||
const viewMock = newViewRepositoryMock();
|
const viewMock = newViewRepositoryMock();
|
||||||
|
|
||||||
const sut = new Service(
|
const sut = new Service(
|
||||||
worker,
|
|
||||||
loggerMock,
|
loggerMock,
|
||||||
accessMock,
|
accessMock,
|
||||||
activityMock,
|
activityMock,
|
||||||
|
|||||||
+1
-1
@@ -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
|
RUN apk add --no-cache tini
|
||||||
USER node
|
USER node
|
||||||
|
|||||||
Generated
+129
-121
@@ -23,9 +23,9 @@
|
|||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"socket.io-client": "~4.7.5",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-gestures": "^5.0.4",
|
"svelte-gestures": "^5.0.4",
|
||||||
"svelte-i18n": "^4.0.1",
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
"svelte-maplibre": "^0.9.13",
|
"svelte-maplibre": "^0.9.13",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
@@ -35,12 +35,12 @@
|
|||||||
"@eslint/js": "^9.8.0",
|
"@eslint/js": "^9.8.0",
|
||||||
"@faker-js/faker": "^9.0.0",
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@socket.io/component-emitter": "^3.1.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/enhanced-img": "^0.3.0",
|
||||||
"@sveltejs/kit": "^2.7.2",
|
"@sveltejs/kit": "^2.5.18",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@testing-library/jest-dom": "^6.4.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",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/dom-to-image": "^2.6.7",
|
"@types/dom-to-image": "^2.6.7",
|
||||||
"@types/justified-layout": "^4.1.4",
|
"@types/justified-layout": "^4.1.4",
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
"prettier-plugin-sort-json": "^4.0.0",
|
"prettier-plugin-sort-json": "^4.0.0",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^5.1.5",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
@@ -80,7 +80,7 @@
|
|||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.8.1",
|
"@types/node": "^22.8.0",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1994,42 +1994,43 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||||
"version": "4.0.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz",
|
||||||
"integrity": "sha512-kpVJwF+gNiMEsoHaw+FJL76IYiwBikkxYU83+BpqQLdVMff19KeRKLd2wisS8niNBMJ2omv5gG+iGDDwd8jzag==",
|
"integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^3.0.0-next.0||^3.0.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^2.1.0",
|
||||||
"debug": "^4.3.7",
|
"debug": "^4.3.4",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"kleur": "^4.1.5",
|
"kleur": "^4.1.5",
|
||||||
"magic-string": "^0.30.12",
|
"magic-string": "^0.30.10",
|
||||||
"vitefu": "^1.0.3"
|
"svelte-hmr": "^0.16.0",
|
||||||
|
"vitefu": "^0.2.5"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22"
|
"node": "^18.0.0 || >=20"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||||
"version": "3.0.1",
|
"version": "2.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-3.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-2.1.0.tgz",
|
||||||
"integrity": "sha512-2CKypmj1sM4GE7HjllT7UKmo4Q6L5xFRd7VMGEWhYnZ+wc6AUVU01IBd7yUi6WnFndEwWoMNOd6e8UjoN0nbvQ==",
|
"integrity": "sha512-9QX28IymvBlSCqsCll5t0kQVxipsfhFFL+L2t3nTWfXnddYwxBuAEtTtlaVQpRz9c37BhJjltSeY4AJSC03SSg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"debug": "^4.3.7"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.0.0 || ^20.0.0 || >=22"
|
"node": "^18.0.0 || >=20"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0-next.0||^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.0.0",
|
||||||
"svelte": "^5.0.0-next.96 || ^5.0.0",
|
"svelte": "^4.0.0 || ^5.0.0-next.0",
|
||||||
"vite": "^5.0.0"
|
"vite": "^5.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -2229,6 +2230,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.4.tgz",
|
||||||
"integrity": "sha512-EFdy73+lULQgMJ1WolAymrxWWrPv9DWyDuDFKKlUip2PA/EXuHptzfYOKWljccFWDKhhGOu3dqNmoc2f/h/Ecg==",
|
"integrity": "sha512-EFdy73+lULQgMJ1WolAymrxWWrPv9DWyDuDFKKlUip2PA/EXuHptzfYOKWljccFWDKhhGOu3dqNmoc2f/h/Ecg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@testing-library/dom": "^10.0.0"
|
"@testing-library/dom": "^10.0.0"
|
||||||
},
|
},
|
||||||
@@ -2795,15 +2797,6 @@
|
|||||||
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
|
"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": {
|
"node_modules/agent-base": {
|
||||||
"version": "7.1.0",
|
"version": "7.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz",
|
||||||
@@ -2890,7 +2883,6 @@
|
|||||||
"version": "5.3.0",
|
"version": "5.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
|
||||||
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"dequal": "^2.0.3"
|
"dequal": "^2.0.3"
|
||||||
}
|
}
|
||||||
@@ -2968,12 +2960,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/axobject-query": {
|
"node_modules/axobject-query": {
|
||||||
"version": "4.1.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.0.0.tgz",
|
||||||
"integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==",
|
"integrity": "sha512-+60uv1hiVFhHZeO+Lz0RYzsVHy5Wr1ayX0mwda9KPDVLNJgZ1T9Ny7VmFbLDzxsH0D87I86vgj3gFrjTJUYznw==",
|
||||||
"license": "Apache-2.0",
|
"dependencies": {
|
||||||
"engines": {
|
"dequal": "^2.0.3"
|
||||||
"node": ">= 0.4"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/balanced-match": {
|
"node_modules/balanced-match": {
|
||||||
@@ -3280,6 +3271,18 @@
|
|||||||
"node": ">=6"
|
"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": {
|
"node_modules/color": {
|
||||||
"version": "4.2.3",
|
"version": "4.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
|
||||||
@@ -3401,6 +3404,18 @@
|
|||||||
"node": ">= 8"
|
"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": {
|
"node_modules/css.escape": {
|
||||||
"version": "1.5.1",
|
"version": "1.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||||
@@ -3490,12 +3505,11 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.3.7",
|
"version": "4.3.6",
|
||||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz",
|
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz",
|
||||||
"integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==",
|
"integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ms": "^2.1.3"
|
"ms": "2.1.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=6.0"
|
"node": ">=6.0"
|
||||||
@@ -3642,23 +3656,22 @@
|
|||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-client": {
|
"node_modules/engine.io-client": {
|
||||||
"version": "6.5.4",
|
"version": "6.6.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.1.tgz",
|
||||||
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
|
"integrity": "sha512-aYuoak7I+R83M/BBPIOs2to51BmFIpC1wZe6zZzMrT2llVsHy5cvcmdsJgP2Qz6smHu+sD9oexiSUAVd8OfBPw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.1",
|
"debug": "~4.3.1",
|
||||||
"engine.io-parser": "~5.2.1",
|
"engine.io-parser": "~5.2.1",
|
||||||
"ws": "~8.17.1",
|
"ws": "~8.17.1",
|
||||||
"xmlhttprequest-ssl": "~2.0.0"
|
"xmlhttprequest-ssl": "~2.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/engine.io-parser": {
|
"node_modules/engine.io-parser": {
|
||||||
"version": "5.2.3",
|
"version": "5.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz",
|
||||||
"integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
|
"integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==",
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.0.0"
|
"node": ">=10.0.0"
|
||||||
}
|
}
|
||||||
@@ -4138,7 +4151,8 @@
|
|||||||
"node_modules/esm-env": {
|
"node_modules/esm-env": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
|
"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": {
|
"node_modules/esniff": {
|
||||||
"version": "2.0.1",
|
"version": "2.0.1",
|
||||||
@@ -4183,16 +4197,6 @@
|
|||||||
"node": ">=0.10"
|
"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": {
|
"node_modules/esrecurse": {
|
||||||
"version": "4.3.0",
|
"version": "4.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
|
||||||
@@ -4218,7 +4222,6 @@
|
|||||||
"version": "3.0.3",
|
"version": "3.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz",
|
||||||
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
"integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "^1.0.0"
|
"@types/estree": "^1.0.0"
|
||||||
}
|
}
|
||||||
@@ -4959,7 +4962,6 @@
|
|||||||
"version": "3.0.2",
|
"version": "3.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.2.tgz",
|
||||||
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
"integrity": "sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==",
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "*"
|
"@types/estree": "*"
|
||||||
}
|
}
|
||||||
@@ -5327,9 +5329,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/magic-string": {
|
"node_modules/magic-string": {
|
||||||
"version": "0.30.12",
|
"version": "0.30.11",
|
||||||
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz",
|
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz",
|
||||||
"integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==",
|
"integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0"
|
"@jridgewell/sourcemap-codec": "^1.5.0"
|
||||||
@@ -5473,6 +5475,11 @@
|
|||||||
"url": "https://github.com/maplibre/maplibre-gl-js?sponsor=1"
|
"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": {
|
"node_modules/memoizee": {
|
||||||
"version": "0.4.17",
|
"version": "0.4.17",
|
||||||
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
|
"resolved": "https://registry.npmjs.org/memoizee/-/memoizee-0.4.17.tgz",
|
||||||
@@ -5594,10 +5601,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/ms": {
|
"node_modules/ms": {
|
||||||
"version": "2.1.3",
|
"version": "2.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
|
||||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
"license": "MIT"
|
|
||||||
},
|
},
|
||||||
"node_modules/murmurhash-js": {
|
"node_modules/murmurhash-js": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
@@ -5937,6 +5943,16 @@
|
|||||||
"pbf": "bin/pbf"
|
"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": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.0",
|
"version": "1.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
|
||||||
@@ -6844,14 +6860,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/socket.io-client": {
|
"node_modules/socket.io-client": {
|
||||||
"version": "4.7.5",
|
"version": "4.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
|
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz",
|
||||||
"integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==",
|
"integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@socket.io/component-emitter": "~3.1.0",
|
"@socket.io/component-emitter": "~3.1.0",
|
||||||
"debug": "~4.3.2",
|
"debug": "~4.3.2",
|
||||||
"engine.io-client": "~6.5.2",
|
"engine.io-client": "~6.6.1",
|
||||||
"socket.io-parser": "~4.2.4"
|
"socket.io-parser": "~4.2.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -6914,7 +6930,6 @@
|
|||||||
"version": "1.2.1",
|
"version": "1.2.1",
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||||
"dev": true,
|
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
@@ -7161,27 +7176,28 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte": {
|
"node_modules/svelte": {
|
||||||
"version": "5.1.5",
|
"version": "4.2.19",
|
||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz",
|
||||||
"integrity": "sha512-AyYondx6wS0g8mmBMfwJVnOYYBswjBv6L4bc99awfbET2KozWvVwxe8NSN7fhx7Pgr7pOfOXIv7K8+Impc0OoQ==",
|
"integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@ampproject/remapping": "^2.3.0",
|
"@ampproject/remapping": "^2.2.1",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.4.15",
|
||||||
"@types/estree": "^1.0.5",
|
"@jridgewell/trace-mapping": "^0.3.18",
|
||||||
"acorn": "^8.12.1",
|
"@types/estree": "^1.0.1",
|
||||||
"acorn-typescript": "^1.4.13",
|
"acorn": "^8.9.0",
|
||||||
"aria-query": "^5.3.1",
|
"aria-query": "^5.3.0",
|
||||||
"axobject-query": "^4.1.0",
|
"axobject-query": "^4.0.0",
|
||||||
"esm-env": "^1.0.0",
|
"code-red": "^1.0.3",
|
||||||
"esrap": "^1.2.2",
|
"css-tree": "^2.3.1",
|
||||||
"is-reference": "^3.0.2",
|
"estree-walker": "^3.0.3",
|
||||||
|
"is-reference": "^3.0.1",
|
||||||
"locate-character": "^3.0.0",
|
"locate-character": "^3.0.0",
|
||||||
"magic-string": "^0.30.11",
|
"magic-string": "^0.30.4",
|
||||||
"zimmerframe": "^1.1.2"
|
"periscopic": "^3.1.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=16"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/svelte-check": {
|
"node_modules/svelte-check": {
|
||||||
@@ -7302,6 +7318,18 @@
|
|||||||
"integrity": "sha512-kElJnoZrQtlkXE0O/RcKioz9NP0Sxx05j31ohyosNkydo6NOEsZB85mhoaCxOQNjxN+QPumYWfmIUsznYFjihA==",
|
"integrity": "sha512-kElJnoZrQtlkXE0O/RcKioz9NP0Sxx05j31ohyosNkydo6NOEsZB85mhoaCxOQNjxN+QPumYWfmIUsznYFjihA==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/svelte-i18n": {
|
||||||
"version": "4.0.1",
|
"version": "4.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/svelte-i18n/-/svelte-i18n-4.0.1.tgz",
|
"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"
|
"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": {
|
"node_modules/symbol-tree": {
|
||||||
"version": "3.2.4",
|
"version": "3.2.4",
|
||||||
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
|
||||||
@@ -8401,17 +8420,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitefu": {
|
"node_modules/vitefu": {
|
||||||
"version": "1.0.3",
|
"version": "0.2.5",
|
||||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-0.2.5.tgz",
|
||||||
"integrity": "sha512-iKKfOMBHob2WxEJbqbJjHAkmYgvFDPhuqrO82om83S8RLk+17FtyMBfcyeH8GqD0ihShtkMW/zzJgiA51hCNCQ==",
|
"integrity": "sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
|
||||||
"workspaces": [
|
|
||||||
"tests/deps/*",
|
|
||||||
"tests/projects/*"
|
|
||||||
],
|
|
||||||
"peerDependencies": {
|
"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": {
|
"peerDependenciesMeta": {
|
||||||
"vite": {
|
"vite": {
|
||||||
@@ -8743,9 +8757,9 @@
|
|||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
"node_modules/xmlhttprequest-ssl": {
|
"node_modules/xmlhttprequest-ssl": {
|
||||||
"version": "2.0.0",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.1.tgz",
|
||||||
"integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==",
|
"integrity": "sha512-ptjR8YSJIXoA3Mbv5po7RtSYHO6mZr8s7i5VGmEk7QY2pQWyT1o0N+W1gKbOyJPUCGXGnuw0wqe8f0L6Y0ny7g==",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.4.0"
|
"node": ">=0.4.0"
|
||||||
}
|
}
|
||||||
@@ -8806,12 +8820,6 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-8
@@ -8,7 +8,7 @@
|
|||||||
"build:stats": "BUILD_STATS=true vite build",
|
"build:stats": "BUILD_STATS=true vite build",
|
||||||
"package": "svelte-kit package",
|
"package": "svelte-kit package",
|
||||||
"preview": "vite preview",
|
"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:typescript": "tsc --noEmit",
|
||||||
"check:watch": "npm run check:svelte -- --watch",
|
"check:watch": "npm run check:svelte -- --watch",
|
||||||
"check:code": "npm run format && npm run lint && npm run check:svelte && npm run check:typescript",
|
"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",
|
"@eslint/js": "^9.8.0",
|
||||||
"@faker-js/faker": "^9.0.0",
|
"@faker-js/faker": "^9.0.0",
|
||||||
"@socket.io/component-emitter": "^3.1.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/enhanced-img": "^0.3.0",
|
||||||
"@sveltejs/kit": "^2.7.2",
|
"@sveltejs/kit": "^2.5.18",
|
||||||
"@sveltejs/vite-plugin-svelte": "^4.0.0",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@testing-library/jest-dom": "^6.4.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",
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/dom-to-image": "^2.6.7",
|
"@types/dom-to-image": "^2.6.7",
|
||||||
"@types/justified-layout": "^4.1.4",
|
"@types/justified-layout": "^4.1.4",
|
||||||
@@ -55,7 +55,7 @@
|
|||||||
"prettier-plugin-sort-json": "^4.0.0",
|
"prettier-plugin-sort-json": "^4.0.0",
|
||||||
"prettier-plugin-svelte": "^3.2.6",
|
"prettier-plugin-svelte": "^3.2.6",
|
||||||
"rollup-plugin-visualizer": "^5.12.0",
|
"rollup-plugin-visualizer": "^5.12.0",
|
||||||
"svelte": "^5.1.5",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^4.0.0",
|
"svelte-check": "^4.0.0",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tslib": "^2.6.2",
|
"tslib": "^2.6.2",
|
||||||
@@ -79,9 +79,9 @@
|
|||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"socket.io-client": "~4.7.5",
|
"socket.io-client": "^4.7.4",
|
||||||
"svelte-gestures": "^5.0.4",
|
"svelte-gestures": "^5.0.4",
|
||||||
"svelte-i18n": "^4.0.1",
|
"svelte-i18n": "^4.0.0",
|
||||||
"svelte-local-storage-store": "^0.6.4",
|
"svelte-local-storage-store": "^0.6.4",
|
||||||
"svelte-maplibre": "^0.9.13",
|
"svelte-maplibre": "^0.9.13",
|
||||||
"thumbhash": "^0.1.1"
|
"thumbhash": "^0.1.1"
|
||||||
|
|||||||
@@ -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;
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
@@ -14,14 +14,14 @@
|
|||||||
mdiPlay,
|
mdiPlay,
|
||||||
mdiSelectionSearch,
|
mdiSelectionSearch,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import { type Component } from 'svelte';
|
import { type ComponentType } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import JobTileButton from './job-tile-button.svelte';
|
import JobTileButton from './job-tile-button.svelte';
|
||||||
import JobTileStatus from './job-tile-status.svelte';
|
import JobTileStatus from './job-tile-status.svelte';
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let subtitle: string | undefined;
|
export let subtitle: string | undefined;
|
||||||
export let description: Component | undefined;
|
export let description: ComponentType | undefined;
|
||||||
export let jobCounts: JobCountsDto;
|
export let jobCounts: JobCountsDto;
|
||||||
export let queueStatus: QueueStatusDto;
|
export let queueStatus: QueueStatusDto;
|
||||||
export let icon: string;
|
export let icon: string;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
mdiTagFaces,
|
mdiTagFaces,
|
||||||
mdiVideo,
|
mdiVideo,
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import type { Component } from 'svelte';
|
import type { ComponentType } from 'svelte';
|
||||||
import JobTile from './job-tile.svelte';
|
import JobTile from './job-tile.svelte';
|
||||||
import StorageMigrationDescription from './storage-migration-description.svelte';
|
import StorageMigrationDescription from './storage-migration-description.svelte';
|
||||||
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
interface JobDetails {
|
interface JobDetails {
|
||||||
title: string;
|
title: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
description?: Component;
|
description?: ComponentType;
|
||||||
allText?: string;
|
allText?: string;
|
||||||
refreshText?: string;
|
refreshText?: string;
|
||||||
missingText: string;
|
missingText: string;
|
||||||
@@ -56,7 +56,6 @@
|
|||||||
await handleCommand(jobId, dto);
|
await handleCommand(jobId, dto);
|
||||||
};
|
};
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
|
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
|
||||||
[JobName.ThumbnailGeneration]: {
|
[JobName.ThumbnailGeneration]: {
|
||||||
icon: mdiFileJpgBox,
|
icon: mdiFileJpgBox,
|
||||||
|
|||||||
@@ -343,6 +343,15 @@
|
|||||||
subtitle={$t('admin.transcoding_advanced_options_description')}
|
subtitle={$t('admin.transcoding_advanced_options_description')}
|
||||||
>
|
>
|
||||||
<div class="ml-4 mt-4 flex flex-col gap-4">
|
<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
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label={$t('admin.transcoding_max_b_frames')}
|
label={$t('admin.transcoding_max_b_frames')}
|
||||||
|
|||||||
@@ -87,7 +87,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: {
|
$: {
|
||||||
if (selectedGroupOption.id === AlbumGroupBy.None) {
|
if (selectedGroupOption.id === AlbumGroupBy.None) {
|
||||||
groupIcon = mdiFolderRemoveOutline;
|
groupIcon = mdiFolderRemoveOutline;
|
||||||
@@ -97,10 +96,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
|
$: sortIcon = $albumViewSettings.sortOrder === SortOrder.Desc ? mdiArrowDownThin : mdiArrowUpThin;
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: albumFilterNames = ((): Record<AlbumFilter, string> => {
|
$: albumFilterNames = ((): Record<AlbumFilter, string> => {
|
||||||
return {
|
return {
|
||||||
[AlbumFilter.All]: $t('all'),
|
[AlbumFilter.All]: $t('all'),
|
||||||
@@ -109,7 +106,6 @@
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
||||||
return {
|
return {
|
||||||
[AlbumSortBy.Title]: $t('sort_title'),
|
[AlbumSortBy.Title]: $t('sort_title'),
|
||||||
@@ -121,7 +117,6 @@
|
|||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
|
$: albumGroupByNames = ((): Record<AlbumGroupBy, string> => {
|
||||||
return {
|
return {
|
||||||
[AlbumGroupBy.None]: $t('group_no'),
|
[AlbumGroupBy.None]: $t('group_no'),
|
||||||
|
|||||||
@@ -135,7 +135,6 @@
|
|||||||
let isOpen = false;
|
let isOpen = false;
|
||||||
|
|
||||||
// Step 1: Filter between Owned and Shared albums, or both.
|
// Step 1: Filter between Owned and Shared albums, or both.
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: {
|
$: {
|
||||||
switch (userSettings.filter) {
|
switch (userSettings.filter) {
|
||||||
case AlbumFilter.Owned: {
|
case AlbumFilter.Owned: {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
$albumViewSettings.sortOrder = option.defaultOrder;
|
$albumViewSettings.sortOrder = option.defaultOrder;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
$: albumSortByNames = ((): Record<AlbumSortBy, string> => {
|
||||||
return {
|
return {
|
||||||
[AlbumSortBy.Title]: $t('sort_title'),
|
[AlbumSortBy.Title]: $t('sort_title'),
|
||||||
|
|||||||
@@ -293,7 +293,7 @@
|
|||||||
class="h-[18px] {disabled
|
class="h-[18px] {disabled
|
||||||
? 'cursor-not-allowed'
|
? 'cursor-not-allowed'
|
||||||
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
: ''} w-full max-h-56 pr-2 items-center overflow-y-auto leading-4 outline-none resize-none bg-gray-200"
|
||||||
></textarea>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{#if isSendingMessage}
|
{#if isSendingMessage}
|
||||||
<div class="flex items-end place-items-center pb-2 ml-0">
|
<div class="flex items-end place-items-center pb-2 ml-0">
|
||||||
|
|||||||
@@ -15,31 +15,13 @@ describe('AssetViewerNavBar component', () => {
|
|||||||
showShareButton: false,
|
showShareButton: false,
|
||||||
onZoomImage: () => {},
|
onZoomImage: () => {},
|
||||||
onCopyImage: () => {},
|
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(() => {
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
resetSavedUser();
|
resetSavedUser();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shows back button', () => {
|
it('shows back button', () => {
|
||||||
const asset = assetFactory.build({ isTrashed: false });
|
const asset = assetFactory.build({ isTrashed: false });
|
||||||
const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });
|
const { getByTitle } = render(AssetViewerNavBar, { asset, ...additionalProps });
|
||||||
|
|||||||
@@ -61,7 +61,6 @@
|
|||||||
|
|
||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
$: isOwner = $user && asset.ownerId === $user?.id;
|
$: isOwner = $user && asset.ownerId === $user?.id;
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
$: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline;
|
||||||
// $: showEditorButton =
|
// $: showEditorButton =
|
||||||
// isOwner &&
|
// isOwner &&
|
||||||
|
|||||||
@@ -598,7 +598,7 @@
|
|||||||
|
|
||||||
{#if stackedAsset.id == asset.id}
|
{#if stackedAsset.id == asset.id}
|
||||||
<div class="w-full flex place-items-center place-content-center">
|
<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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
<div class="border border-t-0 border-red-400 bg-red-100 px-4 py-3 text-red-700">
|
||||||
<p>
|
<p>
|
||||||
{#if $user?.isAdmin}
|
{#if $user?.isAdmin}
|
||||||
{$t('admin.asset_offline_description')}
|
<p>{$t('admin.asset_offline_description')}</p>
|
||||||
{:else}
|
{:else}
|
||||||
{$t('asset_offline_description')}
|
{$t('asset_offline_description')}
|
||||||
{/if}
|
{/if}
|
||||||
@@ -345,45 +345,43 @@
|
|||||||
</Portal>
|
</Portal>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<div class="flex gap-4 py-4">
|
{#if asset.exifInfo?.fileSizeInByte}
|
||||||
<div><Icon path={mdiImageOutline} size="24" /></div>
|
<div class="flex gap-4 py-4">
|
||||||
|
<div><Icon path={mdiImageOutline} size="24" /></div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<p class="break-all flex place-items-center gap-2">
|
<p class="break-all flex place-items-center gap-2">
|
||||||
{asset.originalFileName}
|
{asset.originalFileName}
|
||||||
{#if isOwner}
|
{#if isOwner}
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
icon={mdiInformationOutline}
|
icon={mdiInformationOutline}
|
||||||
title={$t('show_file_location')}
|
title={$t('show_file_location')}
|
||||||
size="16"
|
size="16"
|
||||||
padding="2"
|
padding="2"
|
||||||
on:click={toggleAssetPath}
|
on:click={toggleAssetPath}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
|
||||||
{#if showAssetPath}
|
|
||||||
<p class="text-xs opacity-50 break-all pb-2" transition:slide={{ duration: 250 }}>
|
|
||||||
{asset.originalPath}
|
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
|
||||||
{#if (asset.exifInfo?.exifImageHeight && asset.exifInfo?.exifImageWidth) || asset.exifInfo?.fileSizeInByte}
|
|
||||||
<div class="flex gap-2 text-sm">
|
<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)}
|
{#if getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)}
|
||||||
<p>
|
<p>
|
||||||
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
{getMegapixel(asset.exifInfo.exifImageHeight, asset.exifInfo.exifImageWidth)} MP
|
||||||
</p>
|
</p>
|
||||||
{@const { width, height } = getDimensions(asset.exifInfo)}
|
|
||||||
<p>{width} x {height}</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
{@const { width, height } = getDimensions(asset.exifInfo)}
|
||||||
|
<p>{width} x {height}</p>
|
||||||
{/if}
|
{/if}
|
||||||
{#if asset.exifInfo?.fileSizeInByte}
|
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
|
||||||
<p>{getByteUnitString(asset.exifInfo.fileSizeInByte, $locale)}</p>
|
|
||||||
{/if}
|
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{#if showAssetPath}
|
||||||
|
<p class="text-xs opacity-50 break-all" transition:slide={{ duration: 250 }}>
|
||||||
|
{asset.originalPath}
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
|
{#if asset.exifInfo?.make || asset.exifInfo?.model || asset.exifInfo?.fNumber}
|
||||||
<div class="flex gap-4 py-4">
|
<div class="flex gap-4 py-4">
|
||||||
|
|||||||
@@ -32,7 +32,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="flex place-items-center gap-2">
|
<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] 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>
|
</div>
|
||||||
<p class="min-w-[4em] whitespace-nowrap text-right">
|
<p class="min-w-[4em] whitespace-nowrap text-right">
|
||||||
<span class="text-immich-primary">
|
<span class="text-immich-primary">
|
||||||
|
|||||||
@@ -23,7 +23,6 @@
|
|||||||
export let onClose: () => void;
|
export let onClose: () => void;
|
||||||
|
|
||||||
let selectedType: string = editTypes[0].name;
|
let selectedType: string = editTypes[0].name;
|
||||||
// svelte-ignore reactive_declaration_non_reactive_property
|
|
||||||
$: selectedTypeObj = editTypes.find((t) => t.name === selectedType) || editTypes[0];
|
$: selectedTypeObj = editTypes.find((t) => t.name === selectedType) || editTypes[0];
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user