Compare commits
26 Commits
v1.106.2
...
feat/fast-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e04d25d8f5 | ||
|
|
c642150b85 | ||
|
|
a8a7d29891 | ||
|
|
67e98ed313 | ||
|
|
47ef48e3c2 | ||
|
|
376feadb76 | ||
|
|
3d82005797 | ||
|
|
10aa00af21 | ||
|
|
1f8bdcdce7 | ||
|
|
98ebfc22f8 | ||
|
|
032b99fe93 | ||
|
|
07156135c2 | ||
|
|
9dbf5db72e | ||
|
|
52170423be | ||
|
|
ae095baad3 | ||
|
|
f99f289f74 | ||
|
|
476eea44df | ||
|
|
e84657192c | ||
|
|
5dda5d93f5 | ||
|
|
6260caf649 | ||
|
|
9e5c52b7b7 | ||
|
|
0e1311e3d3 | ||
|
|
216cca4383 | ||
|
|
cdc98de848 | ||
|
|
126cbeabe8 | ||
|
|
2e0c6f6fff |
6
cli/package-lock.json
generated
6
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
interval: 5m
|
interval: 5m
|
||||||
start_interval: 30s
|
start_interval: 30s
|
||||||
start_period: 5m
|
start_period: 5m
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
interval: 5m
|
interval: 5m
|
||||||
start_interval: 30s
|
start_interval: 30s
|
||||||
start_period: 5m
|
start_period: 5m
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ services:
|
|||||||
volumes:
|
volumes:
|
||||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
interval: 5m
|
interval: 5m
|
||||||
start_interval: 30s
|
start_interval: 30s
|
||||||
start_period: 5m
|
start_period: 5m
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ if [ -n "${quota:-}" ] && [ -n "${period:-}" ]; then
|
|||||||
cpus=1
|
cpus=1
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
cpus=$(grep -c processor /proc/cpuinfo)
|
cpus=$(grep -c ^processor /proc/cpuinfo)
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "$cpus"
|
echo "$cpus"
|
||||||
|
|||||||
@@ -60,17 +60,17 @@ For RKMPP to work:
|
|||||||
#### Basic Setup
|
#### Basic Setup
|
||||||
|
|
||||||
1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||||
2. In the `docker-compose.yml` under `immich-microservices`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
2. In the `docker-compose.yml` under `immich-server`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
||||||
|
|
||||||
- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
|
- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
|
||||||
|
|
||||||
3. Redeploy the `immich-microservices` container with these updated settings.
|
3. Redeploy the `immich-server` container with these updated settings.
|
||||||
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
|
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
|
||||||
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
|
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
|
||||||
|
|
||||||
#### Single Compose File
|
#### Single Compose File
|
||||||
|
|
||||||
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly.
|
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly.
|
||||||
|
|
||||||
For example, the `qsv` section in this file is:
|
For example, the `qsv` section in this file is:
|
||||||
|
|
||||||
@@ -79,21 +79,22 @@ devices:
|
|||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
```
|
```
|
||||||
|
|
||||||
You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`:
|
You can add this to the `immich-server` service instead of extending from `hwaccel.transcoding.yml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
immich-microservices:
|
immich-server:
|
||||||
container_name: immich_microservices
|
container_name: immich_server
|
||||||
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
|
||||||
# Note the lack of an `extends` section
|
# Note the lack of an `extends` section
|
||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
command: ['start.sh', 'microservices']
|
|
||||||
volumes:
|
volumes:
|
||||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
|
ports:
|
||||||
|
- 2283:3001
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
- redis
|
||||||
- database
|
- database
|
||||||
|
|||||||
4
docs/static/archived-versions.json
vendored
4
docs/static/archived-versions.json
vendored
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.106.3",
|
||||||
|
"url": "https://v1.106.3.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.106.2",
|
"label": "v1.106.2",
|
||||||
"url": "https://v1.106.2.archive.immich.app"
|
"url": "https://v1.106.2.archive.immich.app"
|
||||||
|
|||||||
8
e2e/package-lock.json
generated
8
e2e/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@immich/cli": "file:../cli",
|
"@immich/cli": "file:../cli",
|
||||||
@@ -39,7 +39,7 @@
|
|||||||
},
|
},
|
||||||
"../cli": {
|
"../cli": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.2",
|
"version": "2.2.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -81,7 +81,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1148,4 +1148,29 @@ describe('/asset', () => {
|
|||||||
expect(video.checksum).toStrictEqual(checksum);
|
expect(video.checksum).toStrictEqual(checksum);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /assets/exist', () => {
|
||||||
|
it('ignores invalid deviceAssetIds', async () => {
|
||||||
|
const response = await utils.checkExistingAssets(user1.accessToken, {
|
||||||
|
deviceId: 'test-assets-exist',
|
||||||
|
deviceAssetIds: ['invalid', 'INVALID'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.existingIds).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns the IDs of existing assets', async () => {
|
||||||
|
await utils.createAsset(user1.accessToken, {
|
||||||
|
deviceId: 'test-assets-exist',
|
||||||
|
deviceAssetId: 'test-asset-0',
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await utils.checkExistingAssets(user1.accessToken, {
|
||||||
|
deviceId: 'test-assets-exist',
|
||||||
|
deviceAssetIds: ['test-asset-0'],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(response.existingIds).toEqual(['test-asset-0']);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
AssetMediaCreateDto,
|
AssetMediaCreateDto,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
|
CheckExistingAssetsDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
SharedLinkCreateDto,
|
SharedLinkCreateDto,
|
||||||
UserAdminCreateDto,
|
UserAdminCreateDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
|
checkExistingAssets,
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
createLibrary,
|
createLibrary,
|
||||||
@@ -374,6 +376,9 @@ export const utils = {
|
|||||||
|
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
|
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
|
||||||
|
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
|
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
|
||||||
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "machine-learning"
|
name = "machine-learning"
|
||||||
version = "1.106.2"
|
version = "1.106.3"
|
||||||
description = ""
|
description = ""
|
||||||
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ platform :android do
|
|||||||
task: 'bundle',
|
task: 'bundle',
|
||||||
build_type: 'Release',
|
build_type: 'Release',
|
||||||
properties: {
|
properties: {
|
||||||
"android.injected.version.code" => 142,
|
"android.injected.version.code" => 143,
|
||||||
"android.injected.version.name" => "1.106.2",
|
"android.injected.version.name" => "1.106.3",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
|
||||||
|
|||||||
@@ -5,17 +5,17 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000218">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000381">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="58.982292">
|
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="52.832426">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.253303">
|
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="27.616558">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -383,7 +383,7 @@
|
|||||||
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 = 159;
|
CURRENT_PROJECT_VERSION = 160;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -525,7 +525,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 159;
|
CURRENT_PROJECT_VERSION = 160;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
@@ -553,7 +553,7 @@
|
|||||||
CLANG_ENABLE_MODULES = YES;
|
CLANG_ENABLE_MODULES = YES;
|
||||||
CODE_SIGN_IDENTITY = "Apple Development";
|
CODE_SIGN_IDENTITY = "Apple Development";
|
||||||
CODE_SIGN_STYLE = Automatic;
|
CODE_SIGN_STYLE = Automatic;
|
||||||
CURRENT_PROJECT_VERSION = 159;
|
CURRENT_PROJECT_VERSION = 160;
|
||||||
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
DEVELOPMENT_TEAM = 2F67MQ8R79;
|
||||||
ENABLE_BITCODE = NO;
|
ENABLE_BITCODE = NO;
|
||||||
INFOPLIST_FILE = Runner/Info.plist;
|
INFOPLIST_FILE = Runner/Info.plist;
|
||||||
|
|||||||
@@ -58,11 +58,11 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>APPL</string>
|
<string>APPL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>1.106.1</string>
|
<string>1.106.3</string>
|
||||||
<key>CFBundleSignature</key>
|
<key>CFBundleSignature</key>
|
||||||
<string>????</string>
|
<string>????</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>159</string>
|
<string>160</string>
|
||||||
<key>FLTEnableImpeller</key>
|
<key>FLTEnableImpeller</key>
|
||||||
<true />
|
<true />
|
||||||
<key>ITSAppUsesNonExemptEncryption</key>
|
<key>ITSAppUsesNonExemptEncryption</key>
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ platform :ios do
|
|||||||
desc "iOS Beta"
|
desc "iOS Beta"
|
||||||
lane :beta do
|
lane :beta do
|
||||||
increment_version_number(
|
increment_version_number(
|
||||||
version_number: "1.106.2"
|
version_number: "1.106.3"
|
||||||
)
|
)
|
||||||
increment_build_number(
|
increment_build_number(
|
||||||
build_number: latest_testflight_build_number + 1,
|
build_number: latest_testflight_build_number + 1,
|
||||||
|
|||||||
@@ -5,32 +5,32 @@
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000242">
|
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000491">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.191393">
|
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="39.414297">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="4.210274">
|
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="32.521647">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.183895">
|
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.511733">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="4: build_app" time="153.101993">
|
<testcase classname="fastlane.lanes" name="4: build_app" time="202.628277">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|
||||||
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="76.170022">
|
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.861852">
|
||||||
|
|
||||||
</testcase>
|
</testcase>
|
||||||
|
|
||||||
|
|||||||
@@ -75,9 +75,7 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
// Also sets the error if there is an error in the playback
|
// Also sets the error if there is an error in the playback
|
||||||
void updateVideoPlayback() {
|
void updateVideoPlayback() {
|
||||||
final videoPlayback = VideoPlaybackValue.fromController(controller);
|
final videoPlayback = VideoPlaybackValue.fromController(controller);
|
||||||
if (!loopVideo) {
|
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
||||||
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
|
|
||||||
}
|
|
||||||
final state = videoPlayback.state;
|
final state = videoPlayback.state;
|
||||||
|
|
||||||
// Enable the WakeLock while the video is playing
|
// Enable the WakeLock while the video is playing
|
||||||
@@ -110,7 +108,9 @@ class VideoViewerPage extends HookConsumerWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Subscribes to listener
|
// Subscribes to listener
|
||||||
controller.addListener(updateVideoPlayback);
|
Future.microtask(() {
|
||||||
|
controller.addListener(updateVideoPlayback);
|
||||||
|
});
|
||||||
return () {
|
return () {
|
||||||
// Removes listener when we dispose
|
// Removes listener when we dispose
|
||||||
controller.removeListener(updateVideoPlayback);
|
controller.removeListener(updateVideoPlayback);
|
||||||
|
|||||||
@@ -50,8 +50,19 @@ class AssetNotifier extends StateNotifier<bool> {
|
|||||||
await clearAssetsAndAlbums(_db);
|
await clearAssetsAndAlbums(_db);
|
||||||
log.info("Manual refresh requested, cleared assets and albums from db");
|
log.info("Manual refresh requested, cleared assets and albums from db");
|
||||||
}
|
}
|
||||||
|
|
||||||
final bool changedUsers = await _userService.refreshUsers();
|
final bool changedUsers = await _userService.refreshUsers();
|
||||||
|
|
||||||
|
final assetCount = await _db.assets.count();
|
||||||
|
|
||||||
|
if (assetCount == 0) {
|
||||||
|
debugPrint("First sync, refreshing all assets");
|
||||||
|
await _assetService.refreshRemoteAssets(firstSync: true);
|
||||||
|
debugPrint("First sync, DONE refreshing all assets");
|
||||||
|
}
|
||||||
|
debugPrint("First sync, CONTINUE refreshing all assets");
|
||||||
final bool newRemote = await _assetService.refreshRemoteAssets();
|
final bool newRemote = await _assetService.refreshRemoteAssets();
|
||||||
|
|
||||||
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
final bool newLocal = await _albumService.refreshDeviceAlbums();
|
||||||
debugPrint(
|
debugPrint(
|
||||||
"changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal",
|
"changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal",
|
||||||
|
|||||||
@@ -93,4 +93,18 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
|
|||||||
pause: !state.pause,
|
pause: !state.pause,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void restart() {
|
||||||
|
state = VideoPlaybackControls(
|
||||||
|
position: 0,
|
||||||
|
mute: state.mute,
|
||||||
|
pause: true,
|
||||||
|
);
|
||||||
|
|
||||||
|
state = VideoPlaybackControls(
|
||||||
|
position: 0,
|
||||||
|
mute: state.mute,
|
||||||
|
pause: false,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class AssetService {
|
|||||||
|
|
||||||
/// Checks the server for updated assets and updates the local database if
|
/// Checks the server for updated assets and updates the local database if
|
||||||
/// required. Returns `true` if there were any changes.
|
/// required. Returns `true` if there were any changes.
|
||||||
Future<bool> refreshRemoteAssets() async {
|
Future<bool> refreshRemoteAssets({bool firstSync = false}) async {
|
||||||
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
|
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
|
||||||
final List<User> syncedUsers = syncedUserIds.isEmpty
|
final List<User> syncedUsers = syncedUserIds.isEmpty
|
||||||
? []
|
? []
|
||||||
@@ -57,6 +57,7 @@ class AssetService {
|
|||||||
getChangedAssets: _getRemoteAssetChanges,
|
getChangedAssets: _getRemoteAssetChanges,
|
||||||
loadAssets: _getRemoteAssets,
|
loadAssets: _getRemoteAssets,
|
||||||
refreshUsers: _userService.getUsersFromServer,
|
refreshUsers: _userService.getUsersFromServer,
|
||||||
|
firstSync: firstSync,
|
||||||
);
|
);
|
||||||
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
|
||||||
return changes;
|
return changes;
|
||||||
@@ -97,8 +98,12 @@ class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `null` if the server state did not change, else list of assets
|
/// Returns `null` if the server state did not change, else list of assets
|
||||||
Future<List<Asset>?> _getRemoteAssets(User user, DateTime until) async {
|
Future<List<Asset>?> _getRemoteAssets(
|
||||||
const int chunkSize = 10000;
|
User user,
|
||||||
|
DateTime until,
|
||||||
|
bool firstSync,
|
||||||
|
) async {
|
||||||
|
int chunkSize = firstSync ? 1000 : 10000;
|
||||||
try {
|
try {
|
||||||
final List<Asset> allAssets = [];
|
final List<Asset> allAssets = [];
|
||||||
String? lastId;
|
String? lastId;
|
||||||
@@ -120,6 +125,11 @@ class AssetService {
|
|||||||
allAssets.addAll(assets.map(Asset.remote));
|
allAssets.addAll(assets.map(Asset.remote));
|
||||||
if (assets.length != chunkSize) break;
|
if (assets.length != chunkSize) break;
|
||||||
lastId = assets.last.id;
|
lastId = assets.last.id;
|
||||||
|
|
||||||
|
if (firstSync) {
|
||||||
|
// first sync only loads the first chunk
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return allAssets;
|
return allAssets;
|
||||||
} catch (error, stack) {
|
} catch (error, stack) {
|
||||||
|
|||||||
@@ -46,14 +46,18 @@ class SyncService {
|
|||||||
List<User> users,
|
List<User> users,
|
||||||
DateTime since,
|
DateTime since,
|
||||||
) getChangedAssets,
|
) getChangedAssets,
|
||||||
required FutureOr<List<Asset>?> Function(User user, DateTime until)
|
required FutureOr<List<Asset>?> Function(
|
||||||
loadAssets,
|
User user,
|
||||||
|
DateTime until,
|
||||||
|
bool firstSync,
|
||||||
|
) loadAssets,
|
||||||
required FutureOr<List<User>?> Function() refreshUsers,
|
required FutureOr<List<User>?> Function() refreshUsers,
|
||||||
|
required bool firstSync,
|
||||||
}) =>
|
}) =>
|
||||||
_lock.run(
|
_lock.run(
|
||||||
() async =>
|
() async =>
|
||||||
await _syncRemoteAssetChanges(users, getChangedAssets) ??
|
await _syncRemoteAssetChanges(users, getChangedAssets) ??
|
||||||
await _syncRemoteAssetsFull(refreshUsers, loadAssets),
|
await _syncRemoteAssetsFull(refreshUsers, loadAssets, firstSync),
|
||||||
);
|
);
|
||||||
|
|
||||||
/// Syncs remote albums to the database
|
/// Syncs remote albums to the database
|
||||||
@@ -212,7 +216,9 @@ class SyncService {
|
|||||||
/// Syncs assets by loading and comparing all assets from the server.
|
/// Syncs assets by loading and comparing all assets from the server.
|
||||||
Future<bool> _syncRemoteAssetsFull(
|
Future<bool> _syncRemoteAssetsFull(
|
||||||
FutureOr<List<User>?> Function() refreshUsers,
|
FutureOr<List<User>?> Function() refreshUsers,
|
||||||
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
|
FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
|
||||||
|
loadAssets,
|
||||||
|
bool firstSync,
|
||||||
) async {
|
) async {
|
||||||
final serverUsers = await refreshUsers();
|
final serverUsers = await refreshUsers();
|
||||||
if (serverUsers == null) {
|
if (serverUsers == null) {
|
||||||
@@ -228,17 +234,19 @@ class SyncService {
|
|||||||
.findAll();
|
.findAll();
|
||||||
bool changes = false;
|
bool changes = false;
|
||||||
for (User u in users) {
|
for (User u in users) {
|
||||||
changes |= await _syncRemoteAssetsForUser(u, loadAssets);
|
changes |= await _syncRemoteAssetsForUser(u, loadAssets, firstSync);
|
||||||
}
|
}
|
||||||
return changes;
|
return changes;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> _syncRemoteAssetsForUser(
|
Future<bool> _syncRemoteAssetsForUser(
|
||||||
User user,
|
User user,
|
||||||
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
|
FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
|
||||||
|
loadAssets,
|
||||||
|
bool firstSync,
|
||||||
) async {
|
) async {
|
||||||
final DateTime now = DateTime.now().toUtc();
|
final DateTime now = DateTime.now().toUtc();
|
||||||
final List<Asset>? remote = await loadAssets(user, now);
|
final List<Asset>? remote = await loadAssets(user, now, firstSync);
|
||||||
if (remote == null) {
|
if (remote == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,6 +64,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
|
|||||||
final state = ref.read(videoPlaybackValueProvider).state;
|
final state = ref.read(videoPlaybackValueProvider).state;
|
||||||
if (state == VideoPlaybackState.playing) {
|
if (state == VideoPlaybackState.playing) {
|
||||||
ref.read(videoPlayerControlsProvider.notifier).pause();
|
ref.read(videoPlayerControlsProvider.notifier).pause();
|
||||||
|
} else if (state == VideoPlaybackState.completed) {
|
||||||
|
ref.read(videoPlayerControlsProvider.notifier).restart();
|
||||||
} else {
|
} else {
|
||||||
ref.read(videoPlayerControlsProvider.notifier).play();
|
ref.read(videoPlayerControlsProvider.notifier).play();
|
||||||
}
|
}
|
||||||
|
|||||||
2
mobile/openapi/README.md
generated
2
mobile/openapi/README.md
generated
@@ -3,7 +3,7 @@ Immich API
|
|||||||
|
|
||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.106.2
|
- API version: 1.106.3
|
||||||
- Generator version: 7.5.0
|
- Generator version: 7.5.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ class DuplicateDetectionConfig {
|
|||||||
|
|
||||||
return DuplicateDetectionConfig(
|
return DuplicateDetectionConfig(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
maxDistance: mapValueOfType<double>(json, r'maxDistance')!,
|
maxDistance: (mapValueOfType<num>(json, r'maxDistance')!).toDouble(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -74,9 +74,9 @@ class FacialRecognitionConfig {
|
|||||||
|
|
||||||
return FacialRecognitionConfig(
|
return FacialRecognitionConfig(
|
||||||
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
enabled: mapValueOfType<bool>(json, r'enabled')!,
|
||||||
maxDistance: mapValueOfType<double>(json, r'maxDistance')!,
|
maxDistance: (mapValueOfType<num>(json, r'maxDistance')!).toDouble(),
|
||||||
minFaces: mapValueOfType<int>(json, r'minFaces')!,
|
minFaces: mapValueOfType<int>(json, r'minFaces')!,
|
||||||
minScore: mapValueOfType<double>(json, r'minScore')!,
|
minScore: (mapValueOfType<num>(json, r'minScore')!).toDouble(),
|
||||||
modelName: mapValueOfType<String>(json, r'modelName')!,
|
modelName: mapValueOfType<String>(json, r'modelName')!,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ class ServerStorageResponseDto {
|
|||||||
diskAvailableRaw: mapValueOfType<int>(json, r'diskAvailableRaw')!,
|
diskAvailableRaw: mapValueOfType<int>(json, r'diskAvailableRaw')!,
|
||||||
diskSize: mapValueOfType<String>(json, r'diskSize')!,
|
diskSize: mapValueOfType<String>(json, r'diskSize')!,
|
||||||
diskSizeRaw: mapValueOfType<int>(json, r'diskSizeRaw')!,
|
diskSizeRaw: mapValueOfType<int>(json, r'diskSizeRaw')!,
|
||||||
diskUsagePercentage: mapValueOfType<double>(json, r'diskUsagePercentage')!,
|
diskUsagePercentage: (mapValueOfType<num>(json, r'diskUsagePercentage')!).toDouble(),
|
||||||
diskUse: mapValueOfType<String>(json, r'diskUse')!,
|
diskUse: mapValueOfType<String>(json, r'diskUse')!,
|
||||||
diskUseRaw: mapValueOfType<int>(json, r'diskUseRaw')!,
|
diskUseRaw: mapValueOfType<int>(json, r'diskUseRaw')!,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: immich_mobile
|
|||||||
description: Immich - selfhosted backup media file on mobile phone
|
description: Immich - selfhosted backup media file on mobile phone
|
||||||
|
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
version: 1.106.2+142
|
version: 1.106.3+143
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=3.3.0 <4.0.0'
|
sdk: '>=3.3.0 <4.0.0'
|
||||||
|
|||||||
@@ -78,8 +78,9 @@ void main() {
|
|||||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c1, isFalse);
|
expect(c1, isFalse);
|
||||||
expect(db.assets.countSync(), 5);
|
expect(db.assets.countSync(), 5);
|
||||||
@@ -99,8 +100,9 @@ void main() {
|
|||||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c1, isTrue);
|
expect(c1, isTrue);
|
||||||
expect(db.assets.countSync(), 7);
|
expect(db.assets.countSync(), 7);
|
||||||
@@ -120,16 +122,18 @@ void main() {
|
|||||||
final bool c1 = await s.syncRemoteAssetsToDb(
|
final bool c1 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c1, isTrue);
|
expect(c1, isTrue);
|
||||||
expect(db.assets.countSync(), 8);
|
expect(db.assets.countSync(), 8);
|
||||||
final bool c2 = await s.syncRemoteAssetsToDb(
|
final bool c2 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c2, isFalse);
|
expect(c2, isFalse);
|
||||||
expect(db.assets.countSync(), 8);
|
expect(db.assets.countSync(), 8);
|
||||||
@@ -137,8 +141,9 @@ void main() {
|
|||||||
final bool c3 = await s.syncRemoteAssetsToDb(
|
final bool c3 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c3, isTrue);
|
expect(c3, isTrue);
|
||||||
expect(db.assets.countSync(), 7);
|
expect(db.assets.countSync(), 7);
|
||||||
@@ -147,8 +152,9 @@ void main() {
|
|||||||
final bool c4 = await s.syncRemoteAssetsToDb(
|
final bool c4 = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: _failDiff,
|
getChangedAssets: _failDiff,
|
||||||
loadAssets: (u, d) => remoteAssets,
|
loadAssets: (u, d, firstSync) => remoteAssets,
|
||||||
refreshUsers: () => [owner],
|
refreshUsers: () => [owner],
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c4, isTrue);
|
expect(c4, isTrue);
|
||||||
expect(db.assets.countSync(), 9);
|
expect(db.assets.countSync(), 9);
|
||||||
@@ -166,8 +172,9 @@ void main() {
|
|||||||
final bool c = await s.syncRemoteAssetsToDb(
|
final bool c = await s.syncRemoteAssetsToDb(
|
||||||
users: [owner],
|
users: [owner],
|
||||||
getChangedAssets: (user, since) async => (toUpsert, toDelete),
|
getChangedAssets: (user, since) async => (toUpsert, toDelete),
|
||||||
loadAssets: (user, date) => throw Exception(),
|
loadAssets: (user, date, firstSync) => throw Exception(),
|
||||||
refreshUsers: () => throw Exception(),
|
refreshUsers: () => throw Exception(),
|
||||||
|
firstSync: false,
|
||||||
);
|
);
|
||||||
expect(c, isTrue);
|
expect(c, isTrue);
|
||||||
expect(db.assets.countSync(), 6);
|
expect(db.assets.countSync(), 6);
|
||||||
|
|||||||
@@ -6735,7 +6735,7 @@
|
|||||||
"info": {
|
"info": {
|
||||||
"title": "Immich",
|
"title": "Immich",
|
||||||
"description": "Immich API",
|
"description": "Immich API",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"contact": {}
|
"contact": {}
|
||||||
},
|
},
|
||||||
"tags": [],
|
"tags": [],
|
||||||
@@ -8146,7 +8146,7 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"maxDistance": {
|
"maxDistance": {
|
||||||
"format": "float",
|
"format": "double",
|
||||||
"maximum": 0.1,
|
"maximum": 0.1,
|
||||||
"minimum": 0.001,
|
"minimum": 0.001,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
@@ -8347,7 +8347,7 @@
|
|||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
"maxDistance": {
|
"maxDistance": {
|
||||||
"format": "float",
|
"format": "double",
|
||||||
"maximum": 2,
|
"maximum": 2,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
@@ -8357,7 +8357,7 @@
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"minScore": {
|
"minScore": {
|
||||||
"format": "float",
|
"format": "double",
|
||||||
"maximum": 1,
|
"maximum": 1,
|
||||||
"minimum": 0,
|
"minimum": 0,
|
||||||
"type": "number"
|
"type": "number"
|
||||||
@@ -9797,7 +9797,7 @@
|
|||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"diskUsagePercentage": {
|
"diskUsagePercentage": {
|
||||||
"format": "float",
|
"format": "double",
|
||||||
"type": "number"
|
"type": "number"
|
||||||
},
|
},
|
||||||
"diskUse": {
|
"diskUse": {
|
||||||
|
|||||||
4
open-api/typescript-sdk/package-lock.json
generated
4
open-api/typescript-sdk/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"description": "Auto-generated TypeScript SDK for the Immich API",
|
"description": "Auto-generated TypeScript SDK for the Immich API",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./build/index.js",
|
"main": "./build/index.js",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* Immich
|
* Immich
|
||||||
* 1.106.2
|
* 1.106.3
|
||||||
* DO NOT MODIFY - This file has been generated using oazapfts.
|
* DO NOT MODIFY - This file has been generated using oazapfts.
|
||||||
* See https://www.npmjs.com/package/oazapfts
|
* See https://www.npmjs.com/package/oazapfts
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
"schedule": "on tuesday"
|
"schedule": "on tuesday"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"ignorePaths": ["mobile/openapi/pubspec.yaml"],
|
"ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"],
|
||||||
"ignoreDeps": ["http", "intl"],
|
"ignoreDeps": ["http", "intl"],
|
||||||
"labels": ["dependencies", "renovate"]
|
"labels": ["dependencies", "renovate"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# dev build
|
# dev build
|
||||||
FROM ghcr.io/immich-app/base-server-dev:20240604@sha256:bb31fafb1e8fcb4338c2f7a8f424da3d9c5cf6dd6bdb266c54477c795dd07819 as dev
|
FROM ghcr.io/immich-app/base-server-dev:20240611@sha256:2047ec0f857a800675379c65404dfdf4ac02ab7684c759916e3256d3d9566027 as dev
|
||||||
|
|
||||||
RUN apt-get install --no-install-recommends -yqq tini
|
RUN apt-get install --no-install-recommends -yqq tini
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
@@ -41,7 +41,7 @@ RUN npm run build
|
|||||||
|
|
||||||
|
|
||||||
# prod build
|
# prod build
|
||||||
FROM ghcr.io/immich-app/base-server-prod:20240604@sha256:481ea3ee56fb0e130804fec25c124d28477f10f8a01f7d06fb2e3f85c181bbb9
|
FROM ghcr.io/immich-app/base-server-prod:20240611@sha256:efd32a2af6e7ace8bcea1e94115fe95a971fe1d1fef7e667ff6e77364ce51c46
|
||||||
|
|
||||||
WORKDIR /usr/src/app
|
WORKDIR /usr/src/app
|
||||||
ENV NODE_ENV=production \
|
ENV NODE_ENV=production \
|
||||||
|
|||||||
4
server/package-lock.json
generated
4
server/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@nestjs/bullmq": "^10.0.1",
|
"@nestjs/bullmq": "^10.0.1",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich",
|
"name": "immich",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"author": "",
|
"author": "",
|
||||||
"private": true,
|
"private": true,
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ export class StorageCore {
|
|||||||
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: true });
|
||||||
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
||||||
const { checksum } = assetInfo;
|
const { checksum } = assetInfo;
|
||||||
const newChecksum = await this.cryptoRepository.hashFile(newPath);
|
const newChecksum = await this.cryptoRepository.hashFile(newPath);
|
||||||
|
|||||||
@@ -42,8 +42,8 @@ export class SystemConfigCore {
|
|||||||
instance = null;
|
instance = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfig(force = false): Promise<SystemConfig> {
|
async getConfig({ withCache }: { withCache: boolean }): Promise<SystemConfig> {
|
||||||
if (force || !this.config) {
|
if (!withCache || !this.config) {
|
||||||
const lastUpdated = this.lastUpdated;
|
const lastUpdated = this.lastUpdated;
|
||||||
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
|
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
|
||||||
if (lastUpdated === this.lastUpdated) {
|
if (lastUpdated === this.lastUpdated) {
|
||||||
@@ -74,13 +74,13 @@ export class SystemConfigCore {
|
|||||||
|
|
||||||
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
||||||
|
|
||||||
const config = await this.getConfig(true);
|
const config = await this.getConfig({ withCache: false });
|
||||||
this.config$.next(config);
|
this.config$.next(config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
async refreshConfig() {
|
async refreshConfig() {
|
||||||
const newConfig = await this.getConfig(true);
|
const newConfig = await this.getConfig({ withCache: false });
|
||||||
this.config$.next(newConfig);
|
this.config$.next(newConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class DuplicateDetectionConfig extends TaskConfig {
|
|||||||
@Min(0.001)
|
@Min(0.001)
|
||||||
@Max(0.1)
|
@Max(0.1)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiProperty({ type: 'number', format: 'float' })
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
maxDistance!: number;
|
maxDistance!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,14 +30,14 @@ export class FacialRecognitionConfig extends ModelConfig {
|
|||||||
@Min(0)
|
@Min(0)
|
||||||
@Max(1)
|
@Max(1)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiProperty({ type: 'number', format: 'float' })
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
minScore!: number;
|
minScore!: number;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
@Min(0)
|
@Min(0)
|
||||||
@Max(2)
|
@Max(2)
|
||||||
@Type(() => Number)
|
@Type(() => Number)
|
||||||
@ApiProperty({ type: 'number', format: 'float' })
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
maxDistance!: number;
|
maxDistance!: number;
|
||||||
|
|
||||||
@IsNumber()
|
@IsNumber()
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ export class ServerStorageResponseDto {
|
|||||||
@ApiProperty({ type: 'integer', format: 'int64' })
|
@ApiProperty({ type: 'integer', format: 'int64' })
|
||||||
diskAvailableRaw!: number;
|
diskAvailableRaw!: number;
|
||||||
|
|
||||||
@ApiProperty({ type: 'number', format: 'float' })
|
@ApiProperty({ type: 'number', format: 'double' })
|
||||||
diskUsagePercentage!: number;
|
diskUsagePercentage!: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ export interface IAssetRepository {
|
|||||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]>;
|
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]>;
|
||||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||||
getById(
|
getById(
|
||||||
id: string,
|
id: string,
|
||||||
|
|||||||
@@ -155,8 +155,8 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]> {
|
async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]> {
|
||||||
return this.repository.find({
|
const assets = await this.repository.find({
|
||||||
select: { deviceAssetId: true },
|
select: { deviceAssetId: true },
|
||||||
where: {
|
where: {
|
||||||
deviceAssetId: In(deviceAssetIds),
|
deviceAssetId: In(deviceAssetIds),
|
||||||
@@ -165,6 +165,8 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
},
|
},
|
||||||
withDeleted: true,
|
withDeleted: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return assets.map((asset) => asset.deviceAssetId);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByUserId(
|
getByUserId(
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export class MediaRepository implements IMediaRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
|
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
|
||||||
const pipeline = sharp(input, { failOn: 'none' })
|
const pipeline = sharp(input, { failOn: 'none', limitInputPixels: false })
|
||||||
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
|
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
|
||||||
.rotate();
|
.rotate();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||||
import { DefaultExiftoolArgs, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
|
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
|
||||||
import geotz from 'geo-tz';
|
import geotz from 'geo-tz';
|
||||||
import { DummyValue, GenerateSql } from 'src/decorators';
|
import { DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
@@ -21,23 +21,18 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
) {
|
) {
|
||||||
this.logger.setContext(MetadataRepository.name);
|
this.logger.setContext(MetadataRepository.name);
|
||||||
}
|
}
|
||||||
private exiftool: ExifTool = this.initExiftool();
|
|
||||||
|
|
||||||
async teardown() {
|
async teardown() {
|
||||||
await this.exiftool.end();
|
await exiftool.end();
|
||||||
}
|
|
||||||
|
|
||||||
private initExiftool() {
|
|
||||||
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
|
||||||
const exiftoolArgs = ['-api', 'largefilesupport=1', ...DefaultExiftoolArgs];
|
|
||||||
return new ExifTool({ exiftoolArgs });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
readTags(path: string): Promise<ImmichTags | null> {
|
readTags(path: string): Promise<ImmichTags | null> {
|
||||||
return this.exiftool
|
return exiftool
|
||||||
.read(path, undefined, {
|
.read(path, undefined, {
|
||||||
...DefaultReadTaskOptions,
|
...DefaultReadTaskOptions,
|
||||||
|
|
||||||
|
// Enable exiftool LFS to parse metadata for files larger than 2GB.
|
||||||
|
optionalArgs: ['-api', 'largefilesupport=1'],
|
||||||
defaultVideosToUTC: true,
|
defaultVideosToUTC: true,
|
||||||
backfillTimezones: true,
|
backfillTimezones: true,
|
||||||
inferTimezoneFromDatestamps: true,
|
inferTimezoneFromDatestamps: true,
|
||||||
@@ -53,12 +48,12 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
|
||||||
return this.exiftool.extractBinaryTagToBuffer(tagName, path);
|
return exiftool.extractBinaryTagToBuffer(tagName, path);
|
||||||
}
|
}
|
||||||
|
|
||||||
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await this.exiftool.write(path, tags, ['-overwrite_original']);
|
await exiftool.write(path, tags, ['-overwrite_original']);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
this.logger.warn(`Error writing exif data (${path}): ${error}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,14 +277,12 @@ export class AssetMediaService {
|
|||||||
auth: AuthDto,
|
auth: AuthDto,
|
||||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||||
): Promise<CheckExistingAssetsResponseDto> {
|
): Promise<CheckExistingAssetsResponseDto> {
|
||||||
const assets = await this.assetRepository.getByDeviceIds(
|
const existingIds = await this.assetRepository.getByDeviceIds(
|
||||||
auth.user.id,
|
auth.user.id,
|
||||||
checkExistingAssetsDto.deviceId,
|
checkExistingAssetsDto.deviceId,
|
||||||
checkExistingAssetsDto.deviceAssetIds,
|
checkExistingAssetsDto.deviceAssetIds,
|
||||||
);
|
);
|
||||||
return {
|
return { existingIds };
|
||||||
existingIds: assets.map((asset) => asset.id),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||||
|
|||||||
@@ -245,7 +245,7 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
async handleAssetDeletionCheck(): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
const trashedDays = config.trash.enabled ? config.trash.days : 0;
|
||||||
const trashedBefore = DateTime.now()
|
const trashedBefore = DateTime.now()
|
||||||
.minus(Duration.fromObject({ days: trashedDays }))
|
.minus(Duration.fromObject({ days: trashedDays }))
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
async login(dto: LoginCredentialDto, details: LoginDetails) {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!config.passwordLogin.enabled) {
|
if (!config.passwordLogin.enabled) {
|
||||||
throw new UnauthorizedException('Password login has been disabled');
|
throw new UnauthorizedException('Password login has been disabled');
|
||||||
}
|
}
|
||||||
@@ -174,7 +174,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
|
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!config.oauth.enabled) {
|
if (!config.oauth.enabled) {
|
||||||
throw new BadRequestException('OAuth is not enabled');
|
throw new BadRequestException('OAuth is not enabled');
|
||||||
}
|
}
|
||||||
@@ -190,7 +190,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
const profile = await this.getOAuthProfile(config, dto.url);
|
const profile = await this.getOAuthProfile(config, dto.url);
|
||||||
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
|
||||||
let user = await this.userRepository.getByOAuthId(profile.sub);
|
let user = await this.userRepository.getByOAuthId(profile.sub);
|
||||||
@@ -242,7 +242,7 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
|
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
|
||||||
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
const duplicate = await this.userRepository.getByOAuthId(oauthId);
|
||||||
if (duplicate && duplicate.id !== auth.user.id) {
|
if (duplicate && duplicate.id !== auth.user.id) {
|
||||||
@@ -264,7 +264,7 @@ export class AuthService {
|
|||||||
return LOGIN_URL;
|
return LOGIN_URL;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!config.oauth.enabled) {
|
if (!config.oauth.enabled) {
|
||||||
return LOGIN_URL;
|
return LOGIN_URL;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,25 +42,25 @@ export class CliService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async disablePasswordLogin(): Promise<void> {
|
async disablePasswordLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
config.passwordLogin.enabled = false;
|
config.passwordLogin.enabled = false;
|
||||||
await this.configCore.updateConfig(config);
|
await this.configCore.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enablePasswordLogin(): Promise<void> {
|
async enablePasswordLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
config.passwordLogin.enabled = true;
|
config.passwordLogin.enabled = true;
|
||||||
await this.configCore.updateConfig(config);
|
await this.configCore.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async disableOAuthLogin(): Promise<void> {
|
async disableOAuthLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
config.oauth.enabled = false;
|
config.oauth.enabled = false;
|
||||||
await this.configCore.updateConfig(config);
|
await this.configCore.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
async enableOAuthLogin(): Promise<void> {
|
async enableOAuthLogin(): Promise<void> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
config.oauth.enabled = true;
|
config.oauth.enabled = true;
|
||||||
await this.configCore.updateConfig(config);
|
await this.configCore.updateConfig(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export class DuplicateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,7 @@ export class DuplicateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
|
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
if (!isDuplicateDetectionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ export class JobService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init(jobHandlers: Record<JobName, JobHandler>) {
|
async init(jobHandlers: Record<JobName, JobHandler>) {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
for (const queueName of Object.values(QueueName)) {
|
for (const queueName of Object.values(QueueName)) {
|
||||||
let concurrency = 1;
|
let concurrency = 1;
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class LibraryService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
const { watch, scan } = config.library;
|
const { watch, scan } = config.library;
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export class MapService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getMapStyle(theme: 'light' | 'dark') {
|
async getMapStyle(theme: 'light' | 'dark') {
|
||||||
const { map } = await this.configCore.getConfig();
|
const { map } = await this.configCore.getConfig({ withCache: false });
|
||||||
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
const styleUrl = theme === 'dark' ? map.darkStyle : map.lightStyle;
|
||||||
|
|
||||||
if (styleUrl) {
|
if (styleUrl) {
|
||||||
|
|||||||
@@ -149,7 +149,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
async handleAssetMigration({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { image } = await this.configCore.getConfig();
|
const { image } = await this.configCore.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@@ -164,7 +164,7 @@ export class MediaService {
|
|||||||
|
|
||||||
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [{ image }, [asset]] = await Promise.all([
|
const [{ image }, [asset]] = await Promise.all([
|
||||||
this.configCore.getConfig(),
|
this.configCore.getConfig({ withCache: true }),
|
||||||
this.assetRepository.getByIds([id], { exifInfo: true }),
|
this.assetRepository.getByIds([id], { exifInfo: true }),
|
||||||
]);
|
]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -185,7 +185,7 @@ export class MediaService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
|
private async generateThumbnail(asset: AssetEntity, type: GeneratedImageType, format: ImageFormat) {
|
||||||
const { image, ffmpeg } = await this.configCore.getConfig();
|
const { image, ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
||||||
const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
|
const size = type === AssetPathType.PREVIEW ? image.previewSize : image.thumbnailSize;
|
||||||
const path = StorageCore.getImagePath(asset, type, format);
|
const path = StorageCore.getImagePath(asset, type, format);
|
||||||
this.storageCore.ensureFolders(path);
|
this.storageCore.ensureFolders(path);
|
||||||
@@ -237,7 +237,7 @@ export class MediaService {
|
|||||||
|
|
||||||
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGenerateThumbnail({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [{ image }, [asset]] = await Promise.all([
|
const [{ image }, [asset]] = await Promise.all([
|
||||||
this.configCore.getConfig(),
|
this.configCore.getConfig({ withCache: true }),
|
||||||
this.assetRepository.getByIds([id], { exifInfo: true }),
|
this.assetRepository.getByIds([id], { exifInfo: true }),
|
||||||
]);
|
]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
@@ -318,7 +318,7 @@ export class MediaService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { ffmpeg } = await this.configCore.getConfig();
|
const { ffmpeg } = await this.configCore.getConfig({ withCache: true });
|
||||||
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
|
const target = this.getTranscodeTarget(ffmpeg, mainVideoStream, mainAudioStream);
|
||||||
if (target === TranscodeTarget.NONE) {
|
if (target === TranscodeTarget.NONE) {
|
||||||
if (asset.encodedVideoPath) {
|
if (asset.encodedVideoPath) {
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ export class MetadataService {
|
|||||||
this.subscription = this.configCore.config$.subscribe(() => handlePromiseError(this.init(), this.logger));
|
this.subscription = this.configCore.config$.subscribe(() => handlePromiseError(this.init(), this.logger));
|
||||||
}
|
}
|
||||||
|
|
||||||
const { reverseGeocoding } = await this.configCore.getConfig();
|
const { reverseGeocoding } = await this.configCore.getConfig({ withCache: false });
|
||||||
const { enabled } = reverseGeocoding;
|
const { enabled } = reverseGeocoding;
|
||||||
|
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
@@ -333,7 +333,7 @@ export class MetadataService {
|
|||||||
|
|
||||||
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
private async applyReverseGeocoding(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) {
|
||||||
const { latitude, longitude } = exifData;
|
const { latitude, longitude } = exifData;
|
||||||
const { reverseGeocoding } = await this.configCore.getConfig();
|
const { reverseGeocoding } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!reverseGeocoding.enabled || !longitude || !latitude) {
|
if (!reverseGeocoding.enabled || !longitude || !latitude) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export class NotificationService {
|
|||||||
throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error });
|
throw new HttpException('Failed to verify SMTP configuration', HttpStatus.BAD_REQUEST, { cause: error });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig();
|
const { server } = await this.configCore.getConfig({ withCache: false });
|
||||||
const { html, text } = this.notificationRepository.renderEmail({
|
const { html, text } = this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.TEST_EMAIL,
|
template: EmailTemplate.TEST_EMAIL,
|
||||||
data: {
|
data: {
|
||||||
@@ -94,7 +94,7 @@ export class NotificationService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig();
|
const { server } = await this.configCore.getConfig({ withCache: true });
|
||||||
const { html, text } = this.notificationRepository.renderEmail({
|
const { html, text } = this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.WELCOME,
|
template: EmailTemplate.WELCOME,
|
||||||
data: {
|
data: {
|
||||||
@@ -137,7 +137,7 @@ export class NotificationService {
|
|||||||
|
|
||||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig();
|
const { server } = await this.configCore.getConfig({ withCache: false });
|
||||||
const { html, text } = this.notificationRepository.renderEmail({
|
const { html, text } = this.notificationRepository.renderEmail({
|
||||||
template: EmailTemplate.ALBUM_INVITE,
|
template: EmailTemplate.ALBUM_INVITE,
|
||||||
data: {
|
data: {
|
||||||
@@ -179,7 +179,7 @@ export class NotificationService {
|
|||||||
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
|
const recipients = [...album.albumUsers.map((user) => user.user), owner].filter((user) => user.id !== senderId);
|
||||||
const attachment = await this.getAlbumThumbnailAttachment(album);
|
const attachment = await this.getAlbumThumbnailAttachment(album);
|
||||||
|
|
||||||
const { server } = await this.configCore.getConfig();
|
const { server } = await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
for (const recipient of recipients) {
|
for (const recipient of recipients) {
|
||||||
const user = await this.userRepository.get(recipient.id, { withDeleted: false });
|
const user = await this.userRepository.get(recipient.id, { withDeleted: false });
|
||||||
@@ -220,7 +220,7 @@ export class NotificationService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
async handleSendEmail(data: IEmailJob): Promise<JobStatus> {
|
||||||
const { notifications } = await this.configCore.getConfig();
|
const { notifications } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!notifications.smtp.enabled) {
|
if (!notifications.smtp.enabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
async getAll(auth: AuthDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
const people = await this.repository.getAllForUser(auth.user.id, {
|
const people = await this.repository.getAllForUser(auth.user.id, {
|
||||||
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
minimumFaceCount: machineLearning.facialRecognition.minFaces,
|
||||||
withHidden: dto.withHidden || false,
|
withHidden: dto.withHidden || false,
|
||||||
@@ -282,7 +282,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueDetectFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -313,7 +313,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
async handleDetectFaces({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -371,7 +371,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueRecognizeFaces({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueRecognizeFaces({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -402,7 +402,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
async handleRecognizeFaces({ id, deferred }: IDeferrableJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -486,7 +486,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning, image } = await this.configCore.getConfig();
|
const { machineLearning, image } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
async searchSmart(auth: AuthDto, dto: SmartSearchDto): Promise<SearchResponseDto> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
throw new BadRequestException('Smart search is not enabled');
|
throw new BadRequestException('Smart search is not enabled');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export class ServerInfoService {
|
|||||||
|
|
||||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||||
const { reverseGeocoding, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
const { reverseGeocoding, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
||||||
await this.configCore.getConfig();
|
await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
smartSearch: isSmartSearchEnabled(machineLearning),
|
smartSearch: isSmartSearchEnabled(machineLearning),
|
||||||
@@ -85,12 +85,12 @@ export class ServerInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getTheme() {
|
async getTheme() {
|
||||||
const { theme } = await this.configCore.getConfig();
|
const { theme } = await this.configCore.getConfig({ withCache: false });
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getConfig(): Promise<ServerConfigDto> {
|
async getConfig(): Promise<ServerConfigDto> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
const isInitialized = await this.userRepository.hasAdmin();
|
const isInitialized = await this.userRepository.hasAdmin();
|
||||||
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
|
const onboarding = await this.systemMetadataRepository.get(SystemMetadataKey.ADMIN_ONBOARDING);
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ export class SmartInfoService {
|
|||||||
|
|
||||||
await this.jobRepository.waitForQueueCompletion(QueueName.SMART_SEARCH);
|
await this.jobRepository.waitForQueueCompletion(QueueName.SMART_SEARCH);
|
||||||
|
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, () =>
|
await this.databaseRepository.withLock(DatabaseLock.CLIPDimSize, () =>
|
||||||
this.repository.init(machineLearning.clip.modelName),
|
this.repository.init(machineLearning.clip.modelName),
|
||||||
@@ -50,7 +50,7 @@ export class SmartInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
|
async handleQueueEncodeClip({ force }: IBaseJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ export class SmartInfoService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
async handleEncodeClip({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning } = await this.configCore.getConfig();
|
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isSmartSearchEnabled(machineLearning)) {
|
if (!isSmartSearchEnabled(machineLearning)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ export class StorageTemplateService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMigrationSingle({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: true });
|
||||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||||
if (!storageTemplateEnabled) {
|
if (!storageTemplateEnabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
@@ -140,7 +140,7 @@ export class StorageTemplateService {
|
|||||||
|
|
||||||
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.configCore.getConfig();
|
const { storageTemplate } = await this.configCore.getConfig({ withCache: true });
|
||||||
const { enabled } = storageTemplate;
|
const { enabled } = storageTemplate;
|
||||||
if (!enabled) {
|
if (!enabled) {
|
||||||
this.logger.log('Storage template migration disabled, skipping');
|
this.logger.log('Storage template migration disabled, skipping');
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
const config = await this.core.getConfig();
|
const config = await this.core.getConfig({ withCache: false });
|
||||||
this.config$.next(config);
|
this.config$.next(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getConfig(): Promise<SystemConfigDto> {
|
async getConfig(): Promise<SystemConfigDto> {
|
||||||
const config = await this.core.getConfig();
|
const config = await this.core.getConfig({ withCache: false });
|
||||||
return mapConfig(config);
|
return mapConfig(config);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,7 +71,7 @@ export class SystemConfigService {
|
|||||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldConfig = await this.core.getConfig();
|
const oldConfig = await this.core.getConfig({ withCache: false });
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.eventRepository.serverSendAsync(ServerAsyncEvent.CONFIG_VALIDATE, {
|
await this.eventRepository.serverSendAsync(ServerAsyncEvent.CONFIG_VALIDATE, {
|
||||||
@@ -110,7 +110,7 @@ export class SystemConfigService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getCustomCss(): Promise<string> {
|
async getCustomCss(): Promise<string> {
|
||||||
const { theme } = await this.core.getConfig();
|
const { theme } = await this.core.getConfig({ withCache: false });
|
||||||
return theme.customCss;
|
return theme.customCss;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class UserService {
|
|||||||
|
|
||||||
async handleUserDeleteCheck(): Promise<JobStatus> {
|
async handleUserDeleteCheck(): Promise<JobStatus> {
|
||||||
const users = await this.userRepository.getDeletedUsers();
|
const users = await this.userRepository.getDeletedUsers();
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.getConfig({ withCache: false });
|
||||||
await this.jobRepository.queueAll(
|
await this.jobRepository.queueAll(
|
||||||
users.flatMap((user) =>
|
users.flatMap((user) =>
|
||||||
this.isReadyForDeletion(user, config.user.deleteDelay)
|
this.isReadyForDeletion(user, config.user.deleteDelay)
|
||||||
@@ -140,7 +140,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
async handleUserDelete({ id, force }: IEntityJob): Promise<JobStatus> {
|
||||||
const config = await this.configCore.getConfig();
|
const config = await this.configCore.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) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export class VersionService {
|
|||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newVersionCheck } = await this.configCore.getConfig();
|
const { newVersionCheck } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!newVersionCheck.enabled) {
|
if (!newVersionCheck.enabled) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ const port = Number(process.env.IMMICH_PORT) || 3001;
|
|||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
|
|
||||||
const main = async () => {
|
const main = async () => {
|
||||||
|
if (!process.env.IMMICH_WORKERS_INCLUDE?.includes('api')) {
|
||||||
|
process.exit();
|
||||||
|
}
|
||||||
|
|
||||||
const timeout = setTimeout(() => controller.abort(), 2000);
|
const timeout = setTimeout(() => controller.abort(), 2000);
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`http://localhost:${port}/api/server-info/ping`, {
|
const response = await fetch(`http://localhost:${port}/api/server-info/ping`, {
|
||||||
|
|||||||
@@ -842,7 +842,7 @@ export class VAAPIConfig extends BaseHWConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getSupportedCodecs() {
|
getSupportedCodecs() {
|
||||||
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9];
|
return [VideoCodec.H264, VideoCodec.HEVC, VideoCodec.VP9, VideoCodec.AV1];
|
||||||
}
|
}
|
||||||
|
|
||||||
useCQP() {
|
useCQP() {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
SwaggerDocumentOptions,
|
SwaggerDocumentOptions,
|
||||||
SwaggerModule,
|
SwaggerModule,
|
||||||
} from '@nestjs/swagger';
|
} from '@nestjs/swagger';
|
||||||
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
import { ReferenceObject, SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { writeFileSync } from 'node:fs';
|
import { writeFileSync } from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
@@ -111,6 +111,14 @@ function sortKeys<T>(target: T): T {
|
|||||||
export const routeToErrorMessage = (methodName: string) =>
|
export const routeToErrorMessage = (methodName: string) =>
|
||||||
'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
|
'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
|
||||||
|
|
||||||
|
const isSchema = (schema: string | ReferenceObject | SchemaObject): schema is SchemaObject => {
|
||||||
|
if (typeof schema === 'string' || '$ref' in schema) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
const patchOpenAPI = (document: OpenAPIObject) => {
|
const patchOpenAPI = (document: OpenAPIObject) => {
|
||||||
document.paths = sortKeys(document.paths);
|
document.paths = sortKeys(document.paths);
|
||||||
|
|
||||||
@@ -119,13 +127,23 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
|||||||
|
|
||||||
document.components.schemas = sortKeys(schemas);
|
document.components.schemas = sortKeys(schemas);
|
||||||
|
|
||||||
for (const schema of Object.values(schemas)) {
|
for (const [schemaName, schema] of Object.entries(schemas)) {
|
||||||
if (schema.properties) {
|
if (schema.properties) {
|
||||||
schema.properties = sortKeys(schema.properties);
|
schema.properties = sortKeys(schema.properties);
|
||||||
}
|
|
||||||
|
|
||||||
if (schema.required) {
|
for (const [key, value] of Object.entries(schema.properties)) {
|
||||||
schema.required = schema.required.sort();
|
if (typeof value === 'string') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isSchema(value) && value.type === 'number' && value.format === 'float') {
|
||||||
|
throw new Error(`Invalid number format: ${schemaName}.${key}=float (use double instead). `);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (schema.required) {
|
||||||
|
schema.required = schema.required.sort();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
6
web/package-lock.json
generated
6
web/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
@@ -68,7 +68,7 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"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"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-web",
|
"name": "immich-web",
|
||||||
"version": "1.106.2",
|
"version": "1.106.3",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
"dev": "vite dev --host 0.0.0.0 --port 3000",
|
||||||
|
|||||||
@@ -76,13 +76,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if forceDelete}
|
{#if forceDelete}
|
||||||
<p class="text-immich-error">
|
<p class="text-immich-error">{$t('admin.force_delete_user_warning')}</p>
|
||||||
WARNING: This will immediately remove the user and all assets. This cannot be undone and the files cannot be
|
|
||||||
recovered.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p class="immich-form-label text-sm" id="confirm-user-desc">
|
<p class="immich-form-label text-sm" id="confirm-user-desc">
|
||||||
To confirm, type "{user.email}" below
|
{$t('admin.confirm_email_below', { values: { email: user.email } })}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -58,7 +58,7 @@
|
|||||||
<Badge color="primary">
|
<Badge color="primary">
|
||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{jobCounts.failed.toLocaleString($locale)} failed
|
{$t('admin.jobs_failed', { values: { jobCount: jobCounts.failed.toLocaleString($locale) } })}
|
||||||
</span>
|
</span>
|
||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
{#if jobCounts.delayed > 0}
|
{#if jobCounts.delayed > 0}
|
||||||
<Badge color="secondary">
|
<Badge color="secondary">
|
||||||
<span class="text-sm">
|
<span class="text-sm">
|
||||||
{jobCounts.delayed.toLocaleString($locale)} delayed
|
{$t('admin.jobs_delayed', { values: { jobCount: jobCounts.delayed.toLocaleString($locale) } })}
|
||||||
</span>
|
</span>
|
||||||
</Badge>
|
</Badge>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -119,12 +119,14 @@
|
|||||||
color="light-gray"
|
color="light-gray"
|
||||||
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
||||||
>
|
>
|
||||||
<Icon path={mdiAlertCircle} size="36" /> DISABLED
|
<Icon path={mdiAlertCircle} size="36" />
|
||||||
|
{$t('disabled').toUpperCase()}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{:else if !isIdle}
|
{:else if !isIdle}
|
||||||
{#if waitingCount > 0}
|
{#if waitingCount > 0}
|
||||||
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
|
<JobTileButton color="gray" on:click={() => dispatch('command', { command: JobCommand.Empty, force: false })}>
|
||||||
<Icon path={mdiClose} size="24" /> CLEAR
|
<Icon path={mdiClose} size="24" />
|
||||||
|
{$t('clear').toUpperCase()}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
{#if queueStatus.isPaused}
|
{#if queueStatus.isPaused}
|
||||||
@@ -134,14 +136,16 @@
|
|||||||
on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
|
on:click={() => dispatch('command', { command: JobCommand.Resume, force: false })}
|
||||||
>
|
>
|
||||||
<!-- size property is not reactive, so have to use width and height -->
|
<!-- size property is not reactive, so have to use width and height -->
|
||||||
<Icon path={mdiFastForward} {size} /> RESUME
|
<Icon path={mdiFastForward} {size} />
|
||||||
|
{$t('resume').toUpperCase()}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{:else}
|
{:else}
|
||||||
<JobTileButton
|
<JobTileButton
|
||||||
color="light-gray"
|
color="light-gray"
|
||||||
on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
|
on:click={() => dispatch('command', { command: JobCommand.Pause, force: false })}
|
||||||
>
|
>
|
||||||
<Icon path={mdiPause} size="24" /> PAUSE
|
<Icon path={mdiPause} size="24" />
|
||||||
|
{$t('pause').toUpperCase()}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
{:else if allowForceCommand}
|
{:else if allowForceCommand}
|
||||||
@@ -161,7 +165,8 @@
|
|||||||
color="light-gray"
|
color="light-gray"
|
||||||
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
on:click={() => dispatch('command', { command: JobCommand.Start, force: false })}
|
||||||
>
|
>
|
||||||
<Icon path={mdiPlay} size="48" /> START
|
<Icon path={mdiPlay} size="48" />
|
||||||
|
{$t('start').toUpperCase()}
|
||||||
</JobTileButton>
|
</JobTileButton>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@
|
|||||||
if (dto.force) {
|
if (dto.force) {
|
||||||
const isConfirmed = await dialogController.show({
|
const isConfirmed = await dialogController.show({
|
||||||
id: 'confirm-reprocess-all-faces',
|
id: 'confirm-reprocess-all-faces',
|
||||||
prompt: 'Are you sure you want to reprocess all faces? This will also clear named people.',
|
prompt: $t('admin.confirm_reprocess_all_faces'),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isConfirmed) {
|
if (isConfirmed) {
|
||||||
@@ -60,23 +60,23 @@
|
|||||||
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
|
$: jobDetails = <Partial<Record<JobName, JobDetails>>>{
|
||||||
[JobName.ThumbnailGeneration]: {
|
[JobName.ThumbnailGeneration]: {
|
||||||
icon: mdiFileJpgBox,
|
icon: mdiFileJpgBox,
|
||||||
title: getJobName(JobName.ThumbnailGeneration),
|
title: $getJobName(JobName.ThumbnailGeneration),
|
||||||
subtitle: $t('admin.thumbnail_generation_job_description'),
|
subtitle: $t('admin.thumbnail_generation_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.MetadataExtraction]: {
|
[JobName.MetadataExtraction]: {
|
||||||
icon: mdiTable,
|
icon: mdiTable,
|
||||||
title: getJobName(JobName.MetadataExtraction),
|
title: $getJobName(JobName.MetadataExtraction),
|
||||||
subtitle: $t('admin.metadata_extraction_job_description'),
|
subtitle: $t('admin.metadata_extraction_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.Library]: {
|
[JobName.Library]: {
|
||||||
icon: mdiLibraryShelves,
|
icon: mdiLibraryShelves,
|
||||||
title: getJobName(JobName.Library),
|
title: $getJobName(JobName.Library),
|
||||||
subtitle: $t('admin.library_tasks_description'),
|
subtitle: $t('admin.library_tasks_description'),
|
||||||
allText: $t('all').toUpperCase(),
|
allText: $t('all').toUpperCase(),
|
||||||
missingText: $t('refresh').toUpperCase(),
|
missingText: $t('refresh').toUpperCase(),
|
||||||
},
|
},
|
||||||
[JobName.Sidecar]: {
|
[JobName.Sidecar]: {
|
||||||
title: getJobName(JobName.Sidecar),
|
title: $getJobName(JobName.Sidecar),
|
||||||
icon: mdiFileXmlBox,
|
icon: mdiFileXmlBox,
|
||||||
subtitle: $t('admin.sidecar_job_description'),
|
subtitle: $t('admin.sidecar_job_description'),
|
||||||
allText: $t('sync').toUpperCase(),
|
allText: $t('sync').toUpperCase(),
|
||||||
@@ -85,46 +85,44 @@
|
|||||||
},
|
},
|
||||||
[JobName.SmartSearch]: {
|
[JobName.SmartSearch]: {
|
||||||
icon: mdiImageSearch,
|
icon: mdiImageSearch,
|
||||||
title: getJobName(JobName.SmartSearch),
|
title: $getJobName(JobName.SmartSearch),
|
||||||
subtitle: $t('admin.smart_search_job_description'),
|
subtitle: $t('admin.smart_search_job_description'),
|
||||||
disabled: !$featureFlags.smartSearch,
|
disabled: !$featureFlags.smartSearch,
|
||||||
},
|
},
|
||||||
[JobName.DuplicateDetection]: {
|
[JobName.DuplicateDetection]: {
|
||||||
icon: mdiContentDuplicate,
|
icon: mdiContentDuplicate,
|
||||||
title: getJobName(JobName.DuplicateDetection),
|
title: $getJobName(JobName.DuplicateDetection),
|
||||||
subtitle: $t('admin.duplicate_detection_job_description'),
|
subtitle: $t('admin.duplicate_detection_job_description'),
|
||||||
disabled: !$featureFlags.duplicateDetection,
|
disabled: !$featureFlags.duplicateDetection,
|
||||||
},
|
},
|
||||||
[JobName.FaceDetection]: {
|
[JobName.FaceDetection]: {
|
||||||
icon: mdiFaceRecognition,
|
icon: mdiFaceRecognition,
|
||||||
title: getJobName(JobName.FaceDetection),
|
title: $getJobName(JobName.FaceDetection),
|
||||||
subtitle:
|
subtitle: $t('admin.face_detection_description'),
|
||||||
'Detect the faces in assets using machine learning. For videos, only the thumbnail is considered. "All" (re-)processes all assets. "Missing" queues assets that haven\'t been processed yet. Detected faces will be queued for Facial Recognition after Face Detection is complete, grouping them into existing or new people.',
|
|
||||||
handleCommand: handleConfirmCommand,
|
handleCommand: handleConfirmCommand,
|
||||||
disabled: !$featureFlags.facialRecognition,
|
disabled: !$featureFlags.facialRecognition,
|
||||||
},
|
},
|
||||||
[JobName.FacialRecognition]: {
|
[JobName.FacialRecognition]: {
|
||||||
icon: mdiTagFaces,
|
icon: mdiTagFaces,
|
||||||
title: getJobName(JobName.FacialRecognition),
|
title: $getJobName(JobName.FacialRecognition),
|
||||||
subtitle:
|
subtitle: $t('admin.facial_recognition_job_description'),
|
||||||
'Group detected faces into people. This step runs after Face Detection is complete. "All" (re-)clusters all faces. "Missing" queues faces that don\'t have a person assigned.',
|
|
||||||
handleCommand: handleConfirmCommand,
|
handleCommand: handleConfirmCommand,
|
||||||
disabled: !$featureFlags.facialRecognition,
|
disabled: !$featureFlags.facialRecognition,
|
||||||
},
|
},
|
||||||
[JobName.VideoConversion]: {
|
[JobName.VideoConversion]: {
|
||||||
icon: mdiVideo,
|
icon: mdiVideo,
|
||||||
title: getJobName(JobName.VideoConversion),
|
title: $getJobName(JobName.VideoConversion),
|
||||||
subtitle: $t('admin.video_conversion_job_description'),
|
subtitle: $t('admin.video_conversion_job_description'),
|
||||||
},
|
},
|
||||||
[JobName.StorageTemplateMigration]: {
|
[JobName.StorageTemplateMigration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
title: getJobName(JobName.StorageTemplateMigration),
|
title: $getJobName(JobName.StorageTemplateMigration),
|
||||||
allowForceCommand: false,
|
allowForceCommand: false,
|
||||||
description: StorageMigrationDescription,
|
description: StorageMigrationDescription,
|
||||||
},
|
},
|
||||||
[JobName.Migration]: {
|
[JobName.Migration]: {
|
||||||
icon: mdiFolderMove,
|
icon: mdiFolderMove,
|
||||||
title: getJobName(JobName.Migration),
|
title: $getJobName(JobName.Migration),
|
||||||
subtitle: $t('admin.migration_job_description'),
|
subtitle: $t('admin.migration_job_description'),
|
||||||
allowForceCommand: false,
|
allowForceCommand: false,
|
||||||
},
|
},
|
||||||
@@ -140,14 +138,14 @@
|
|||||||
switch (jobCommand.command) {
|
switch (jobCommand.command) {
|
||||||
case JobCommand.Empty: {
|
case JobCommand.Empty: {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `Cleared jobs for: ${title}`,
|
message: $t('admin.cleared_jobs', { values: { job: title } }),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, `Command '${jobCommand.command}' failed for job: ${title}`);
|
handleError(error, $t('admin.failed_job_command', { values: { command: jobCommand.command, job: title } }));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -110,7 +110,7 @@
|
|||||||
{#if user.quotaSizeInBytes}
|
{#if user.quotaSizeInBytes}
|
||||||
({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%)
|
({((user.usage / user.quotaSizeInBytes) * 100).toFixed(0)}%)
|
||||||
{:else}
|
{:else}
|
||||||
(Unlimited)
|
({$t('unlimited')})
|
||||||
{/if}
|
{/if}
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: 'Reset settings to the recent saved settings',
|
message: $t('admin.reset_settings_to_recent_saved'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: $t('reset_settings_to_default'),
|
message: $t('admin.reset_settings_to_default'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -270,7 +270,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: TranscodeHWAccel.Disabled,
|
value: TranscodeHWAccel.Disabled,
|
||||||
text: $t('admin.disabled'),
|
text: $t('disabled'),
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
isEdited={config.ffmpeg.accel !== savedConfig.ffmpeg.accel}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import SettingInputField, {
|
import SettingInputField, {
|
||||||
SettingInputFieldType,
|
SettingInputFieldType,
|
||||||
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
} from '$lib/components/shared-components/settings/setting-input-field.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let savedConfig: SystemConfigDto;
|
export let savedConfig: SystemConfigDto;
|
||||||
export let defaultConfig: SystemConfigDto;
|
export let defaultConfig: SystemConfigDto;
|
||||||
@@ -45,7 +46,7 @@
|
|||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
{disabled}
|
{disabled}
|
||||||
label="{getJobName(jobName)} Concurrency"
|
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||||
desc=""
|
desc=""
|
||||||
bind:value={config.job[jobName].concurrency}
|
bind:value={config.job[jobName].concurrency}
|
||||||
required={true}
|
required={true}
|
||||||
@@ -54,11 +55,11 @@
|
|||||||
{:else}
|
{:else}
|
||||||
<SettingInputField
|
<SettingInputField
|
||||||
inputType={SettingInputFieldType.NUMBER}
|
inputType={SettingInputFieldType.NUMBER}
|
||||||
label="{getJobName(jobName)} Concurrency"
|
label={$t('admin.job_concurrency', { values: { job: $getJobName(jobName) } })}
|
||||||
desc=""
|
desc=""
|
||||||
value="1"
|
value="1"
|
||||||
disabled={true}
|
disabled={true}
|
||||||
title="This job is not concurrency-safe."
|
title={$t('admin.job_not_concurrency_safe')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
const substitutions: Record<string, string> = {
|
const substitutions: Record<string, string> = {
|
||||||
filename: 'IMAGE_56437',
|
filename: 'IMAGE_56437',
|
||||||
ext: 'jpg',
|
ext: 'jpg',
|
||||||
filetype: $t('img').toUpperCase(),
|
filetype: 'IMG',
|
||||||
filetypefull: 'IMAGE',
|
filetypefull: 'IMAGE',
|
||||||
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
assetId: 'a8312960-e277-447d-b4ea-56717ccba856',
|
||||||
album: $t('album_name'),
|
album: $t('album_name'),
|
||||||
|
|||||||
@@ -177,7 +177,7 @@
|
|||||||
color="opaque"
|
color="opaque"
|
||||||
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
|
icon={asset.isFavorite ? mdiHeart : mdiHeartOutline}
|
||||||
on:click={() => dispatch('favorite')}
|
on:click={() => dispatch('favorite')}
|
||||||
title={asset.isFavorite ? $t('unfavorite') : $t('favorite')}
|
title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')}
|
||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
@@ -242,7 +242,7 @@
|
|||||||
<MenuOption
|
<MenuOption
|
||||||
on:click={() => onMenuClick('toggleArchive')}
|
on:click={() => onMenuClick('toggleArchive')}
|
||||||
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
|
icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline}
|
||||||
text={asset.isArchived ? $t('unarchive') : $t('archive')}
|
text={asset.isArchived ? $t('unarchive') : $t('to_archive')}
|
||||||
/>
|
/>
|
||||||
<MenuOption
|
<MenuOption
|
||||||
icon={mdiUpload}
|
icon={mdiUpload}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onCopyShortcut = (event: KeyboardEvent) => {
|
const onCopyShortcut = (event: KeyboardEvent) => {
|
||||||
if (window.getSelection()?.type === $t('range')) {
|
if (window.getSelection()?.type === 'Range') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -112,7 +112,7 @@
|
|||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{#if imageError}
|
{#if imageError}
|
||||||
<div class="h-full flex items-center justify-center">Error loading image</div>
|
<div class="h-full flex items-center justify-center">{$t('error_loading_image')}</div>
|
||||||
{/if}
|
{/if}
|
||||||
<div bind:this={element} class="relative h-full select-none">
|
<div bind:this={element} class="relative h-full select-none">
|
||||||
<img
|
<img
|
||||||
|
|||||||
@@ -40,7 +40,6 @@
|
|||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
export let showArchiveIcon = false;
|
export let showArchiveIcon = false;
|
||||||
export let showStackedIcon = true;
|
export let showStackedIcon = true;
|
||||||
export let href: string | undefined = undefined;
|
|
||||||
export let onClick: ((asset: AssetResponseDto, event: Event) => void) | undefined = undefined;
|
export let onClick: ((asset: AssetResponseDto, event: Event) => void) | undefined = undefined;
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
@@ -94,7 +93,7 @@
|
|||||||
|
|
||||||
<IntersectionObserver once={false} on:intersected let:intersecting>
|
<IntersectionObserver once={false} on:intersected let:intersecting>
|
||||||
<a
|
<a
|
||||||
href={href ?? currentUrlReplaceAssetId(asset.id)}
|
href={currentUrlReplaceAssetId(asset.id)}
|
||||||
style:width="{width}px"
|
style:width="{width}px"
|
||||||
style:height="{height}px"
|
style:height="{height}px"
|
||||||
class="group focus-visible:outline-none flex overflow-hidden {disabled
|
class="group focus-visible:outline-none flex overflow-hidden {disabled
|
||||||
|
|||||||
@@ -101,23 +101,23 @@
|
|||||||
<ControlAppBar on:close={onClose}>
|
<ControlAppBar on:close={onClose}>
|
||||||
<svelte:fragment slot="leading">
|
<svelte:fragment slot="leading">
|
||||||
{#if hasSelection}
|
{#if hasSelection}
|
||||||
Selected {selectedPeople.length}
|
{$t('selected')} {selectedPeople.length}
|
||||||
{:else}
|
{:else}
|
||||||
Merge people
|
{$t('merge_people')}
|
||||||
{/if}
|
{/if}
|
||||||
<div />
|
<div />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<svelte:fragment slot="trailing">
|
<svelte:fragment slot="trailing">
|
||||||
<Button size={'sm'} disabled={!hasSelection} on:click={handleMerge}>
|
<Button size={'sm'} disabled={!hasSelection} on:click={handleMerge}>
|
||||||
<Icon path={mdiMerge} size={18} />
|
<Icon path={mdiMerge} size={18} />
|
||||||
<span class="ml-2"> Merge</span></Button
|
<span class="ml-2">{$t('merge')}</span></Button
|
||||||
>
|
>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
<section class="bg-immich-bg px-[70px] pt-[100px] dark:bg-immich-dark-bg">
|
||||||
<section id="merge-face-selector relative">
|
<section id="merge-face-selector relative">
|
||||||
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
<div class="mb-10 h-[200px] place-content-center place-items-center">
|
||||||
<p class="mb-4 text-center uppercase dark:text-white">Choose matching people to merge</p>
|
<p class="mb-4 text-center uppercase dark:text-white">{$t('choose_matching_people_to_merge')}</p>
|
||||||
|
|
||||||
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
|
<div class="grid grid-flow-col-dense place-content-center place-items-center gap-4">
|
||||||
{#each selectedPeople as person (person.id)}
|
{#each selectedPeople as person (person.id)}
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FullScreenModal title="Merge people - {title}" onClose={() => dispatch('close')}>
|
<FullScreenModal title="{$t('merge_people')} - {title}" onClose={() => dispatch('close')}>
|
||||||
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
|
<div class="flex items-center justify-center py-4 md:h-36 md:py-4">
|
||||||
{#if !choosePersonToMerge}
|
{#if !choosePersonToMerge}
|
||||||
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
<div class="flex h-20 w-20 items-center px-1 md:h-24 md:w-24 md:px-2">
|
||||||
|
|||||||
@@ -225,8 +225,8 @@
|
|||||||
curve
|
curve
|
||||||
shadow
|
shadow
|
||||||
url={selectedPersonToCreate[face.id]}
|
url={selectedPersonToCreate[face.id]}
|
||||||
altText={'New person'}
|
altText={$t('new_person')}
|
||||||
title={'New person'}
|
title={$t('new_person')}
|
||||||
widthStyle={thumbnailWidth}
|
widthStyle={thumbnailWidth}
|
||||||
heightStyle={thumbnailWidth}
|
heightStyle={thumbnailWidth}
|
||||||
/>
|
/>
|
||||||
@@ -309,7 +309,7 @@
|
|||||||
<CircleIconButton
|
<CircleIconButton
|
||||||
color="primary"
|
color="primary"
|
||||||
icon={mdiMinus}
|
icon={mdiMinus}
|
||||||
title="Select new face"
|
title={$t('select_new_face')}
|
||||||
size="18"
|
size="18"
|
||||||
padding="1"
|
padding="1"
|
||||||
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
class="absolute left-1/2 top-1/2 translate-x-[-50%] translate-y-[-50%] transform"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
<div class="flex items-center">
|
<div class="flex items-center">
|
||||||
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
<CircleIconButton title={$t('close')} icon={mdiClose} on:click={onClose} />
|
||||||
<div class="flex gap-2 items-center">
|
<div class="flex gap-2 items-center">
|
||||||
<p class="ml-2">Show & hide people</p>
|
<p class="ml-2">{$t('show_and_hide_people')}</p>
|
||||||
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
<p class="text-sm text-gray-400 dark:text-gray-600">({countTotalPeople.toLocaleString($locale)})</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -33,8 +33,8 @@
|
|||||||
await signUpAdmin({ signUpDto: { email, password, name } });
|
await signUpAdmin({ signUpDto: { email, password, name } });
|
||||||
await goto(AppRoute.AUTH_LOGIN);
|
await goto(AppRoute.AUTH_LOGIN);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'errors.errors.unable_to_create_admin_account');
|
handleError(error, 'errors.unable_to_create_admin_account');
|
||||||
errorMessage = $t('errors.errors.unable_to_create_admin_account');
|
errorMessage = $t('errors.unable_to_create_admin_account');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,9 @@
|
|||||||
|
|
||||||
{#if $featureFlags.email}
|
{#if $featureFlags.email}
|
||||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||||
<label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email"> Send welcome email </label>
|
<label class="text-sm dark:text-immich-dark-fg" for="send-welcome-email">
|
||||||
|
{$t('admin.send_welcome_email')}
|
||||||
|
</label>
|
||||||
<Slider id="send-welcome-email" bind:checked={notify} />
|
<Slider id="send-welcome-email" bind:checked={notify} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@@ -101,7 +103,7 @@
|
|||||||
|
|
||||||
<div class="my-4 flex place-items-center justify-between gap-2">
|
<div class="my-4 flex place-items-center justify-between gap-2">
|
||||||
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
|
<label class="text-sm dark:text-immich-dark-fg" for="require-password-change">
|
||||||
Require user to change password on first login
|
{$t('admin.require_password_change_on_login')}
|
||||||
</label>
|
</label>
|
||||||
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
|
<Slider id="require-password-change" bind:checked={shouldChangePassword} />
|
||||||
</div>
|
</div>
|
||||||
@@ -113,9 +115,9 @@
|
|||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
||||||
Quota Size (GiB)
|
{$t('admin.quota_size_gib')}
|
||||||
{#if quotaSizeWarning}
|
{#if quotaSizeWarning}
|
||||||
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
|
<p class="text-red-400 text-sm">{$t('admin.quota_higher_than_disk_size')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</label>
|
</label>
|
||||||
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
<input class="immich-form-input" id="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
||||||
|
|||||||
@@ -55,7 +55,7 @@
|
|||||||
const resetPassword = async () => {
|
const resetPassword = async () => {
|
||||||
const isConfirmed = await dialogController.show({
|
const isConfirmed = await dialogController.show({
|
||||||
id: 'confirm-reset-password',
|
id: 'confirm-reset-password',
|
||||||
prompt: `Are you sure you want to reset ${user.name}'s password?`,
|
prompt: $t('admin.confirm_user_password_reset', { values: { user: user.name } }),
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isConfirmed) {
|
if (!isConfirmed) {
|
||||||
@@ -110,13 +110,14 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="flex items-center gap-2 immich-form-label" for="quotaSize"
|
<label class="flex items-center gap-2 immich-form-label" for="quotaSize">
|
||||||
>Quota Size (GiB) {#if quotaSizeWarning}
|
{$t('admin.quota_size_gib')}
|
||||||
<p class="text-red-400 text-sm">You set a quota higher than the disk size</p>
|
{#if quotaSizeWarning}
|
||||||
|
<p class="text-red-400 text-sm">{$t('errors.quota_higher_than_disk_size')}</p>
|
||||||
{/if}</label
|
{/if}</label
|
||||||
>
|
>
|
||||||
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
<input class="immich-form-input" id="quotaSize" name="quotaSize" type="number" min="0" bind:value={quotaSize} />
|
||||||
<p>Note: Enter 0 for unlimited quota</p>
|
<p>{$t('admin.note_unlimited_quota')}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
@@ -130,10 +131,10 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
Note: To apply the Storage Label to previously uploaded assets, run the
|
{$t('admin.note_apply_storage_label_previous_assets')}
|
||||||
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
|
<a href={AppRoute.ADMIN_JOBS} class="text-immich-primary dark:text-immich-dark-primary">
|
||||||
Storage Migration Job</a
|
{$t('admin.storage_template_migration_job')}
|
||||||
>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -32,11 +32,9 @@
|
|||||||
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
|
<FullScreenModal title={$t('add_exclusion_pattern')} icon={mdiFolderRemove} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="add-exclusion-pattern-form">
|
||||||
<p class="py-5 text-sm">
|
<p class="py-5 text-sm">
|
||||||
Exclusion patterns lets you ignore files and folders when scanning your library. This is useful if you have
|
{$t('admin.exclusion_pattern_description')}
|
||||||
folders that contain files you don't want to import, such as RAW files.
|
|
||||||
<br /><br />
|
<br /><br />
|
||||||
Add exclusion patterns. Globbing using *, **, and ? is supported. To ignore all files in any directory named "Raw",
|
{$t('admin.add_exclusion_pattern_description')}
|
||||||
use "**/Raw/**". To ignore all files ending in ".tif", use "**/*.tif". To ignore an absolute path, use "/path/to/ignore/**".
|
|
||||||
</p>
|
</p>
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
|
<label class="immich-form-label" for="exclusionPattern">{$t('pattern')}</label>
|
||||||
@@ -50,7 +48,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="mt-8 flex w-full gap-4">
|
<div class="mt-8 flex w-full gap-4">
|
||||||
{#if isDuplicate}
|
{#if isDuplicate}
|
||||||
<p class="text-red-500 text-sm">This exclusion pattern already exists.</p>
|
<p class="text-red-500 text-sm">{$t('errors.exclusion_pattern_already_exists')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -33,9 +33,7 @@
|
|||||||
|
|
||||||
<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}>
|
<FullScreenModal {title} icon={mdiFolderSync} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="library-import-path-form">
|
||||||
<p class="py-5 text-sm">
|
<p class="py-5 text-sm">{$t('admin.library_import_path_description')}</p>
|
||||||
Specify a folder to import. This folder, including subfolders, will be scanned for images and videos.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div class="my-4 flex flex-col gap-2">
|
<div class="my-4 flex flex-col gap-2">
|
||||||
<label class="immich-form-label" for="path">{$t('path')}</label>
|
<label class="immich-form-label" for="path">{$t('path')}</label>
|
||||||
@@ -44,7 +42,7 @@
|
|||||||
|
|
||||||
<div class="mt-8 flex w-full gap-4">
|
<div class="mt-8 flex w-full gap-4">
|
||||||
{#if isDuplicate}
|
{#if isDuplicate}
|
||||||
<p class="text-red-500 text-sm">This import path already exists.</p>
|
<p class="text-red-500 text-sm">{$t('admin.import_path_already_exists')}</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@
|
|||||||
import type { ValidateLibraryImportPathResponseDto } from '@immich/sdk';
|
import type { ValidateLibraryImportPathResponseDto } from '@immich/sdk';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import { s } from '$lib/utils';
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let library: LibraryResponseDto;
|
export let library: LibraryResponseDto;
|
||||||
@@ -54,13 +53,13 @@
|
|||||||
if (failedPaths === 0) {
|
if (failedPaths === 0) {
|
||||||
if (notifyIfSuccessful) {
|
if (notifyIfSuccessful) {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `All paths validated successfully`,
|
message: $t('admin.paths_validated_successfully'),
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
message: `${failedPaths} path${s(failedPaths)} failed validation`,
|
message: $t('errors.paths_validation_failed', { values: { paths: failedPaths } }),
|
||||||
type: NotificationType.Warning,
|
type: NotificationType.Warning,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -95,7 +94,7 @@
|
|||||||
await revalidate(false);
|
await revalidate(false);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to add import path');
|
handleError(error, $t('errors.unable_to_add_import_path'));
|
||||||
} finally {
|
} finally {
|
||||||
addImportPath = false;
|
addImportPath = false;
|
||||||
importPathToAdd = null;
|
importPathToAdd = null;
|
||||||
@@ -121,7 +120,7 @@
|
|||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
editImportPath = null;
|
editImportPath = null;
|
||||||
handleError(error, 'Unable to edit import path');
|
handleError(error, $t('errors.unable_to_edit_import_path'));
|
||||||
} finally {
|
} finally {
|
||||||
editImportPath = null;
|
editImportPath = null;
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@
|
|||||||
library.importPaths = library.importPaths.filter((path) => path != pathToDelete);
|
library.importPaths = library.importPaths.filter((path) => path != pathToDelete);
|
||||||
await handleValidation();
|
await handleValidation();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to delete import path');
|
handleError(error, $t('errors.unable_to_delete_import_path'));
|
||||||
} finally {
|
} finally {
|
||||||
editImportPath = null;
|
editImportPath = null;
|
||||||
}
|
}
|
||||||
@@ -230,7 +229,7 @@
|
|||||||
>
|
>
|
||||||
<td class="w-4/5 text-ellipsis px-4 text-sm">
|
<td class="w-4/5 text-ellipsis px-4 text-sm">
|
||||||
{#if importPaths.length === 0}
|
{#if importPaths.length === 0}
|
||||||
No paths added
|
{$t('admin.no_paths_added')}
|
||||||
{/if}</td
|
{/if}</td
|
||||||
>
|
>
|
||||||
<td class="w-1/5 text-ellipsis px-4 text-sm"
|
<td class="w-1/5 text-ellipsis px-4 text-sm"
|
||||||
|
|||||||
@@ -54,7 +54,7 @@
|
|||||||
exclusionPatterns = library.exclusionPatterns;
|
exclusionPatterns = library.exclusionPatterns;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to add exclusion pattern');
|
handleError(error, $t('errors.unable_to_add_exclusion_pattern'));
|
||||||
} finally {
|
} finally {
|
||||||
exclusionPatternToAdd = '';
|
exclusionPatternToAdd = '';
|
||||||
addExclusionPattern = false;
|
addExclusionPattern = false;
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
library.exclusionPatterns[editExclusionPattern] = editedExclusionPattern;
|
library.exclusionPatterns[editExclusionPattern] = editedExclusionPattern;
|
||||||
exclusionPatterns = library.exclusionPatterns;
|
exclusionPatterns = library.exclusionPatterns;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to edit exclude pattern');
|
handleError(error, $t('errors.unable_to_edit_exclusion_pattern'));
|
||||||
} finally {
|
} finally {
|
||||||
editExclusionPattern = null;
|
editExclusionPattern = null;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@
|
|||||||
library.exclusionPatterns = library.exclusionPatterns.filter((path) => path != pathToDelete);
|
library.exclusionPatterns = library.exclusionPatterns.filter((path) => path != pathToDelete);
|
||||||
exclusionPatterns = library.exclusionPatterns;
|
exclusionPatterns = library.exclusionPatterns;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Unable to delete exclude pattern');
|
handleError(error, $t('errors.unable_to_delete_exclusion_pattern'));
|
||||||
} finally {
|
} finally {
|
||||||
editExclusionPattern = null;
|
editExclusionPattern = null;
|
||||||
}
|
}
|
||||||
@@ -162,7 +162,7 @@
|
|||||||
>
|
>
|
||||||
<td class="w-3/4 text-ellipsis px-4 text-sm">
|
<td class="w-3/4 text-ellipsis px-4 text-sm">
|
||||||
{#if exclusionPatterns.length === 0}
|
{#if exclusionPatterns.length === 0}
|
||||||
No pattern added
|
{$t('admin.no_pattern_added')}
|
||||||
{/if}
|
{/if}
|
||||||
</td>
|
</td>
|
||||||
<td class="w-1/4 text-ellipsis px-4 text-sm"
|
<td class="w-1/4 text-ellipsis px-4 text-sm"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
|
<FullScreenModal title={$t('select_library_owner')} icon={mdiFolderSync} onClose={handleCancel}>
|
||||||
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
|
<form on:submit|preventDefault={() => handleSubmit()} autocomplete="off" id="select-library-owner-form">
|
||||||
<p class="p-5 text-sm">NOTE: This cannot be changed later!</p>
|
<p class="p-5 text-sm">{$t('admin.note_cannot_be_changed_later')}</p>
|
||||||
|
|
||||||
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
<SettingSelect bind:value={ownerId} options={userOptions} name="user" />
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
export let unarchive = false;
|
export let unarchive = false;
|
||||||
|
|
||||||
$: text = unarchive ? $t('unarchive') : $t('archive');
|
$: text = unarchive ? $t('unarchive') : $t('to_archive');
|
||||||
$: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
|
$: icon = unarchive ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
export let menuItem = false;
|
export let menuItem = false;
|
||||||
export let removeFavorite: boolean;
|
export let removeFavorite: boolean;
|
||||||
|
|
||||||
$: text = removeFavorite ? $t('remove_from_favorites') : $t('favorite');
|
$: text = removeFavorite ? $t('remove_from_favorites') : $t('to_favorite');
|
||||||
$: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
|
$: icon = removeFavorite ? mdiHeartMinusOutline : mdiHeartOutline;
|
||||||
|
|
||||||
let loading = false;
|
let loading = false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script lang="ts" context="module">
|
<script lang="ts" context="module">
|
||||||
import { createContext } from '$lib/utils/context';
|
import { createContext } from '$lib/utils/context';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export interface AssetControlContext {
|
export interface AssetControlContext {
|
||||||
// Wrap assets in a function, because context isn't reactive.
|
// Wrap assets in a function, because context isn't reactive.
|
||||||
@@ -32,7 +33,8 @@
|
|||||||
|
|
||||||
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
|
<ControlAppBar on:close={clearSelect} backIcon={mdiClose} tailwindClasses="bg-white shadow-md">
|
||||||
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
<p class="font-medium text-immich-primary dark:text-immich-dark-primary" slot="leading">
|
||||||
Selected {assets.size.toLocaleString($locale)}
|
{$t('selected')}
|
||||||
|
{assets.size.toLocaleString($locale)}
|
||||||
</p>
|
</p>
|
||||||
<slot slot="trailing" />
|
<slot slot="trailing" />
|
||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
|
|||||||
@@ -78,7 +78,7 @@
|
|||||||
<Button color="dark-gray" size="sm" shadow={false} border>
|
<Button color="dark-gray" size="sm" shadow={false} border>
|
||||||
<div class="flex place-content-center place-items-center gap-2 px-2">
|
<div class="flex place-content-center place-items-center gap-2 px-2">
|
||||||
<Icon path={mdiCog} size="18" />
|
<Icon path={mdiCog} size="18" />
|
||||||
Account Settings
|
{$t('account_settings')}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
</a>
|
</a>
|
||||||
@@ -91,7 +91,7 @@
|
|||||||
on:click={() => dispatch('logout')}
|
on:click={() => dispatch('logout')}
|
||||||
>
|
>
|
||||||
<Icon path={mdiLogout} size={24} />
|
<Icon path={mdiLogout} size={24} />
|
||||||
Sign Out</button
|
{$t('sign_out')}</button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -93,7 +93,7 @@
|
|||||||
? 'item text-immich-primary underline dark:text-immich-dark-primary'
|
? 'item text-immich-primary underline dark:text-immich-dark-primary'
|
||||||
: ''}
|
: ''}
|
||||||
>
|
>
|
||||||
Administration
|
{$t('administration')}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="block sm:hidden" aria-hidden="true">
|
<div class="block sm:hidden" aria-hidden="true">
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { locale } from '$lib/stores/preferences.store.js';
|
|
||||||
import { s } from '$lib/utils.js';
|
|
||||||
import { type AlbumCountResponseDto, getAlbumCount } from '@immich/sdk';
|
import { type AlbumCountResponseDto, getAlbumCount } from '@immich/sdk';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
export let albumCountType: keyof AlbumCountResponseDto;
|
export let albumCountType: keyof AlbumCountResponseDto;
|
||||||
|
|
||||||
@@ -19,6 +18,6 @@
|
|||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then data}
|
{:then data}
|
||||||
<div>
|
<div>
|
||||||
<p>{data[albumCountType].toLocaleString($locale)} Album{s(data[albumCountType])}</p>
|
<p>{$t('albums_count', { values: { count: data[albumCountType] } })}</p>
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user