merge main

This commit is contained in:
martabal
2024-05-26 22:14:36 +02:00
1208 changed files with 13676 additions and 34138 deletions
-2
View File
@@ -2,8 +2,6 @@ mobile/openapi/**/*.md -diff -merge
mobile/openapi/**/*.md linguist-generated=true mobile/openapi/**/*.md linguist-generated=true
mobile/openapi/**/*.dart -diff -merge mobile/openapi/**/*.dart -diff -merge
mobile/openapi/**/*.dart linguist-generated=true mobile/openapi/**/*.dart linguist-generated=true
mobile/openapi/.openapi-generator/FILES -diff -merge
mobile/openapi/.openapi-generator/FILES linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true mobile/lib/**/*.g.dart linguist-generated=true
+1 -1
View File
@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.19.3' flutter-version: '3.22.0'
cache: true cache: true
- name: Create the Keystore - name: Create the Keystore
@@ -0,0 +1,15 @@
name: PR Conventional Commit Validation
on:
pull_request:
types: [opened, synchronize, reopened, edited]
jobs:
validate-pr-title:
runs-on: ubuntu-latest
steps:
- name: PR Conventional Commit Validation
uses: ytanikin/PRConventionalCommits@1.2.0
with:
task_types: '["feat","fix","docs","test","ci","refactor","perf","chore","revert"]'
add_label: 'false'
+2 -2
View File
@@ -22,8 +22,8 @@ jobs:
- name: Setup Flutter SDK - name: Setup Flutter SDK
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: "stable" channel: 'stable'
flutter-version: "3.19.3" flutter-version: '3.22.0'
- name: Install dependencies - name: Install dependencies
run: dart pub get run: dart pub get
+14 -3
View File
@@ -208,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2 uses: subosito/flutter-action@v2
with: with:
channel: 'stable' channel: 'stable'
flutter-version: '3.19.3' flutter-version: '3.22.0'
- name: Run tests - name: Run tests
working-directory: ./mobile working-directory: ./mobile
run: flutter test -j 1 run: flutter test -j 1
@@ -260,9 +260,18 @@ jobs:
name: OpenAPI Clients name: OpenAPI Clients
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - name: Checkout code
uses: actions/checkout@v4
- name: Install server dependencies
run: npm --prefix=server ci
- name: Build the app
run: npm --prefix=server run build
- name: Run API generation - name: Run API generation
run: make open-api run: make open-api
- name: Find file changes - name: Find file changes
uses: tj-actions/verify-changed-files@v20 uses: tj-actions/verify-changed-files@v20
id: verify-changed-files id: verify-changed-files
@@ -270,6 +279,8 @@ jobs:
files: | files: |
mobile/openapi mobile/openapi
open-api/typescript-sdk open-api/typescript-sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed - name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true' if: steps.verify-changed-files.outputs.files_changed == 'true'
run: | run: |
@@ -332,7 +343,7 @@ jobs:
exit 1 exit 1
- name: Run SQL generation - name: Run SQL generation
run: npm run sql:generate run: npm run sync:sql
env: env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich DB_URL: postgres://postgres:postgres@localhost:5432/immich
+4 -1
View File
@@ -14,7 +14,10 @@ mobile/gradle.properties
mobile/openapi/pubspec.lock mobile/openapi/pubspec.lock
mobile/*.jks mobile/*.jks
mobile/libisar.dylib mobile/libisar.dylib
mobile/openapi/test
mobile/openapi/doc
mobile/openapi/.openapi-generator/FILES
open-api/typescript-sdk/build open-api/typescript-sdk/build
mobile/android/fastlane/report.xml mobile/android/fastlane/report.xml
mobile/ios/fastlane/report.xml mobile/ios/fastlane/report.xml
+12 -2
View File
@@ -1,6 +1,16 @@
{ {
"editor.formatOnSave": true, "editor.formatOnSave": true,
"[javascript][typescript][css]": { "[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2,
"editor.formatOnSave": true
},
"[css]": {
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.tabSize": 2, "editor.tabSize": 2,
"editor.formatOnSave": true "editor.formatOnSave": true
@@ -31,4 +41,4 @@
"explorer.fileNesting.patterns": { "explorer.fileNesting.patterns": {
"*.ts": "${capture}.spec.ts,${capture}.mock.ts" "*.ts": "${capture}.spec.ts,${capture}.mock.ts"
} }
} }
+1 -1
View File
@@ -37,7 +37,7 @@ open-api-typescript:
cd ./open-api && bash ./bin/generate-open-api.sh typescript cd ./open-api && bash ./bin/generate-open-api.sh typescript
sql: sql:
npm --prefix server run sql:generate npm --prefix server run sync:sql
attach-server: attach-server:
docker exec -it docker_immich-server_1 sh docker exec -it docker_immich-server_1 sh
+1
View File
@@ -72,6 +72,7 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
``` ```
## Activities ## Activities
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image") ![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Features ## Features
+1 -1
View File
@@ -1 +1 @@
v20.12 20.13
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:fac6f741d51194c175c517f66bc3125588313327ad7e0ecd673e161e4fa807f3 as core FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+204 -140
View File
@@ -31,7 +31,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0", "eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
@@ -47,14 +47,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.103.1", "version": "1.105.1",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.12.8", "@types/node": "^20.12.12",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -174,9 +174,9 @@
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -1113,12 +1113,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/lodash": { "node_modules/@types/lodash": {
"version": "4.17.0", "version": "4.17.0",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz", "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.0.tgz",
@@ -1144,10 +1138,11 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.8", "version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@@ -1158,28 +1153,21 @@
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
"dev": true "dev": true
}, },
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.8.0", "@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.8.0", "@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
"engines": { "engines": {
@@ -1200,15 +1188,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1228,13 +1217,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0" "@typescript-eslint/visitor-keys": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1245,13 +1235,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.8.0", "@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1272,10 +1263,11 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
}, },
@@ -1285,13 +1277,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1313,18 +1306,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15", "@typescript-eslint/scope-manager": "7.9.0",
"@types/semver": "^7.5.8", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0"
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.8.0",
"semver": "^7.6.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1338,12 +1329,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -1361,9 +1353,9 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==", "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
@@ -1384,17 +1376,17 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"vitest": "1.5.3" "vitest": "1.6.0"
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==", "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/spy": "1.5.3", "@vitest/spy": "1.6.0",
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"chai": "^4.3.10" "chai": "^4.3.10"
}, },
"funding": { "funding": {
@@ -1402,12 +1394,12 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==", "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"p-limit": "^5.0.0", "p-limit": "^5.0.0",
"pathe": "^1.1.1" "pathe": "^1.1.1"
}, },
@@ -1443,9 +1435,9 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==", "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
@@ -1457,9 +1449,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==", "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"tinyspy": "^2.2.0" "tinyspy": "^2.2.0"
@@ -1469,9 +1461,9 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==", "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"diff-sequences": "^29.6.3", "diff-sequences": "^29.6.3",
@@ -1564,6 +1556,7 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1588,6 +1581,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
@@ -1822,12 +1816,12 @@
"dev": true "dev": true
}, },
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.36.0", "version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"browserslist": "^4.22.3" "browserslist": "^4.23.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -1897,6 +1891,7 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-type": "^4.0.0" "path-type": "^4.0.0"
}, },
@@ -2094,17 +2089,17 @@
} }
}, },
"node_modules/eslint-plugin-unicorn": { "node_modules/eslint-plugin-unicorn": {
"version": "52.0.0", "version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==", "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.22.20", "@babel/helper-validator-identifier": "^7.24.5",
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^2.1.4", "@eslint/eslintrc": "^3.0.2",
"ci-info": "^4.0.0", "ci-info": "^4.0.0",
"clean-regexp": "^1.0.0", "clean-regexp": "^1.0.0",
"core-js-compat": "^3.34.0", "core-js-compat": "^3.37.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"indent-string": "^4.0.0", "indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1", "is-builtin-module": "^3.2.1",
@@ -2113,11 +2108,11 @@
"read-pkg-up": "^7.0.1", "read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27", "regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0", "regjsparser": "^0.10.0",
"semver": "^7.5.4", "semver": "^7.6.1",
"strip-indent": "^3.0.0" "strip-indent": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=18.18"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2126,6 +2121,92 @@
"eslint": ">=8.56.0" "eslint": ">=8.56.0"
} }
}, },
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2466,6 +2547,7 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"array-union": "^2.1.0", "array-union": "^2.1.0",
"dir-glob": "^3.0.1", "dir-glob": "^3.0.1",
@@ -2969,6 +3051,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
@@ -3232,6 +3315,7 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -3711,13 +3795,10 @@
} }
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.0", "version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true, "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@@ -3725,18 +3806,6 @@
"node": ">=10" "node": ">=10"
} }
}, },
"node_modules/semver/node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -3781,6 +3850,7 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -4265,9 +4335,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.3.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==", "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
@@ -4306,16 +4376,16 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.3.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==", "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/expect": "1.5.3", "@vitest/expect": "1.6.0",
"@vitest/runner": "1.5.3", "@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.5.3", "@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.5.3", "@vitest/spy": "1.6.0",
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2", "acorn-walk": "^8.3.2",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -4329,7 +4399,7 @@
"tinybench": "^2.5.1", "tinybench": "^2.5.1",
"tinypool": "^0.8.3", "tinypool": "^0.8.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-node": "1.5.3", "vite-node": "1.6.0",
"why-is-node-running": "^2.2.2" "why-is-node-running": "^2.2.2"
}, },
"bin": { "bin": {
@@ -4344,8 +4414,8 @@
"peerDependencies": { "peerDependencies": {
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.5.3", "@vitest/browser": "1.6.0",
"@vitest/ui": "1.5.3", "@vitest/ui": "1.6.0",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
@@ -4407,12 +4477,6 @@
"integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
"dev": true "dev": true
}, },
"node_modules/yallist": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz",
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true
},
"node_modules/yaml": { "node_modules/yaml": {
"version": "2.4.2", "version": "2.4.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz",
+2 -2
View File
@@ -28,7 +28,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0", "eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"volta": { "volta": {
"node": "20.12.2" "node": "20.13.1"
} }
} }
+2 -3
View File
@@ -1,4 +1,4 @@
import { defaults, getMyUserInfo, isHttpError } from '@immich/sdk'; import { getMyUserInfo, init, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob'; import { glob } from 'fast-glob';
import { createHash } from 'node:crypto'; import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs'; import { createReadStream } from 'node:fs';
@@ -46,8 +46,7 @@ export const connect = async (url: string, key: string) => {
// noop // noop
} }
defaults.baseUrl = url; init({ baseUrl: url, apiKey: key });
defaults.headers = { 'x-api-key': key };
const [error] = await withError(getMyUserInfo()); const [error] = await withError(getMyUserInfo());
if (isHttpError(error)) { if (isHttpError(error)) {
+31 -38
View File
@@ -4,32 +4,29 @@
name: immich-dev name: immich-dev
x-server-build: &server-common
image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: ['/usr/src/app/bin/immich-dev', 'immich'] command: ['/usr/src/app/bin/immich-dev']
<<: *server-common image: immich-server-dev:latest
build:
context: ../
dockerfile: server/Dockerfile
target: dev
restart: always
volumes:
- ../server:/usr/src/app
- ../open-api:/usr/src/open-api
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
- /usr/src/app/node_modules
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ulimits:
nofile:
soft: 1048576
hard: 1048576
ports: ports:
- 3001:3001 - 3001:3001
- 9230:9230 - 9230:9230
@@ -37,19 +34,6 @@ services:
- redis - redis
- database - database
immich-microservices:
container_name: immich_microservices
command: ['/usr/src/app/bin/immich-dev', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
ports:
- 9231:9230
depends_on:
- database
- immich-server
immich-web: immich-web:
container_name: immich_web container_name: immich_web
image: immich-web-dev:latest image: immich-web-dev:latest
@@ -97,7 +81,9 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
database: database:
container_name: immich_postgres container_name: immich_postgres
@@ -108,11 +94,18 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes: volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
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
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
# set IMMICH_METRICS=true in .env to enable metrics # set IMMICH_METRICS=true in .env to enable metrics
# immich-prometheus: # immich-prometheus:
# container_name: immich_prometheus # container_name: immich_prometheus
+22 -28
View File
@@ -1,39 +1,23 @@
name: immich-prod name: immich-prod
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
restart: always
services: services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
command: ['start.sh', 'immich'] image: immich-server:latest
<<: *server-common build:
context: ../
dockerfile: server/Dockerfile
volumes:
- ${UPLOAD_LOCATION}/photos:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports: ports:
- 2283:3001 - 2283:3001
depends_on: depends_on:
- redis - redis
- database - database
restart: always
immich-microservices:
container_name: immich_microservices
command: ['start.sh', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
depends_on:
- redis
- database
- immich-server
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
@@ -54,7 +38,9 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
restart: always restart: always
database: database:
@@ -66,10 +52,18 @@ services:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes: volumes:
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data - ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
ports: ports:
- 5432:5432 - 5432:5432
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
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
# set IMMICH_METRICS=true in .env to enable metrics # set IMMICH_METRICS=true in .env to enable metrics
immich-prometheus: immich-prometheus:
@@ -88,7 +82,7 @@ services:
command: ['./run.sh', '-disable-reporting'] command: ['./run.sh', '-disable-reporting']
ports: ports:
- 3000:3000 - 3000:3000
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958 image: grafana/grafana:11.0.0-ubuntu@sha256:02e99d1ee0b52dc9d3000c7b5314e7a07e0dfd69cc49bb3f8ce323491ed3406b
volumes: volumes:
- grafana-data:/var/lib/grafana - grafana-data:/var/lib/grafana
+11 -20
View File
@@ -12,7 +12,6 @@ services:
immich-server: immich-server:
container_name: immich_server container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release} image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: ['start.sh', 'immich']
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro - /etc/localtime:/etc/localtime:ro
@@ -25,23 +24,6 @@ services:
- database - database
restart: always restart: always
immich-microservices:
container_name: immich_microservices
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
depends_on:
- redis
- database
restart: always
immich-machine-learning: immich-machine-learning:
container_name: immich_machine_learning container_name: immich_machine_learning
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag. # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
@@ -58,18 +40,27 @@ services:
redis: redis:
container_name: immich_redis container_name: immich_redis
image: registry.hub.docker.com/library/redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 image: docker.io/redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
healthcheck:
test: redis-cli ping || exit 1
restart: always restart: always
database: database:
container_name: immich_postgres container_name: immich_postgres
image: registry.hub.docker.com/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
environment: environment:
POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_PASSWORD: ${DB_PASSWORD}
POSTGRES_USER: ${DB_USERNAME} POSTGRES_USER: ${DB_USERNAME}
POSTGRES_DB: ${DB_DATABASE_NAME} POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes: volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data - ${DB_DATA_LOCATION}:/var/lib/postgresql/data
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
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always restart: always
volumes: volumes:
+2 -4
View File
@@ -1,9 +1,7 @@
version: "3.8"
# Configurations for hardware-accelerated machine learning # Configurations for hardware-accelerated machine learning
# If using Unraid or another platform that doesn't allow multiple Compose files, # If using Unraid or another platform that doesn't allow multiple Compose files,
# you can inline the config for a backend by copying its contents # you can inline the config for a backend by copying its contents
# into the immich-machine-learning service in the docker-compose.yml file. # into the immich-machine-learning service in the docker-compose.yml file.
# See https://immich.app/docs/features/ml-hardware-acceleration for info on usage. # See https://immich.app/docs/features/ml-hardware-acceleration for info on usage.
@@ -30,7 +28,7 @@ services:
openvino: openvino:
device_cgroup_rules: device_cgroup_rules:
- "c 189:* rmw" - 'c 189:* rmw'
devices: devices:
- /dev/dri:/dev/dri - /dev/dri:/dev/dri
volumes: volumes:
-2
View File
@@ -1,5 +1,3 @@
version: "3.8"
# Configurations for hardware-accelerated transcoding # Configurations for hardware-accelerated transcoding
# If using Unraid or another platform that doesn't allow multiple Compose files, # If using Unraid or another platform that doesn't allow multiple Compose files,
+1 -1
View File
@@ -1 +1 @@
v20.12 20.13
+5 -5
View File
@@ -5,13 +5,13 @@ This website is built using [Docusaurus](https://docusaurus.io/), a modern stati
### Installation ### Installation
``` ```
$ yarn $ npm install
``` ```
### Local Development ### Local Development
``` ```
$ yarn start $ npm run start
``` ```
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
@@ -19,7 +19,7 @@ This command starts a local development server and opens up a browser window. Mo
### Build ### Build
``` ```
$ yarn build $ npm run build
``` ```
This command generates static content into the `build` directory and can be served using any static contents hosting service. This command generates static content into the `build` directory and can be served using any static contents hosting service.
@@ -29,13 +29,13 @@ This command generates static content into the `build` directory and can be serv
Using SSH: Using SSH:
``` ```
$ USE_SSH=true yarn deploy $ USE_SSH=true npm run deploy
``` ```
Not using SSH: Not using SSH:
``` ```
$ GIT_USER=<Your GitHub username> yarn deploy $ GIT_USER=<Your GitHub username> npm run deploy
``` ```
If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch.
+41 -2
View File
@@ -399,8 +399,47 @@ If it mentions SIGILL (note the lack of a K) or error code 132, it most likely m
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job. If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
### Why does Immich log migration errors on startup? ## Database
Sometimes Immich logs errors such as "duplicate key value violates unique constraint" or "column (...) of relation (...) already exists". Because of Immich's container structure, this error can be seen when both immich and immich-microservices start at the same time and attempt to migrate or create the database structure. Since the database migration is run sequentially and inside of transactions, this error message does not cause harm to your installation of Immich and can safely be ignored. If needed, you can manually restart Immich by running `docker restart immich immich-microservices`. ### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
### How can I verify the integrity of my database?
If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
A result of `on` means that checksums are enabled.
<details>
<summary>Check if checksums are enabled</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
data_checksums
----------------
on
(1 row)
```
</details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
<details>
<summary>Check for database corruption</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
datname | checksum_failures | checksum_last_failure
-----------+-------------------+-----------------------
postgres | 0 |
immich | 0 |
template1 | 0 |
template0 | 0 |
(4 rows)
```
</details>
[huggingface]: https://huggingface.co/immich-app [huggingface]: https://huggingface.co/immich-app
@@ -55,7 +55,7 @@ docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them. docker compose create # Create Docker containers for Immich apps without running them.
docker start immich_postgres # Start Postgres server docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up sleep 10 # Wait for Postgres server to start up
gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup gc "C:\path\to\backup\dump.sql" | docker exec -i immich_postgres psql --username=postgres # Restore Backup
docker compose up -d # Start remainder of Immich apps docker compose up -d # Start remainder of Immich apps
``` ```
@@ -5,7 +5,7 @@ While not officially recommended, it is possible to run Immich using a pre-exist
By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich. By default, Immich expects superuser permission on the Postgres database and requires certain extensions to be installed. This guide outlines the steps required to prepare a pre-existing Postgres server to be used by Immich.
:::tip :::tip
Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication, data page checksums, and streaming write-ahead log backups using programs like pgBackRest or Barman. Running with a pre-existing Postgres server can unlock powerful administrative features, including logical replication and streaming write-ahead log backups using programs like pgBackRest or Barman.
::: :::
## Prerequisites ## Prerequisites
+1 -1
View File
@@ -18,7 +18,7 @@ In any other situation, there are 3 different options that can appear:
- MATCHES - These files are matched by their checksums. - MATCHES - These files are matched by their checksums.
- OFFLINE PATHS - These files are the result of manually deleting files in the upload library or a failed file move in the past (losing track of a file). - OFFLINE PATHS - These files are the result of manually deleting files from immich or a failed file move in the past (losing track of a file).
- UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug. - UNTRACKED FILES - These files are not tracked by the application. They can be the result of failed moves, interrupted uploads, or left behind due to a bug.
+5
View File
@@ -9,6 +9,11 @@ When contributing code through a pull request, please check the following:
- [ ] `npm run check:svelte` (Type checking via SvelteKit) - [ ] `npm run check:svelte` (Type checking via SvelteKit)
- [ ] `npm test` (unit tests) - [ ] `npm test` (unit tests)
## Documentation
- [ ] `npm run format` (formatting via Prettier)
- [ ] Update the `_redirects` file if you have renamed a page or removed it from the documentation.
:::tip AIO :::tip AIO
Run all web checks with `npm run check:all` Run all web checks with `npm run check:all`
::: :::
+5 -3
View File
@@ -22,7 +22,8 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
- WSL2 does not support Quick Sync. - WSL2 does not support Quick Sync.
- Raspberry Pi is currently not supported. - Raspberry Pi is currently not supported.
- Two-pass mode is only supported for NVENC. Other APIs will ignore this setting. - Two-pass mode is only supported for NVENC. Other APIs will ignore this setting.
- Only encoding is currently hardware accelerated, so the CPU is still used for software decoding and tone-mapping. - By default, only encoding is currently hardware accelerated. This means the CPU is still used for software decoding and tone-mapping.
- NVENC and RKMPP can be fully accelerated by enabling hardware decoding in the video transcoding settings.
- Hardware dependent - Hardware dependent
- Codec support varies, but H.264 and HEVC are usually supported. - Codec support varies, but H.264 and HEVC are usually supported.
- Notably, NVIDIA and AMD GPUs do not support VP9 encoding. - Notably, NVIDIA and AMD GPUs do not support VP9 encoding.
@@ -33,7 +34,7 @@ You do not need to redo any transcoding jobs after enabling hardware acceleratio
#### NVENC #### NVENC
- You must have the official NVIDIA driver installed on the server. - You must have the official NVIDIA driver installed on the server.
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed. - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### QSV #### QSV
@@ -65,6 +66,7 @@ For RKMPP to work:
3. Redeploy the `immich-microservices` container with these updated settings. 3. Redeploy the `immich-microservices` 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.
#### Single Compose File #### Single Compose File
@@ -122,7 +124,7 @@ Once this is done, you can continue to step 3 of "Basic Setup".
- While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices - While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/ [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux [jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations [jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases [libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
+1 -5
View File
@@ -4,13 +4,9 @@
Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries. Immich supports the creation of libraries which is a top-level asset container. Currently, there are two types of libraries: traditional upload libraries that can sync with a mobile device, and external libraries, that keeps up to date with files on disk. Libraries are different from albums in that an asset can belong to multiple albums but only one library, and deleting a library deletes all assets contained within. As of August 2023, this is a new feature and libraries have a lot of potential for future development beyond what is documented here. This document attempts to describe the current state of libraries.
## The Upload Library
Immich comes preconfigured with an upload library for each user. All assets uploaded to Immich are added to this library. This library can be renamed, but not deleted. The upload library is the only library that can be synced with a mobile device. No items in an upload library is allowed to have the same sha1 hash as another item in the same library in order to prevent duplicates.
## External Libraries ## External Libraries
External libraries tracks assets stored outside of immich, i.e. in the file system. Immich will only read data from the files, and will not modify them in any way. Therefore, the delete button is disabled for external assets. When the external library is scanned, immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc. External libraries tracks assets stored outside of Immich, i.e. in the file system. When the external library is scanned, Immich will read the metadata from the file and create an asset in the library for each image or video file. These items will then be shown in the main timeline, and they will look and behave like any other asset, including viewing on the map, adding to albums, etc.
If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case: If a file is modified outside of Immich, the changes will not be reflected in immich until the library is scanned again. There are different ways to scan a library depending on the use case:
@@ -38,7 +38,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- The GPU must have compute capability 5.2 or greater. - The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed. - The server must have the official NVIDIA driver installed.
- The installed driver must be >= 535 (it must support CUDA 12.2). - The installed driver must be >= 535 (it must support CUDA 12.2).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Runtime][nvcr] installed. - On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### OpenVINO #### OpenVINO
@@ -95,11 +95,11 @@ immich-machine-learning:
Once this is done, you can redeploy the `immich-machine-learning` container. Once this is done, you can redeploy the `immich-machine-learning` container.
:::info :::info
You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully. You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for CUDA, `intel_gpu_top` for OpenVINO, etc.). You can also enable debug logging by setting `IMMICH_LOG_LEVEL=debug` in the `.env` file and restarting the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, you should see a log for `Available ORT providers` containing the relevant provider. In the case of ARM NN, the absence of a `Could not load ANN shared libraries` log entry means it loaded successfully.
::: :::
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml
[nvcr]: https://github.com/NVIDIA/nvidia-container-runtime/ [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
## Tips ## Tips
+38 -34
View File
@@ -2,48 +2,52 @@
A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich. A short guide on connecting [pgAdmin](https://www.pgadmin.org/) to Immich.
:::note
In order to connect to the database the immich_postgres container **must be running**.
The passwords and usernames used below match the ones specified in the example `.env` file. If changed, please use actual values instead.
**Optional:** To connect to the database **outside** of your Docker's network:
- Expose port 5432 in your `docker-compose.yml` file.
- Edit the PostgreSQL [`pg_hba.conf`](https://www.postgresql.org/docs/current/auth-pg-hba-conf.html) file.
- Make sure your firewall does not block access to port 5432.
Note that exposing the database port increases the risk of getting attacked by hackers.
Make sure to remove the binding port after finishing the database's tasks.
:::
## 1. Install pgAdmin ## 1. Install pgAdmin
Download and install [pgAdmin](https://www.pgadmin.org/download/) following the official documentation. Add a file `docker-compose-pgadmin.yml` next to your `docker-compose.yml` with the following content:
```
name: immich
services:
pgadmin:
image: dpage/pgadmin4
container_name: pgadmin4_container
restart: always
ports:
- "8888:80"
environment:
PGADMIN_DEFAULT_EMAIL: user-name@domain-name.com
PGADMIN_DEFAULT_PASSWORD: strong-password
volumes:
- pgadmin-data:/var/lib/pgadmin
volumes:
pgadmin-data:
```
Change the values of `PGADMIN_DEFAULT_EMAIL` and `PGADMIN_DEFAULT_PASSWORD` in this file.
Run `docker compose -f docker-compose.yml -f docker-compose-pgadmin.yml up` to start immich along with `pgAdmin`.
## 2. Add a Server ## 2. Add a Server
Open pgAdmin and click "Add New Server". Open [localhost:8888](http://localhost:8888) and login with the default credentials from above.
<img src={require('./img/add-new-server-option.png').default} width="50%" title="new server option" /> Right click on `Servers` and click on `Register >> Server..` then enter the values below in the `Connection` tab.
## 3. Enter Connection Details <img src={require('./img/pgadmin-add-new-server.png').default} width="50%" title="new server option" />
| Name | Value | :::note
| -------------------- | ----------- | The parameters used here match those specified in the example `.env` file. If you have changed your `.env` file, you'll need to adjust accordingly.
| Host name/address | `localhost` | :::
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
<img src={require('./img/Connection-Pgadmin.png').default} width="75%" title="Connection" /> | Name | Value |
| -------------------- | ----------------- |
## 4. Save Connection | Host name/address | `immich_postgres` |
| Port | `5432` |
| Maintenance database | `immich` |
| Username | `postgres` |
| Password | `postgres` |
Click on "Save" to connect to the Immich database. Click on "Save" to connect to the Immich database.
:::tip
View [Database Queries](/docs/guides/database-queries/) for common database queries.
:::
+1 -1
View File
@@ -96,7 +96,7 @@ SELECT * FROM "users";
## System Config ## System Config
```sql title="Custom settings" ```sql title="Custom settings"
SELECT "key", "value" FROM "system_config"; SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
``` ```
(Only used when not using the [config file](/docs/install/config-file)) (Only used when not using the [config file](/docs/install/config-file))
+9 -5
View File
@@ -6,15 +6,19 @@ in a directory on the same machine.
# Mount the directory into the containers. # Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points under `volumes:` Edit `docker-compose.yml` to add two new mount points in the sections `immich-server:` and `immich-microservices:` under `volumes:`
``` ```diff
immich-server: immich-server:
volumes: volumes:
- ${EXTERNAL_PATH}:/usr/src/app/external + - ${EXTERNAL_PATH}:/usr/src/app/external
immich-microservices:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
``` ```
Be sure to add exactly the same line to both `immich-server:` and `immich-microservices:`. Be sure to add exactly the same path to both services.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer: Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

+1 -1
View File
@@ -15,7 +15,7 @@ The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/downlo
::: :::
```yaml ```yaml
version: '3.8' name: immich_remote_ml
services: services:
immich-machine-learning: immich-machine-learning:
+4 -3
View File
@@ -77,6 +77,10 @@ The default configuration looks like this:
"enabled": true, "enabled": true,
"modelName": "ViT-B-32__openai" "modelName": "ViT-B-32__openai"
}, },
"duplicateDetection": {
"enabled": false,
"maxDistance": 0.03
},
"facialRecognition": { "facialRecognition": {
"enabled": true, "enabled": true,
"modelName": "buffalo_l", "modelName": "buffalo_l",
@@ -153,9 +157,6 @@ The default configuration looks like this:
"server": { "server": {
"externalDomain": "", "externalDomain": "",
"loginPageMessage": "" "loginPageMessage": ""
},
"user": {
"deleteDelay": 7
} }
} }
``` ```
+15 -11
View File
@@ -24,18 +24,25 @@ If this should not work, try running `docker compose up -d --force-recreate`.
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database | | `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
:::tip :::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly. These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
::: :::
### Supported filesystems
The Immich Postgres database (`DB_DATA_LOCATION`) must be located on a filesystem that supports user/group
ownership and permissions (EXT2/3/4, ZFS, APFS, BTRFS, XFS, etc.). It will not work on any filesystem formatted in NTFS or ex/FAT/32.
It will not work in WSL (Windows Subsystem for Linux) when using a mounted host directory (commonly under `/mnt`).
If this is an issue, you can change the bind mount to a Docker volume instead.
Regardless of filesystem, it is not recommended to use a network share for your database location due to performance and possible data loss issues.
## General ## General
| Variable | Description | Default | Services | | Variable | Description | Default | Services |
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- | | :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
| `TZ` | Timezone | | microservices | | `TZ` | Timezone | | microservices |
| `NODE_ENV` | Environment (production, development) | `production` | server, microservices, machine learning | | `IMMICH_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning | | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices | | `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | | `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
@@ -52,13 +59,10 @@ It only need to be set if the Immich deployment method is changing.
## Ports ## Ports
| Variable | Description | Default | Services | | Variable | Description | Default |
| :---------------------- | :-------------------- | :-------: | :-------------------- | | :------------ | :------------- | :------------------------------------: |
| `HOST` | Host | `0.0.0.0` | server, microservices | | `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `SERVER_PORT` | Server Port | `3001` | server | | `IMMICH_PORT` | Listening port | 3001 (server), 3003 (machine learning) |
| `MICROSERVICES_PORT` | Microservices Port | `3002` | microservices |
| `MACHINE_LEARNING_HOST` | Machine Learning Host | `0.0.0.0` | machine learning |
| `MACHINE_LEARNING_PORT` | Machine Learning Port | `3003` | machine learning |
## Database ## Database
+3 -2
View File
@@ -38,8 +38,9 @@ style={{border: '1px solid #ddd'}}
alt="Dot Env Example" alt="Dot Env Example"
/> />
- Populate custom database information if necessary. - Change the default `DB_PASSWORD`, and add custom database connection information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. - Change `DB_DATA_LOCATION` to a folder where the database will be saved to disk.
- Change `UPLOAD_LOCATION` to a folder where media (uploaded and generated) will be stored.
11. Click on "**Deploy the stack**". 11. Click on "**Deploy the stack**".
+10 -7
View File
@@ -15,12 +15,15 @@ Hardware and software requirements for Immich
Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich. Immich requires the command `docker compose` - the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer compatible with Immich.
::: :::
:::info Podman
You can also use Podman to run the application. However, additional configuration might be required.
:::
## Hardware ## Hardware
- **OS**: Preferred unix-based operating system (Ubuntu, Debian, MacOS, etc). Windows works too, with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) - **OS**: Recommended Linux operating system (Ubuntu, Debian, etc).
- **RAM**: At least 4GB, preferred 6GB. - Windows is supported with [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
- **CPU**: At least 2 cores, preferred 4 cores. - macOS is supported with [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
- **RAM**: Minimum 4GB, recommended 6GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.
- This can present an issue for Windows users. See [here](/docs/install/environment-variables#supported-filesystems)
for more details and alternatives.
- The generation of thumbnails and transcoded video can increase the size of the photo library by 10-20% on average.
- Network shares are supported for the storage of image and video assets only.
+5
View File
@@ -119,6 +119,11 @@ const config = {
label: 'GitHub', label: 'GitHub',
position: 'right', position: 'right',
}, },
{
href: 'https://discord.gg/D8JsnBEuKb',
label: 'Discord',
position: 'right',
},
], ],
}, },
footer: { footer: {
+597 -678
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -56,6 +56,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "20.12.2" "node": "20.13.1"
} }
} }
+9 -4
View File
@@ -10,17 +10,17 @@ interface CommunityGuidesProps {
const guides: CommunityGuidesProps[] = [ const guides: CommunityGuidesProps[] = [
{ {
title: 'Cloudflare Tunnels with SSO/OAuth', title: 'Cloudflare Tunnels with SSO/OAuth',
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`, description: `Setting up Cloudflare Tunnels and a SaaS App for Immich.`,
url: 'https://github.com/immich-app/immich/discussions/8299', url: 'https://github.com/immich-app/immich/discussions/8299',
}, },
{ {
title: 'Database backup in Truenas', title: 'Database backup in TrueNAS',
description: `Create a database backup with pgAdmin in Truenas.`, description: `Create a database backup with pgAdmin in TrueNAS.`,
url: 'https://github.com/immich-app/immich/discussions/8809', url: 'https://github.com/immich-app/immich/discussions/8809',
}, },
{ {
title: 'Unraid backup scripts', title: 'Unraid backup scripts',
description: `Back up your assets in Unarid with a pre-prepared script.`, description: `Back up your assets in Unraid with a pre-prepared script.`,
url: 'https://github.com/immich-app/immich/discussions/8416', url: 'https://github.com/immich-app/immich/discussions/8416',
}, },
{ {
@@ -33,6 +33,11 @@ const guides: CommunityGuidesProps[] = [
description: 'Documentation for simple podman setup using quadlets.', description: 'Documentation for simple podman setup using quadlets.',
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md', url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
}, },
{
title: 'Google Photos import + albums',
description: 'Import your Google Photos files into Immich and add your albums',
url: 'https://github.com/immich-app/immich/discussions/1340',
},
]; ];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element { function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
+6 -1
View File
@@ -10,7 +10,7 @@ interface CommunityProjectProps {
const projects: CommunityProjectProps[] = [ const projects: CommunityProjectProps[] = [
{ {
title: 'immich-go', title: 'immich-go',
description: `An alternative to the immich-CLI command that doesn't depend on nodejs installation. It tries its best for importing google photos takeout archives.`, description: `An alternative to the immich-CLI that doesn't depend on nodejs. It specializes in importing Google Photos Takeout archives.`,
url: 'https://github.com/simulot/immich-go', url: 'https://github.com/simulot/immich-go',
}, },
{ {
@@ -33,6 +33,11 @@ const projects: CommunityProjectProps[] = [
description: 'A Python script to create albums based on the folder structure of an external library.', description: 'A Python script to create albums based on the folder structure of an external library.',
url: 'https://github.com/Salvoxia/immich-folder-album-creator', url: 'https://github.com/Salvoxia/immich-folder-album-creator',
}, },
{
title: 'Immich-Tools',
description: 'Provides scripts for handling problems on the repair page.',
url: 'https://github.com/clumsyCoder00/Immich-Tools',
},
{ {
title: 'Lightroom Publisher: mi.Immich.Publisher', title: 'Lightroom Publisher: mi.Immich.Publisher',
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.', description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
+7
View File
@@ -33,6 +33,13 @@ function HomepageHeader() {
> >
Demo portal Demo portal
</Link> </Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://discord.gg/D8JsnBEuKb"
>
Discord
</Link>
</div> </div>
<img src="/img/immich-screenshots.png" alt="screenshots" width={'70%'} /> <img src="/img/immich-screenshots.png" alt="screenshots" width={'70%'} />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1"> <div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

+1 -1
View File
@@ -1 +1 @@
v20.12 20.13
+19 -27
View File
@@ -2,40 +2,32 @@ version: '3.8'
name: immich-e2e name: immich-e2e
x-server-build: &server-common
image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
depends_on:
- redis
- database
services: services:
immich-server: immich-server:
container_name: immich-e2e-server container_name: immich-e2e-server
command: ['./start.sh', 'immich'] command: ['./start.sh']
<<: *server-common image: immich-server:latest
build:
context: ../
dockerfile: server/Dockerfile
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
- DB_PASSWORD=postgres
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
depends_on:
- redis
- database
ports: ports:
- 2283:3001 - 2283:3001
immich-microservices:
container_name: immich-e2e-microservices
command: ['./start.sh', 'microservices']
<<: *server-common
redis: redis:
image: redis:6.2-alpine@sha256:84882e87b54734154586e5f8abd4dce69fe7311315e2fc6d67c29614c8de2672 image: redis:6.2-alpine@sha256:e31ca60b18f7e9b78b573d156702471d4eda038803c0b8e6f01559f350031e93
database: database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
+208 -151
View File
@@ -1,17 +1,17 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.103.1", "version": "1.105.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.103.1", "version": "1.105.1",
"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",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
@@ -23,7 +23,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0", "eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0", "exiftool-vendored": "^26.0.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
@@ -65,7 +65,7 @@
"eslint": "^8.56.0", "eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0", "eslint-plugin-unicorn": "^53.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^3.2.4",
@@ -81,7 +81,7 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.103.1", "version": "1.105.1",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
@@ -208,9 +208,9 @@
} }
}, },
"node_modules/@babel/helper-validator-identifier": { "node_modules/@babel/helper-validator-identifier": {
"version": "7.22.20", "version": "7.24.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz",
"integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==",
"dev": true, "dev": true,
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
@@ -971,12 +971,12 @@
} }
}, },
"node_modules/@playwright/test": { "node_modules/@playwright/test": {
"version": "1.43.1", "version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.43.1.tgz", "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-HgtQzFgNEEo4TE22K/X7sYTYNqEMMTZmFS8kTq6m8hXj+m1D8TgwgIbumHddJa9h4yl4GkKb8/bgAl2+g7eDgA==", "integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright": "1.43.1" "playwright": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -1217,12 +1217,6 @@
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
"dev": true "dev": true
}, },
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true
},
"node_modules/@types/luxon": { "node_modules/@types/luxon": {
"version": "3.4.2", "version": "3.4.2",
"resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz", "resolved": "https://registry.npmjs.org/@types/luxon/-/luxon-3.4.2.tgz",
@@ -1236,10 +1230,11 @@
"dev": true "dev": true
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.12.8", "version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.8.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-NU0rJLJnshZWdE/097cdCBbyW1h4hEg0xpovcoAQYHl8dnEyp/NAOiE45pvc+Bd1Dt+2r94v2eGFpQJ4R7g+2w==", "integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~5.26.4" "undici-types": "~5.26.4"
} }
@@ -1251,9 +1246,9 @@
"dev": true "dev": true
}, },
"node_modules/@types/pg": { "node_modules/@types/pg": {
"version": "8.11.5", "version": "8.11.6",
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.5.tgz", "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
"integrity": "sha512-2xMjVviMxneZHDHX5p5S6tsRRs7TpDHeeK7kTTMe/kAC/mRRNjWHjZg0rkiY+e17jXSZV3zJYDxXV8Cy72/Vuw==", "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@types/node": "*", "@types/node": "*",
@@ -1327,12 +1322,6 @@
"@types/node": "*" "@types/node": "*"
} }
}, },
"node_modules/@types/semver": {
"version": "7.5.8",
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz",
"integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==",
"dev": true
},
"node_modules/@types/superagent": { "node_modules/@types/superagent": {
"version": "8.1.3", "version": "8.1.3",
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.3.tgz",
@@ -1355,21 +1344,20 @@
} }
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-gFTT+ezJmkwutUPmB0skOj3GZJtlEGnlssems4AjkVweUPGj7jRwwqg0Hhg7++kPGJqKtTYx+R05Ftww372aIg==", "integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.8.0", "@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.8.0", "@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
"semver": "^7.6.0",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
"engines": { "engines": {
@@ -1390,15 +1378,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-KgKQly1pv0l4ltcftP59uQZCi4HUYswCLbTqVZEJu7uLX8CTLyswqMLqLN+2QFz4jCptqWVV4SB7vdxcH2+0kQ==", "integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1418,13 +1407,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-viEmZ1LmwsGcnr85gIq+FCYI7nO90DVbE37/ll51hjv9aG+YZMb4WDE2fyWpUR4O/UrhGRpYXK/XajcGTk2B8g==", "integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0" "@typescript-eslint/visitor-keys": "7.9.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1435,13 +1425,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-H70R3AefQDQpz9mGv13Uhi121FNMh+WEaRqcXTX09YEDky21km4dV1ZXJIp8QjXc4ZaVkXVdohvWDzbnbHDS+A==", "integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.8.0", "@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1462,10 +1453,11 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-wf0peJ+ZGlcH+2ZS23aJbOv+ztjeeP8uQ9GgwMJGVLx/Nj9CJt17GWgWWoSmoRVKAX2X+7fzEnAjxdvK2gqCLw==", "integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
}, },
@@ -1475,13 +1467,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-5pfUCOwK5yjPaJQNy44prjCwtr981dO8Qo9J9PwYXZ0MosgAbfEMB008dJ5sNo3+/BN6ytBPuSvXUg9SAqB0dg==", "integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.8.0", "@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1507,6 +1500,7 @@
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"balanced-match": "^1.0.0" "balanced-match": "^1.0.0"
} }
@@ -1516,6 +1510,7 @@
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==", "integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"brace-expansion": "^2.0.1" "brace-expansion": "^2.0.1"
}, },
@@ -1527,18 +1522,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-L0yFqOCflVqXxiZyXrDr80lnahQfSOfc9ELAAZ75sqicqp2i36kEZZGuUymHNFoYOqxRT05up760b4iGsl02nQ==", "integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@types/json-schema": "^7.0.15", "@typescript-eslint/scope-manager": "7.9.0",
"@types/semver": "^7.5.8", "@typescript-eslint/types": "7.9.0",
"@typescript-eslint/scope-manager": "7.8.0", "@typescript-eslint/typescript-estree": "7.9.0"
"@typescript-eslint/types": "7.8.0",
"@typescript-eslint/typescript-estree": "7.8.0",
"semver": "^7.6.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1552,12 +1545,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.8.0", "version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.8.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-q4/gibTNBQNA0lGyYQCmWRS5D15n8rXh4QjK3KV+MBPlTYHpfBUT3D3PaPR/HeNiI9W6R7FvlkcGhNyAoP+caA==", "integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.8.0", "@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -1575,9 +1569,9 @@
"dev": true "dev": true
}, },
"node_modules/@vitest/coverage-v8": { "node_modules/@vitest/coverage-v8": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz",
"integrity": "sha512-DPyGSu/fPHOJuPxzFSQoT4N/Fu/2aJfZRtEpEp8GI7NHsXBGE94CQ+pbEGBUMFjatsHPDJw/+TAF9r4ens2CNw==", "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@ampproject/remapping": "^2.2.1", "@ampproject/remapping": "^2.2.1",
@@ -1598,17 +1592,17 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
}, },
"peerDependencies": { "peerDependencies": {
"vitest": "1.5.3" "vitest": "1.6.0"
} }
}, },
"node_modules/@vitest/expect": { "node_modules/@vitest/expect": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz",
"integrity": "sha512-y+waPz31pOFr3rD7vWTbwiLe5+MgsMm40jTZbQE8p8/qXyBX3CQsIXRx9XK12IbY7q/t5a5aM/ckt33b4PxK2g==", "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/spy": "1.5.3", "@vitest/spy": "1.6.0",
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"chai": "^4.3.10" "chai": "^4.3.10"
}, },
"funding": { "funding": {
@@ -1616,12 +1610,12 @@
} }
}, },
"node_modules/@vitest/runner": { "node_modules/@vitest/runner": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz",
"integrity": "sha512-7PlfuReN8692IKQIdCxwir1AOaP5THfNkp0Uc4BKr2na+9lALNit7ub9l3/R7MP8aV61+mHKRGiqEKRIwu6iiQ==", "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"p-limit": "^5.0.0", "p-limit": "^5.0.0",
"pathe": "^1.1.1" "pathe": "^1.1.1"
}, },
@@ -1630,9 +1624,9 @@
} }
}, },
"node_modules/@vitest/snapshot": { "node_modules/@vitest/snapshot": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz",
"integrity": "sha512-K3mvIsjyKYBhNIDujMD2gfQEzddLe51nNOAf45yKRt/QFJcUIeTQd2trRvv6M6oCBHNVnZwFWbQ4yj96ibiDsA==", "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"magic-string": "^0.30.5", "magic-string": "^0.30.5",
@@ -1644,9 +1638,9 @@
} }
}, },
"node_modules/@vitest/spy": { "node_modules/@vitest/spy": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz",
"integrity": "sha512-Llj7Jgs6lbnL55WoshJUUacdJfjU2honvGcAJBxhra5TPEzTJH8ZuhI3p/JwqqfnTr4PmP7nDmOXP53MS7GJlg==", "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"tinyspy": "^2.2.0" "tinyspy": "^2.2.0"
@@ -1656,9 +1650,9 @@
} }
}, },
"node_modules/@vitest/utils": { "node_modules/@vitest/utils": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.3.tgz", "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz",
"integrity": "sha512-rE9DTN1BRhzkzqNQO+kw8ZgfeEBCLXiHJwetk668shmNBpSagQxneT5eSqEBLP+cqSiAeecvQmbpFfdMyLcIQA==", "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"diff-sequences": "^29.6.3", "diff-sequences": "^29.6.3",
@@ -1785,6 +1779,7 @@
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -1840,6 +1835,7 @@
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"fill-range": "^7.0.1" "fill-range": "^7.0.1"
}, },
@@ -2121,12 +2117,12 @@
"dev": true "dev": true
}, },
"node_modules/core-js-compat": { "node_modules/core-js-compat": {
"version": "3.36.0", "version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz", "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-iV9Pd/PsgjNWBXeq8XRtWVSgz2tKAfhfvBs7qxYty+RlRd+OCksaWmOnc4JKrTc1cToXL1N0s3l/vwlxPtdElw==", "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"browserslist": "^4.22.3" "browserslist": "^4.23.0"
}, },
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -2247,6 +2243,7 @@
"resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
"integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"path-type": "^4.0.0" "path-type": "^4.0.0"
}, },
@@ -2487,17 +2484,17 @@
} }
}, },
"node_modules/eslint-plugin-unicorn": { "node_modules/eslint-plugin-unicorn": {
"version": "52.0.0", "version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==", "integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@babel/helper-validator-identifier": "^7.22.20", "@babel/helper-validator-identifier": "^7.24.5",
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@eslint/eslintrc": "^2.1.4", "@eslint/eslintrc": "^3.0.2",
"ci-info": "^4.0.0", "ci-info": "^4.0.0",
"clean-regexp": "^1.0.0", "clean-regexp": "^1.0.0",
"core-js-compat": "^3.34.0", "core-js-compat": "^3.37.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"indent-string": "^4.0.0", "indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1", "is-builtin-module": "^3.2.1",
@@ -2506,11 +2503,11 @@
"read-pkg-up": "^7.0.1", "read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27", "regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0", "regjsparser": "^0.10.0",
"semver": "^7.5.4", "semver": "^7.6.1",
"strip-indent": "^3.0.0" "strip-indent": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=16" "node": ">=18.18"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
@@ -2519,6 +2516,70 @@
"eslint": ">=8.56.0" "eslint": ">=8.56.0"
} }
}, },
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"dev": true,
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.0",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"dev": true,
"dependencies": {
"acorn": "^8.11.3",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "7.2.2", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
@@ -2692,6 +2753,7 @@
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
"integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.stat": "^2.0.2",
"@nodelib/fs.walk": "^1.2.3", "@nodelib/fs.walk": "^1.2.3",
@@ -2708,6 +2770,7 @@
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true, "dev": true,
"license": "ISC",
"dependencies": { "dependencies": {
"is-glob": "^4.0.1" "is-glob": "^4.0.1"
}, },
@@ -2759,6 +2822,7 @@
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"to-regex-range": "^5.0.1" "to-regex-range": "^5.0.1"
}, },
@@ -3001,6 +3065,7 @@
"resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
"integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"array-union": "^2.1.0", "array-union": "^2.1.0",
"dir-glob": "^3.0.1", "dir-glob": "^3.0.1",
@@ -3276,6 +3341,7 @@
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.12.0" "node": ">=0.12.0"
} }
@@ -3491,18 +3557,6 @@
"get-func-name": "^2.0.1" "get-func-name": "^2.0.1"
} }
}, },
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
"integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
"dev": true,
"dependencies": {
"yallist": "^4.0.0"
},
"engines": {
"node": ">=10"
}
},
"node_modules/luxon": { "node_modules/luxon": {
"version": "3.4.4", "version": "3.4.4",
"resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz",
@@ -3561,6 +3615,7 @@
"resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz",
"integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">= 8" "node": ">= 8"
} }
@@ -3579,6 +3634,7 @@
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"braces": "^3.0.2", "braces": "^3.0.2",
"picomatch": "^2.3.1" "picomatch": "^2.3.1"
@@ -4047,6 +4103,7 @@
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
"integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -4175,6 +4232,7 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8.6" "node": ">=8.6"
}, },
@@ -4194,12 +4252,12 @@
} }
}, },
"node_modules/playwright": { "node_modules/playwright": {
"version": "1.43.1", "version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.43.1.tgz", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-V7SoH0ai2kNt1Md9E3Gwas5B9m8KR2GVvwZnAI6Pg0m3sh7UvgiYhRrhsziCmqMJNouPckiOhk8T+9bSAK0VIA==", "integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"playwright-core": "1.43.1" "playwright-core": "1.44.1"
}, },
"bin": { "bin": {
"playwright": "cli.js" "playwright": "cli.js"
@@ -4212,9 +4270,9 @@
} }
}, },
"node_modules/playwright-core": { "node_modules/playwright-core": {
"version": "1.43.1", "version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.43.1.tgz", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-EI36Mto2Vrx6VF7rm708qSnesVQKbxEWvPrfA1IPY6HgczBplDx7ENtx+K2n4kJ41sLLkuGfmb0ZLSSXlDhqPg==", "integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"dev": true, "dev": true,
"bin": { "bin": {
"playwright-core": "cli.js" "playwright-core": "cli.js"
@@ -4710,13 +4768,10 @@
] ]
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "7.6.0", "version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"dev": true, "dev": true,
"dependencies": {
"lru-cache": "^6.0.0"
},
"bin": { "bin": {
"semver": "bin/semver.js" "semver": "bin/semver.js"
}, },
@@ -4809,6 +4864,7 @@
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=8" "node": ">=8"
} }
@@ -5135,6 +5191,7 @@
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"is-number": "^7.0.0" "is-number": "^7.0.0"
}, },
@@ -5349,9 +5406,9 @@
} }
}, },
"node_modules/vite-node": { "node_modules/vite-node": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.3.tgz", "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz",
"integrity": "sha512-axFo00qiCpU/JLd8N1gu9iEYL3xTbMbMrbe5nDp9GL0nb6gurIdZLkkFogZXWnE8Oyy5kfSLwNVIcVsnhE7lgQ==", "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"cac": "^6.7.14", "cac": "^6.7.14",
@@ -5385,16 +5442,16 @@
} }
}, },
"node_modules/vitest": { "node_modules/vitest": {
"version": "1.5.3", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.3.tgz", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz",
"integrity": "sha512-2oM7nLXylw3mQlW6GXnRriw+7YvZFk/YNV8AxIC3Z3MfFbuziLGWP9GPxxu/7nRlXhqyxBikpamr+lEEj1sUEw==", "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==",
"dev": true, "dev": true,
"dependencies": { "dependencies": {
"@vitest/expect": "1.5.3", "@vitest/expect": "1.6.0",
"@vitest/runner": "1.5.3", "@vitest/runner": "1.6.0",
"@vitest/snapshot": "1.5.3", "@vitest/snapshot": "1.6.0",
"@vitest/spy": "1.5.3", "@vitest/spy": "1.6.0",
"@vitest/utils": "1.5.3", "@vitest/utils": "1.6.0",
"acorn-walk": "^8.3.2", "acorn-walk": "^8.3.2",
"chai": "^4.3.10", "chai": "^4.3.10",
"debug": "^4.3.4", "debug": "^4.3.4",
@@ -5408,7 +5465,7 @@
"tinybench": "^2.5.1", "tinybench": "^2.5.1",
"tinypool": "^0.8.3", "tinypool": "^0.8.3",
"vite": "^5.0.0", "vite": "^5.0.0",
"vite-node": "1.5.3", "vite-node": "1.6.0",
"why-is-node-running": "^2.2.2" "why-is-node-running": "^2.2.2"
}, },
"bin": { "bin": {
@@ -5423,8 +5480,8 @@
"peerDependencies": { "peerDependencies": {
"@edge-runtime/vm": "*", "@edge-runtime/vm": "*",
"@types/node": "^18.0.0 || >=20.0.0", "@types/node": "^18.0.0 || >=20.0.0",
"@vitest/browser": "1.5.3", "@vitest/browser": "1.6.0",
"@vitest/ui": "1.5.3", "@vitest/ui": "1.6.0",
"happy-dom": "*", "happy-dom": "*",
"jsdom": "*" "jsdom": "*"
}, },
+4 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.103.1", "version": "1.105.1",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -21,7 +21,7 @@
"devDependencies": { "devDependencies": {
"@immich/cli": "file:../cli", "@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.41.2", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.11.17", "@types/node": "^20.11.17",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
@@ -33,7 +33,7 @@
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^52.0.0", "eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0", "exiftool-vendored": "^26.0.0",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"pg": "^8.11.3", "pg": "^8.11.3",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0" "vitest": "^1.3.0"
}, },
"volta": { "volta": {
"node": "20.12.2" "node": "20.13.1"
} }
} }
+34 -32
View File
@@ -14,7 +14,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => { describe('/activities', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let nonOwner: LoginResponseDto; let nonOwner: LoginResponseDto;
let asset: AssetFileUploadResponseDto; let asset: AssetFileUploadResponseDto;
@@ -45,22 +45,24 @@ describe('/activity', () => {
await utils.resetDatabase(['activity']); await utils.resetDatabase(['activity']);
}); });
describe('GET /activity', () => { describe('GET /activities', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/activity'); const { status, body } = await request(app).get('/activities');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require an albumId', async () => { it('should require an albumId', async () => {
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app)
.get('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID']))); expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
}); });
it('should reject an invalid albumId', async () => { it('should reject an invalid albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: uuidDto.invalid }) .query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -69,7 +71,7 @@ describe('/activity', () => {
it('should reject an invalid assetId', async () => { it('should reject an invalid assetId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid }) .query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -78,7 +80,7 @@ describe('/activity', () => {
it('should start off empty', async () => { it('should start off empty', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id }) .query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]); expect(body).toEqual([]);
@@ -102,7 +104,7 @@ describe('/activity', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id }) .query({ albumId: album.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -121,7 +123,7 @@ describe('/activity', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id, type: 'comment' }) .query({ albumId: album.id, type: 'comment' })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -140,7 +142,7 @@ describe('/activity', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id, type: 'like' }) .query({ albumId: album.id, type: 'like' })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -152,7 +154,7 @@ describe('/activity', () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like }); const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const response1 = await request(app) const response1 = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id, userId: uuidDto.notFound }) .query({ albumId: album.id, userId: uuidDto.notFound })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -160,7 +162,7 @@ describe('/activity', () => {
expect(response1.body.length).toBe(0); expect(response1.body.length).toBe(0);
const response2 = await request(app) const response2 = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id, userId: admin.userId }) .query({ albumId: album.id, userId: admin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -180,7 +182,7 @@ describe('/activity', () => {
]); ]);
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/activity') .get('/activities')
.query({ albumId: album.id, assetId: asset.id }) .query({ albumId: album.id, assetId: asset.id })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -189,16 +191,16 @@ describe('/activity', () => {
}); });
}); });
describe('POST /activity', () => { describe('POST /activities', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/activity'); const { status, body } = await request(app).post('/activities');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require an albumId', async () => { it('should require an albumId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid }); .send({ albumId: uuidDto.invalid });
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -207,7 +209,7 @@ describe('/activity', () => {
it('should require a comment when type is comment', async () => { it('should require a comment when type is comment', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null }); .send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400); expect(status).toEqual(400);
@@ -216,7 +218,7 @@ describe('/activity', () => {
it('should add a comment to an album', async () => { it('should add a comment to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
albumId: album.id, albumId: album.id,
@@ -236,7 +238,7 @@ describe('/activity', () => {
it('should add a like to an album', async () => { it('should add a like to an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -253,7 +255,7 @@ describe('/activity', () => {
it('should return a 200 for a duplicate like on the album', async () => { it('should return a 200 for a duplicate like on the album', async () => {
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like }); const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -267,7 +269,7 @@ describe('/activity', () => {
type: ReactionType.Like, type: ReactionType.Like,
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, type: 'like' }); .send({ albumId: album.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -276,7 +278,7 @@ describe('/activity', () => {
it('should add a comment to an asset', async () => { it('should add a comment to an asset', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
albumId: album.id, albumId: album.id,
@@ -297,7 +299,7 @@ describe('/activity', () => {
it('should add a like to an asset', async () => { it('should add a like to an asset', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' }); .send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(201); expect(status).toEqual(201);
@@ -319,7 +321,7 @@ describe('/activity', () => {
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/activity') .post('/activities')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: album.id, assetId: asset.id, type: 'like' }); .send({ albumId: album.id, assetId: asset.id, type: 'like' });
expect(status).toEqual(200); expect(status).toEqual(200);
@@ -327,16 +329,16 @@ describe('/activity', () => {
}); });
}); });
describe('DELETE /activity/:id', () => { describe('DELETE /activities/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`); const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require a valid uuid', async () => { it('should require a valid uuid', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/activity/${uuidDto.invalid}`) .delete(`/activities/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -349,7 +351,7 @@ describe('/activity', () => {
comment: 'This is a test comment', comment: 'This is a test comment',
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activity/${reaction.id}`) .delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
}); });
@@ -360,7 +362,7 @@ describe('/activity', () => {
type: ReactionType.Like, type: ReactionType.Like,
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activity/${reaction.id}`) .delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
}); });
@@ -373,7 +375,7 @@ describe('/activity', () => {
}); });
const { status } = await request(app) const { status } = await request(app)
.delete(`/activity/${reaction.id}`) .delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(204); expect(status).toEqual(204);
@@ -387,7 +389,7 @@ describe('/activity', () => {
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/activity/${reaction.id}`) .delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`); .set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -405,7 +407,7 @@ describe('/activity', () => {
); );
const { status } = await request(app) const { status } = await request(app)
.delete(`/activity/${reaction.id}`) .delete(`/activities/${reaction.id}`)
.set('Authorization', `Bearer ${nonOwner.accessToken}`); .set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
+85 -53
View File
@@ -23,7 +23,7 @@ const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink'; const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared'; const user2NotShared = 'user2NotShared';
describe('/album', () => { describe('/albums', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user1Asset1: AssetFileUploadResponseDto; let user1Asset1: AssetFileUploadResponseDto;
@@ -110,16 +110,16 @@ describe('/album', () => {
await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) }); await deleteUser({ id: user3.userId, deleteUserDto: {} }, { headers: asBearerAuth(admin.accessToken) });
}); });
describe('GET /album', () => { describe('GET /albums', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/album'); const { status, body } = await request(app).get('/albums');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should reject an invalid shared param', async () => { it('should reject an invalid shared param', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album?shared=invalid') .get('/albums?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value'])); expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
@@ -127,7 +127,7 @@ describe('/album', () => {
it('should reject an invalid assetId param', async () => { it('should reject an invalid assetId param', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album?assetId=invalid') .get('/albums?assetId=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400); expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID'])); expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
@@ -135,7 +135,7 @@ describe('/album', () => {
it("should not show other users' favorites", async () => { it("should not show other users' favorites", async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toEqual({ expect(body).toEqual({
@@ -146,7 +146,7 @@ describe('/album', () => {
it('should not return shared albums with a deleted owner', async () => { it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album?shared=true') .get('/albums?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -178,7 +178,7 @@ describe('/album', () => {
}); });
it('should return the album collection including owned and shared', async () => { it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`); const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
expect(body).toEqual( expect(body).toEqual(
@@ -209,7 +209,7 @@ describe('/album', () => {
it('should return the album collection filtered by shared', async () => { it('should return the album collection filtered by shared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album?shared=true') .get('/albums?shared=true')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(4); expect(body).toHaveLength(4);
@@ -241,7 +241,7 @@ describe('/album', () => {
it('should return the album collection filtered by NOT shared', async () => { it('should return the album collection filtered by NOT shared', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album?shared=false') .get('/albums?shared=false')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
@@ -258,7 +258,7 @@ describe('/album', () => {
it('should return the album collection filtered by assetId', async () => { it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album?assetId=${user1Asset2.id}`) .get(`/albums?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(1); expect(body).toHaveLength(1);
@@ -266,7 +266,7 @@ describe('/album', () => {
it('should return the album collection filtered by assetId and ignores shared=true', async () => { it('should return the album collection filtered by assetId and ignores shared=true', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album?shared=true&assetId=${user1Asset1.id}`) .get(`/albums?shared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
@@ -274,23 +274,23 @@ describe('/album', () => {
it('should return the album collection filtered by assetId and ignores shared=false', async () => { it('should return the album collection filtered by assetId and ignores shared=false', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album?shared=false&assetId=${user1Asset1.id}`) .get(`/albums?shared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
}); });
}); });
describe('GET /album/:id', () => { describe('GET /albums/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`); const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return album info for own album', async () => { it('should return album info for own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`) .get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -302,7 +302,7 @@ describe('/album', () => {
it('should return album info for shared album (editor)', async () => { it('should return album info for shared album (editor)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user2Albums[0].id}?withoutAssets=false`) .get(`/albums/${user2Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -311,7 +311,7 @@ describe('/album', () => {
it('should return album info for shared album (viewer)', async () => { it('should return album info for shared album (viewer)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user1Albums[3].id}?withoutAssets=false`) .get(`/albums/${user1Albums[3].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`); .set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -320,7 +320,7 @@ describe('/album', () => {
it('should return album info with assets when withoutAssets is undefined', async () => { it('should return album info with assets when withoutAssets is undefined', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}`) .get(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -332,7 +332,7 @@ describe('/album', () => {
it('should return album info without assets when withoutAssets is true', async () => { it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=true`) .get(`/albums/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -344,16 +344,16 @@ describe('/album', () => {
}); });
}); });
describe('GET /album/count', () => { describe('GET /albums/count', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/album/count'); const { status, body } = await request(app).get('/albums/count');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return total count of albums the user has access to', async () => { it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/album/count') .get('/albums/count')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -361,16 +361,16 @@ describe('/album', () => {
}); });
}); });
describe('POST /album', () => { describe('POST /albums', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' }); const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should create an album', async () => { it('should create an album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/album') .post('/albums')
.send({ albumName: 'New album' }) .send({ albumName: 'New album' })
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201); expect(status).toBe(201);
@@ -383,7 +383,6 @@ describe('/album', () => {
description: '', description: '',
albumThumbnailAssetId: null, albumThumbnailAssetId: null,
shared: false, shared: false,
sharedUsers: [],
albumUsers: [], albumUsers: [],
hasSharedLink: false, hasSharedLink: false,
assets: [], assets: [],
@@ -395,9 +394,9 @@ describe('/album', () => {
}); });
}); });
describe('PUT /album/:id/assets', () => { describe('PUT /albums/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`); const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -405,7 +404,7 @@ describe('/album', () => {
it('should be able to add own asset to own album', async () => { it('should be able to add own asset to own album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`) .put(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
@@ -416,7 +415,7 @@ describe('/album', () => {
it('should be able to add own asset to shared album', async () => { it('should be able to add own asset to shared album', async () => {
const asset = await utils.createAsset(user1.accessToken); const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`) .put(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
@@ -427,19 +426,33 @@ describe('/album', () => {
it('should not be able to add assets to album as a viewer', async () => { it('should not be able to add assets to album as a viewer', async () => {
const asset = await utils.createAsset(user2.accessToken); const asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${user1Albums[3].id}/assets`) .put(`/albums/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [asset.id] }); .send({ ids: [asset.id] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access')); expect(body).toEqual(errorDto.badRequest('Not found or no album.addAsset access'));
}); });
it('should add duplicate assets only once', async () => {
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id, asset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
expect.objectContaining({ id: asset.id, success: false, error: 'duplicate' }),
]);
});
}); });
describe('PATCH /album/:id', () => { describe('PATCH /albums/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/album/${uuidDto.notFound}`) .patch(`/albums/${uuidDto.notFound}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -450,7 +463,7 @@ describe('/album', () => {
albumName: 'New album', albumName: 'New album',
}); });
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/album/${album.id}`) .patch(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ .send({
albumName: 'New album name', albumName: 'New album name',
@@ -467,7 +480,7 @@ describe('/album', () => {
it('should not be able to update as a viewer', async () => { it('should not be able to update as a viewer', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/album/${user1Albums[3].id}`) .patch(`/albums/${user1Albums[3].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
@@ -477,7 +490,7 @@ describe('/album', () => {
it('should not be able to update as an editor', async () => { it('should not be able to update as an editor', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/album/${user1Albums[0].id}`) .patch(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ albumName: 'New album name' }); .send({ albumName: 'New album name' });
@@ -486,10 +499,10 @@ describe('/album', () => {
}); });
}); });
describe('DELETE /album/:id/assets', () => { describe('DELETE /albums/:id/assets', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`) .delete(`/albums/${user1Albums[0].id}/assets`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(401); expect(status).toBe(401);
@@ -498,7 +511,7 @@ describe('/album', () => {
it('should not be able to remove foreign asset from own album', async () => { it('should not be able to remove foreign asset from own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`) .delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -514,7 +527,7 @@ describe('/album', () => {
it('should not be able to remove foreign asset from foreign album', async () => { it('should not be able to remove foreign asset from foreign album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`) .delete(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -530,7 +543,7 @@ describe('/album', () => {
it('should be able to remove own asset from own album', async () => { it('should be able to remove own asset from own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user1Albums[0].id}/assets`) .delete(`/albums/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -540,7 +553,7 @@ describe('/album', () => {
it('should be able to remove own asset from shared album', async () => { it('should be able to remove own asset from shared album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user2Albums[0].id}/assets`) .delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
@@ -550,13 +563,26 @@ describe('/album', () => {
it('should not be able to remove assets from album as a viewer', async () => { it('should not be able to remove assets from album as a viewer', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/album/${user1Albums[3].id}/assets`) .delete(`/albums/${user1Albums[3].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] }); .send({ ids: [user1Asset1.id] });
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access')); expect(body).toEqual(errorDto.badRequest('Not found or no album.removeAsset access'));
}); });
it('should remove duplicate assets only once', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id, user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
expect.objectContaining({ id: user1Asset1.id, success: false, error: 'not_found' }),
]);
});
}); });
describe('PUT :id/users', () => { describe('PUT :id/users', () => {
@@ -569,7 +595,7 @@ describe('/album', () => {
}); });
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] }); const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -577,21 +603,25 @@ describe('/album', () => {
it('should be able to add user to own album', async () => { it('should be able to add user to own album', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
sharedUsers: [expect.objectContaining({ id: user2.userId })], albumUsers: [
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
}), }),
); );
}); });
it('should not be able to share album with owner', async () => { it('should not be able to share album with owner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
@@ -601,12 +631,12 @@ describe('/album', () => {
it('should not be able to add existing user to shared album', async () => { it('should not be able to add existing user to shared album', async () => {
await request(app) await request(app)
.put(`/album/${album.id}/users`) .put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${album.id}/users`) .put(`/albums/${album.id}/users`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] }); .send({ albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }] });
@@ -625,14 +655,16 @@ describe('/album', () => {
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app) const { status } = await request(app)
.put(`/album/${album.id}/user/${user2.userId}`) .put(`/albums/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ role: AlbumUserRole.Editor }); .send({ role: AlbumUserRole.Editor });
expect(status).toBe(200); expect(status).toBe(200);
// Get album to verify the role change // Get album to verify the role change
const { body } = await request(app).get(`/album/${album.id}`).set('Authorization', `Bearer ${user1.accessToken}`); const { body } = await request(app)
.get(`/albums/${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })], albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
@@ -649,7 +681,7 @@ describe('/album', () => {
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer); expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/album/${album.id}/user/${user2.userId}`) .put(`/albums/${album.id}/user/${user2.userId}`)
.set('Authorization', `Bearer ${user2.accessToken}`) .set('Authorization', `Bearer ${user2.accessToken}`)
.send({ role: AlbumUserRole.Editor }); .send({ role: AlbumUserRole.Editor });
+382 -377
View File
@@ -2,11 +2,10 @@ import {
AssetFileUploadResponseDto, AssetFileUploadResponseDto,
AssetResponseDto, AssetResponseDto,
AssetTypeEnum, AssetTypeEnum,
LibraryResponseDto,
LoginResponseDto, LoginResponseDto,
SharedLinkType, SharedLinkType,
getAllLibraries,
getAssetInfo, getAssetInfo,
getMyUserInfo,
updateAssets, updateAssets,
} from '@immich/sdk'; } from '@immich/sdk';
import { exiftool } from 'exiftool-vendored'; import { exiftool } from 'exiftool-vendored';
@@ -73,7 +72,7 @@ describe('/asset', () => {
let stackAssets: AssetFileUploadResponseDto[]; let stackAssets: AssetFileUploadResponseDto[];
let locationAsset: AssetFileUploadResponseDto; let locationAsset: AssetFileUploadResponseDto;
beforeAll(async () => { const setupTests = async () => {
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false }); admin = await utils.adminSetup({ onboarding: false });
@@ -87,6 +86,8 @@ describe('/asset', () => {
utils.userSetup(admin.accessToken, createUserDto.create('stack')), utils.userSetup(admin.accessToken, createUserDto.create('stack')),
]); ]);
await utils.createPartner(user1.accessToken, user2.userId);
// asset location // asset location
locationAsset = await utils.createAsset(admin.accessToken, { locationAsset = await utils.createAsset(admin.accessToken, {
assetData: { assetData: {
@@ -156,7 +157,8 @@ describe('/asset', () => {
assetId: user1Assets[0].id, assetId: user1Assets[0].id,
personId: person1.id, personId: person1.id,
}); });
}, 30_000); };
beforeAll(setupTests, 30_000);
afterAll(() => { afterAll(() => {
utils.disconnectWebsocket(websocket); utils.disconnectWebsocket(websocket);
@@ -236,6 +238,35 @@ describe('/asset', () => {
expect(data.status).toBe(200); expect(data.status).toBe(200);
expect(data.body).not.toHaveProperty('people'); expect(data.body).not.toHaveProperty('people');
}); });
describe('partner assets', () => {
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('disallows viewing archived assets', async () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
it('disallows viewing trashed assets', async () => {
const asset = await utils.createAsset(user1.accessToken);
await utils.deleteAssets(user1.accessToken, [asset.id]);
const { status } = await request(app)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
});
}); });
describe('GET /asset/statistics', () => { describe('GET /asset/statistics', () => {
@@ -546,14 +577,321 @@ describe('/asset', () => {
}); });
}); });
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: locationAsset.id,
});
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/webp');
const exifData = await readTags(body, 'thumbnail.webp');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
it('should not include gps data for jpeg thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const exifData = await readTags(body, 'thumbnail.jpg');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
});
describe('GET /asset/file/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the original', async () => {
const { status, body, type } = await request(app)
.get(`/asset/file/${locationAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
const original = await readFile(locationAssetFilepath);
const originalChecksum = utils.sha1(original);
const downloadChecksum = utils.sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);
});
});
describe('GET /asset/map-marker', () => {
beforeAll(async () => {
const files = [
'formats/avif/8bit-sRGB.avif',
'formats/jpg/el_torcal_rocks.jpg',
'formats/jxl/8bit-sRGB.jxl',
'formats/heic/IMG_2682.heic',
'formats/png/density_plot.png',
'formats/raw/Nikon/D80/glarus.nef',
'formats/raw/Nikon/D700/philadelphia.nef',
'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
];
utils.resetEvents();
const uploadFile = async (input: string) => {
const filepath = join(testAssetDir, input);
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
};
const uploads = files.map((f) => uploadFile(f));
await Promise.all(uploads);
}, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('PUT /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
});
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should add stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
});
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {
// create stack after previous test removed stack children
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[0].id }),
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
);
});
});
describe('PUT /asset/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(200);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// new parent
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
});
describe('POST /asset/upload', () => { describe('POST /asset/upload', () => {
beforeAll(setupTests, 30_000);
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/asset/upload`); const { status, body } = await request(app).post(`/asset/upload`);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401); expect(status).toBe(401);
}); });
const invalid = [ it.each([
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } }, { should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } }, { should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } }, { should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
@@ -562,21 +900,17 @@ describe('/asset', () => {
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } }, { should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } }, { should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } }, { should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
]; ])('should $should', async ({ dto }) => {
const { status, body } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
for (const { should, dto } of invalid) { it.each([
it(`should ${should}`, async () => {
const { status, body } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
}
const tests = [
{ {
input: 'formats/avif/8bit-sRGB.avif', input: 'formats/avif/8bit-sRGB.avif',
expected: { expected: {
@@ -792,26 +1126,22 @@ describe('/asset', () => {
}, },
}, },
}, },
]; ])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
const filepath = join(testAssetDir, input);
for (const { input, expected } of tests) { const { id, duplicate } = await utils.createAsset(admin.accessToken, {
it(`should upload and generate a thumbnail for ${input}`, async () => { assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
const filepath = join(testAssetDir, input);
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
}); });
}
expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
});
it('should handle a duplicate', async () => { it('should handle a duplicate', async () => {
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg'; const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
@@ -825,25 +1155,6 @@ describe('/asset', () => {
expect(duplicate).toBe(true); expect(duplicate).toBe(true);
}); });
it("should not upload to another user's library", async () => {
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
const library = libraries.find((library) => library.ownerId === user1.userId) as LibraryResponseDto;
const { body, status } = await request(app)
.post('/asset/upload')
.set('Authorization', `Bearer ${admin.accessToken}`)
.field('libraryId', library.id)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.field('duration', '0:00:00.000000')
.attach('assetData', makeRandomImage(), 'example.png');
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no asset.upload access'));
});
it('should update the used quota', async () => { it('should update the used quota', async () => {
const { body, status } = await request(app) const { body, status } = await request(app)
.post('/asset/upload') .post('/asset/upload')
@@ -857,7 +1168,7 @@ describe('/asset', () => {
expect(body).toEqual({ id: expect.any(String), duplicate: false }); expect(body).toEqual({ id: expect.any(String), duplicate: false });
expect(status).toBe(201); expect(status).toBe(201);
const { body: user } = await request(app).get('/user/me').set('Authorization', `Bearer ${quotaUser.accessToken}`); const user = await getMyUserInfo({ headers: asBearerAuth(quotaUser.accessToken) });
expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 })); expect(user).toEqual(expect.objectContaining({ quotaUsageInBytes: 70 }));
}); });
@@ -881,7 +1192,7 @@ describe('/asset', () => {
// This ensures that immich+exiftool are extracting the videos the same way Samsung does. // This ensures that immich+exiftool are extracting the videos the same way Samsung does.
// DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives // DO NOT assume immich+exiftool are doing things correctly and just copy whatever hash it gives
// into the test here. // into the test here.
const motionTests = [ it.each([
{ {
filepath: 'formats/motionphoto/Samsung One UI 5.jpg', filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=', checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
@@ -894,329 +1205,23 @@ describe('/asset', () => {
filepath: 'formats/motionphoto/Samsung One UI 6.heic', filepath: 'formats/motionphoto/Samsung One UI 6.heic',
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=', checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
}, },
]; ])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
const response = await utils.createAsset(admin.accessToken, {
for (const { filepath, checksum } of motionTests) { assetData: {
it(`should extract motionphoto video from ${filepath}`, async () => { bytes: await readFile(join(testAssetDir, filepath)),
const response = await utils.createAsset(admin.accessToken, { filename: basename(filepath),
assetData: { },
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
expect(response.duplicate).toBe(false);
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(asset.livePhotoVideoId).toBeDefined();
const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
expect(video.checksum).toStrictEqual(checksum);
});
}
});
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not include gps data for webp thumbnails', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: locationAsset.id,
}); });
const { status, body, type } = await request(app) await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(response.duplicate).toBe(false);
expect(body).toBeDefined();
expect(type).toBe('image/webp');
const exifData = await readTags(body, 'thumbnail.webp'); const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(exifData).not.toHaveProperty('GPSLongitude'); expect(asset.livePhotoVideoId).toBeDefined();
expect(exifData).not.toHaveProperty('GPSLatitude');
});
it('should not include gps data for jpeg thumbnails', async () => { const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
const { status, body, type } = await request(app) expect(video.checksum).toStrictEqual(checksum);
.get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const exifData = await readTags(body, 'thumbnail.jpg');
expect(exifData).not.toHaveProperty('GPSLongitude');
expect(exifData).not.toHaveProperty('GPSLatitude');
});
});
describe('GET /asset/file/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the original', async () => {
const { status, body, type } = await request(app)
.get(`/asset/file/${locationAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await utils.getAssetInfo(admin.accessToken, locationAsset.id);
const original = await readFile(locationAssetFilepath);
const originalChecksum = utils.sha1(original);
const downloadChecksum = utils.sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);
});
});
describe('GET /asset/map-marker', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('GET /asset', () => {
it('should return stack data', async () => {
const { status, body } = await request(app).get('/asset').set('Authorization', `Bearer ${stackUser.accessToken}`);
const stack = body.find((asset: AssetResponseDto) => asset.id === stackAssets[0].id);
expect(status).toBe(200);
expect(stack).toEqual(
expect.objectContaining({
stackCount: 3,
stack:
// Response includes children at the root level
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
}),
);
});
});
describe('PUT /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
});
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should add stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
});
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {
// create stack after previous test removed stack children
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const { status } = await request(app)
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[0].id }),
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
);
});
});
describe('PUT /asset/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const { status, body } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(200);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// new parent
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
}); });
}); });
}); });
+1 -1
View File
@@ -2,7 +2,7 @@ import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from
import { asBearerAuth, utils } from 'src/utils'; import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/audit', () => { describe('/audits', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
+46 -188
View File
@@ -1,11 +1,4 @@
import { import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
LibraryResponseDto,
LibraryType,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs'; import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures'; import { userDto, uuidDto } from 'src/fixtures';
@@ -18,7 +11,7 @@ import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) => const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) }); scanLibrary({ id, scanLibraryDto: dto }, { headers: asBearerAuth(accessToken) });
describe('/library', () => { describe('/libraries', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user: LoginResponseDto; let user: LoginResponseDto;
let library: LibraryResponseDto; let library: LibraryResponseDto;
@@ -29,7 +22,7 @@ describe('/library', () => {
admin = await utils.adminSetup(); admin = await utils.adminSetup();
await utils.resetAdminConfig(admin.accessToken); await utils.resetAdminConfig(admin.accessToken);
user = await utils.userSetup(admin.accessToken, userDto.user1); user = await utils.userSetup(admin.accessToken, userDto.user1);
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External }); library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
websocket = await utils.connectWebsocket(admin.accessToken); websocket = await utils.connectWebsocket(admin.accessToken);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`); utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`); utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
@@ -44,44 +37,26 @@ describe('/library', () => {
utils.resetEvents(); utils.resetEvents();
}); });
describe('GET /library', () => { describe('GET /libraries', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/library'); const { status, body } = await request(app).get('/libraries');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should start with a default upload library', async () => {
const { status, body } = await request(app).get('/library').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.Upload,
name: 'Default Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
]),
);
});
}); });
describe('POST /library', () => { describe('POST /libraries', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post('/library').send({}); const { status, body } = await request(app).post('/libraries').send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should require admin authentication', async () => { it('should require admin authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/library') .post('/libraries')
.set('Authorization', `Bearer ${user.accessToken}`) .set('Authorization', `Bearer ${user.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.External }); .send({ ownerId: admin.userId });
expect(status).toBe(403); expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
@@ -89,15 +64,14 @@ describe('/library', () => {
it('should create an external library with defaults', async () => { it('should create an external library with defaults', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/library') .post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.External }); .send({ ownerId: admin.userId });
expect(status).toBe(201); expect(status).toBe(201);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
name: 'New External Library', name: 'New External Library',
refreshedAt: null, refreshedAt: null,
assetCount: 0, assetCount: 0,
@@ -109,11 +83,10 @@ describe('/library', () => {
it('should create an external library with options', async () => { it('should create an external library with options', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/library') .post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library', name: 'My Awesome Library',
importPaths: ['/path/to/import'], importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**'], exclusionPatterns: ['**/Raw/**'],
@@ -130,11 +103,10 @@ describe('/library', () => {
it('should not create an external library with duplicate import paths', async () => { it('should not create an external library with duplicate import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/library') .post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library', name: 'My Awesome Library',
importPaths: ['/path', '/path'], importPaths: ['/path', '/path'],
exclusionPatterns: ['**/Raw/**'], exclusionPatterns: ['**/Raw/**'],
@@ -146,11 +118,10 @@ describe('/library', () => {
it('should not create an external library with duplicate exclusion patterns', async () => { it('should not create an external library with duplicate exclusion patterns', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/library') .post('/libraries')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
name: 'My Awesome Library', name: 'My Awesome Library',
importPaths: ['/path/to/import'], importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**', '**/Raw/**'], exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
@@ -159,72 +130,18 @@ describe('/library', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"])); expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
}); });
it('should create an upload library with defaults', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.Upload });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.Upload,
name: 'New Upload Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
);
});
it('should create an upload library with options', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.Upload, name: 'My Awesome Library' });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
name: 'My Awesome Library',
}),
);
});
it('should not allow upload libraries to have import paths', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.Upload, importPaths: ['/path/to/import'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
});
it('should not allow upload libraries to have exclusion patterns', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ownerId: admin.userId, type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
});
}); });
describe('PUT /library/:id', () => { describe('PUT /libraries/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({}); const { status, body } = await request(app).put(`/libraries/${uuidDto.notFound}`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should change the library name', async () => { it('should change the library name', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'New Library Name' }); .send({ name: 'New Library Name' });
@@ -238,7 +155,7 @@ describe('/library', () => {
it('should not set an empty name', async () => { it('should not set an empty name', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: '' }); .send({ name: '' });
@@ -248,7 +165,7 @@ describe('/library', () => {
it('should change the import paths', async () => { it('should change the import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [testAssetDirInternal] }); .send({ importPaths: [testAssetDirInternal] });
@@ -262,7 +179,7 @@ describe('/library', () => {
it('should reject an empty import path', async () => { it('should reject an empty import path', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [''] }); .send({ importPaths: [''] });
@@ -272,7 +189,7 @@ describe('/library', () => {
it('should reject duplicate import paths', async () => { it('should reject duplicate import paths', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: ['/path', '/path'] }); .send({ importPaths: ['/path', '/path'] });
@@ -282,7 +199,7 @@ describe('/library', () => {
it('should change the exclusion pattern', async () => { it('should change the exclusion pattern', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/Raw/**'] }); .send({ exclusionPatterns: ['**/Raw/**'] });
@@ -296,7 +213,7 @@ describe('/library', () => {
it('should reject duplicate exclusion patterns', async () => { it('should reject duplicate exclusion patterns', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] }); .send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
@@ -306,7 +223,7 @@ describe('/library', () => {
it('should reject an empty exclusion pattern', async () => { it('should reject an empty exclusion pattern', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/library/${library.id}`) .put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: [''] }); .send({ exclusionPatterns: [''] });
@@ -315,9 +232,9 @@ describe('/library', () => {
}); });
}); });
describe('GET /library/:id', () => { describe('GET /libraries/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`); const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -325,27 +242,23 @@ describe('/library', () => {
it('should require admin access', async () => { it('should require admin access', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/library/${uuidDto.notFound}`) .get(`/libraries/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user.accessToken}`); .set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(403); expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden); expect(body).toEqual(errorDto.forbidden);
}); });
it('should get library by id', async () => { it('should get library by id', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
ownerId: admin.userId,
type: LibraryType.External,
});
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/library/${library.id}`) .get(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
expect.objectContaining({ expect.objectContaining({
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
name: 'New External Library', name: 'New External Library',
refreshedAt: null, refreshedAt: null,
assetCount: 0, assetCount: 0,
@@ -356,41 +269,26 @@ describe('/library', () => {
}); });
}); });
describe('GET /library/:id/statistics', () => { describe('GET /libraries/:id/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`); const { status, body } = await request(app).get(`/libraries/${uuidDto.notFound}/statistics`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
}); });
describe('POST /library/:id/scan', () => { describe('POST /libraries/:id/scan', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({}); const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/scan`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not scan an upload library', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
type: LibraryType.Upload,
});
const { status, body } = await request(app)
.post(`/library/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Can only refresh external libraries'));
});
it('should scan external library', async () => { it('should scan external library', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`], importPaths: [`${testAssetDirInternal}/temp/directoryA`],
}); });
@@ -406,7 +304,6 @@ describe('/library', () => {
it('should scan external library with exclusion pattern', async () => { it('should scan external library with exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
exclusionPatterns: ['**/directoryA'], exclusionPatterns: ['**/directoryA'],
}); });
@@ -423,7 +320,6 @@ describe('/library', () => {
it('should scan multiple import paths', async () => { it('should scan multiple import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`], importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
}); });
@@ -440,7 +336,6 @@ describe('/library', () => {
it('should pick up new files', async () => { it('should pick up new files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -466,7 +361,6 @@ describe('/library', () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -493,7 +387,6 @@ describe('/library', () => {
it('should scan new files', async () => { it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -521,7 +414,6 @@ describe('/library', () => {
it('should reimport modified files', async () => { it('should reimport modified files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -549,7 +441,6 @@ describe('/library', () => {
it('should not reimport unmodified files', async () => { it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -579,7 +470,6 @@ describe('/library', () => {
it('should reimport all files', async () => { it('should reimport all files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -606,9 +496,9 @@ describe('/library', () => {
}); });
}); });
describe('POST /library/:id/removeOffline', () => { describe('POST /libraries/:id/removeOffline', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({}); const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -617,7 +507,6 @@ describe('/library', () => {
it('should remove offline files', async () => { it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -643,7 +532,7 @@ describe('/library', () => {
expect(offlineAssets.count).toBe(1); expect(offlineAssets.count).toBe(1);
const { status } = await request(app) const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`) .post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(); .send();
expect(status).toBe(204); expect(status).toBe(204);
@@ -658,7 +547,6 @@ describe('/library', () => {
it('should not remove online files', async () => { it('should not remove online files', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -669,7 +557,7 @@ describe('/library', () => {
expect(assetsBefore.count).toBeGreaterThan(1); expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app) const { status } = await request(app)
.post(`/library/${library.id}/removeOffline`) .post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send(); .send();
expect(status).toBe(204); expect(status).toBe(204);
@@ -681,9 +569,9 @@ describe('/library', () => {
}); });
}); });
describe('POST /library/:id/validate', () => { describe('POST /libraries/:id/validate', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({}); const { status, body } = await request(app).post(`/libraries/${uuidDto.notFound}/validate`).send({});
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -729,54 +617,25 @@ describe('/library', () => {
}); });
}); });
describe('DELETE /library/:id', () => { describe('DELETE /libraries/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`); const { status, body } = await request(app).delete(`/libraries/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should not delete the last upload library', async () => {
const libraries = await getAllLibraries(
{ $type: LibraryType.Upload },
{ headers: asBearerAuth(admin.accessToken) },
);
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
// delete all but the last upload library
for (const library of adminLibraries) {
const { status } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
const { status, body } = await request(app)
.delete(`/library/${lastLibrary.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
expect(status).toBe(400);
});
it('should delete an external library', async () => { it('should delete an external library', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId });
ownerId: admin.userId,
type: LibraryType.External,
});
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/library/${library.id}`) .delete(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
expect(body).toEqual({}); expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual( expect(libraries).not.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
@@ -789,7 +648,6 @@ describe('/library', () => {
it('should delete an external library with assets', async () => { it('should delete an external library with assets', async () => {
const library = await utils.createLibrary(admin.accessToken, { const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId, ownerId: admin.userId,
type: LibraryType.External,
importPaths: [`${testAssetDirInternal}/temp`], importPaths: [`${testAssetDirInternal}/temp`],
}); });
@@ -797,13 +655,13 @@ describe('/library', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 }); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/library/${library.id}`) .delete(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
expect(body).toEqual({}); expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) }); const libraries = await getAllLibraries({ headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual( expect(libraries).not.toEqual(
expect.arrayContaining([ expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
+16 -16
View File
@@ -5,7 +5,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/partner', () => { describe('/partners', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let user1: LoginResponseDto; let user1: LoginResponseDto;
let user2: LoginResponseDto; let user2: LoginResponseDto;
@@ -28,9 +28,9 @@ describe('/partner', () => {
]); ]);
}); });
describe('GET /partner', () => { describe('GET /partners', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/partner'); const { status, body } = await request(app).get('/partners');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -38,7 +38,7 @@ describe('/partner', () => {
it('should get all partners shared by user', async () => { it('should get all partners shared by user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/partner') .get('/partners')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-by' }); .query({ direction: 'shared-by' });
@@ -48,7 +48,7 @@ describe('/partner', () => {
it('should get all partners that share with user', async () => { it('should get all partners that share with user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/partner') .get('/partners')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.query({ direction: 'shared-with' }); .query({ direction: 'shared-with' });
@@ -57,9 +57,9 @@ describe('/partner', () => {
}); });
}); });
describe('POST /partner/:id', () => { describe('POST /partners/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/partner/${user3.userId}`); const { status, body } = await request(app).post(`/partners/${user3.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -67,7 +67,7 @@ describe('/partner', () => {
it('should share with new partner', async () => { it('should share with new partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/partner/${user3.userId}`) .post(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(201); expect(status).toBe(201);
@@ -76,7 +76,7 @@ describe('/partner', () => {
it('should not share with new partner if already sharing with this partner', async () => { it('should not share with new partner if already sharing with this partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/partner/${user2.userId}`) .post(`/partners/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -84,9 +84,9 @@ describe('/partner', () => {
}); });
}); });
describe('PUT /partner/:id', () => { describe('PUT /partners/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/partner/${user2.userId}`); const { status, body } = await request(app).put(`/partners/${user2.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -94,7 +94,7 @@ describe('/partner', () => {
it('should update partner', async () => { it('should update partner', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/partner/${user2.userId}`) .put(`/partners/${user2.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ inTimeline: false }); .send({ inTimeline: false });
@@ -103,9 +103,9 @@ describe('/partner', () => {
}); });
}); });
describe('DELETE /partner/:id', () => { describe('DELETE /partners/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/partner/${user3.userId}`); const { status, body } = await request(app).delete(`/partners/${user3.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -113,7 +113,7 @@ describe('/partner', () => {
it('should delete partner', async () => { it('should delete partner', async () => {
const { status } = await request(app) const { status } = await request(app)
.delete(`/partner/${user3.userId}`) .delete(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -121,7 +121,7 @@ describe('/partner', () => {
it('should throw a bad request if partner not found', async () => { it('should throw a bad request if partner not found', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/partner/${user3.userId}`) .delete(`/partners/${user3.userId}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
+23 -23
View File
@@ -12,7 +12,7 @@ const invalidBirthday = [
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] }, { birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
]; ];
describe('/person', () => { describe('/people', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto; let visiblePerson: PersonResponseDto;
let hiddenPerson: PersonResponseDto; let hiddenPerson: PersonResponseDto;
@@ -47,11 +47,11 @@ describe('/person', () => {
]); ]);
}); });
describe('GET /person', () => { describe('GET /people', () => {
beforeEach(async () => {}); beforeEach(async () => {});
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/person'); const { status, body } = await request(app).get('/people');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -59,7 +59,7 @@ describe('/person', () => {
it('should return all people (including hidden)', async () => { it('should return all people (including hidden)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/person') .get('/people')
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true }); .query({ withHidden: true });
@@ -76,7 +76,7 @@ describe('/person', () => {
}); });
it('should return only visible people', async () => { it('should return only visible people', async () => {
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/people').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
@@ -90,9 +90,9 @@ describe('/person', () => {
}); });
}); });
describe('GET /person/:id', () => { describe('GET /people/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`); const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -100,7 +100,7 @@ describe('/person', () => {
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}`) .get(`/people/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -109,7 +109,7 @@ describe('/person', () => {
it('should return person information', async () => { it('should return person information', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/person/${visiblePerson.id}`) .get(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -117,9 +117,9 @@ describe('/person', () => {
}); });
}); });
describe('GET /person/:id/statistics', () => { describe('GET /people/:id/statistics', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`); const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -127,7 +127,7 @@ describe('/person', () => {
it('should throw error if person with id does not exist', async () => { it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}/statistics`) .get(`/people/${uuidDto.notFound}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -136,7 +136,7 @@ describe('/person', () => {
it('should return the correct number of assets', async () => { it('should return the correct number of assets', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/person/${multipleAssetsPerson.id}/statistics`) .get(`/people/${multipleAssetsPerson.id}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -144,9 +144,9 @@ describe('/person', () => {
}); });
}); });
describe('POST /person', () => { describe('POST /people', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/person`); const { status, body } = await request(app).post(`/people`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -154,7 +154,7 @@ describe('/person', () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => { it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/person`) .post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
@@ -164,7 +164,7 @@ describe('/person', () => {
it('should create a person', async () => { it('should create a person', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/person`) .post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ .send({
name: 'New Person', name: 'New Person',
@@ -179,9 +179,9 @@ describe('/person', () => {
}); });
}); });
describe('PUT /person/:id', () => { describe('PUT /people/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`); const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -193,7 +193,7 @@ describe('/person', () => {
]) { ]) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`) .put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null }); .send({ [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
@@ -204,7 +204,7 @@ describe('/person', () => {
for (const { birthDate, response } of invalidBirthday) { for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => { it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`) .put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate }); .send({ birthDate });
expect(status).toBe(400); expect(status).toBe(400);
@@ -214,7 +214,7 @@ describe('/person', () => {
it('should update a date of birth', async () => { it('should update a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`) .put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' }); .send({ birthDate: '1990-01-01T05:00:00.000Z' });
expect(status).toBe(200); expect(status).toBe(200);
@@ -223,7 +223,7 @@ describe('/person', () => {
it('should clear a date of birth', async () => { it('should clear a date of birth', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/person/${visiblePerson.id}`) .put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: null }); .send({ birthDate: null });
expect(status).toBe(200); expect(status).toBe(200);
+4 -3
View File
@@ -15,16 +15,16 @@ describe('/server-info', () => {
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1); nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
}); });
describe('GET /server-info', () => { describe('GET /server-info/storage', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/server-info'); const { status, body } = await request(app).get('/server-info/storage');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should return the disk information', async () => { it('should return the disk information', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/server-info') .get('/server-info/storage')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual({ expect(body).toEqual({
@@ -66,6 +66,7 @@ describe('/server-info', () => {
expect(body).toEqual({ expect(body).toEqual({
smartSearch: false, smartSearch: false,
configFile: false, configFile: false,
duplicateDetection: false,
facialRecognition: false, facialRecognition: false,
map: true, map: true,
reverseGeocoding: true, reverseGeocoding: true,
+38 -38
View File
@@ -13,7 +13,7 @@ import { app, asBearerAuth, shareUrl, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-link', () => { describe('/shared-links', () => {
let admin: LoginResponseDto; let admin: LoginResponseDto;
let asset1: AssetFileUploadResponseDto; let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto; let asset2: AssetFileUploadResponseDto;
@@ -114,9 +114,9 @@ describe('/shared-link', () => {
}); });
}); });
describe('GET /shared-link', () => { describe('GET /shared-links', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/shared-link'); const { status, body } = await request(app).get('/shared-links');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -124,7 +124,7 @@ describe('/shared-link', () => {
it('should get all shared links created by user', async () => { it('should get all shared links created by user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-link') .get('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -142,7 +142,7 @@ describe('/shared-link', () => {
it('should not get shared links created by other users', async () => { it('should not get shared links created by other users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-link') .get('/shared-links')
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -150,15 +150,15 @@ describe('/shared-link', () => {
}); });
}); });
describe('GET /shared-link/me', () => { describe('GET /shared-links/me', () => {
it('should not require admin authentication', async () => { it('should not require admin authentication', async () => {
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).get('/shared-links/me').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403); expect(status).toBe(403);
}); });
it('should get data for correct shared link', async () => { it('should get data for correct shared link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key }); const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toEqual( expect(body).toEqual(
@@ -172,7 +172,7 @@ describe('/shared-link', () => {
it('should return unauthorized for incorrect shared link', async () => { it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-link/me') .get('/shared-links/me')
.query({ key: linkWithAlbum.key + 'foo' }); .query({ key: linkWithAlbum.key + 'foo' });
expect(status).toBe(401); expect(status).toBe(401);
@@ -180,14 +180,14 @@ describe('/shared-link', () => {
}); });
it('should return unauthorized if target has been soft deleted', async () => { it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key }); const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey); expect(body).toEqual(errorDto.invalidShareKey);
}); });
it('should return unauthorized for password protected link', async () => { it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key }); const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithPassword.key });
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidSharePassword); expect(body).toEqual(errorDto.invalidSharePassword);
@@ -195,7 +195,7 @@ describe('/shared-link', () => {
it('should get data for correct password protected link', async () => { it('should get data for correct password protected link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get('/shared-link/me') .get('/shared-links/me')
.query({ key: linkWithPassword.key, password: 'foo' }); .query({ key: linkWithPassword.key, password: 'foo' });
expect(status).toBe(200); expect(status).toBe(200);
@@ -209,7 +209,7 @@ describe('/shared-link', () => {
}); });
it('should return metadata for album shared link', async () => { it('should return metadata for album shared link', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key }); const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@@ -225,7 +225,7 @@ describe('/shared-link', () => {
}); });
it('should not return metadata for album shared link without metadata', async () => { it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key }); const { status, body } = await request(app).get('/shared-links/me').query({ key: linkWithoutMetadata.key });
expect(status).toBe(200); expect(status).toBe(200);
expect(body.assets).toHaveLength(1); expect(body.assets).toHaveLength(1);
@@ -239,9 +239,9 @@ describe('/shared-link', () => {
}); });
}); });
describe('GET /shared-link/:id', () => { describe('GET /shared-links/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`); const { status, body } = await request(app).get(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -249,7 +249,7 @@ describe('/shared-link', () => {
it('should get shared link by id', async () => { it('should get shared link by id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`) .get(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -264,7 +264,7 @@ describe('/shared-link', () => {
it('should not get shared link by id if user has not created the link or it does not exist', async () => { it('should not get shared link by id if user has not created the link or it does not exist', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/shared-link/${linkWithAlbum.id}`) .get(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -272,10 +272,10 @@ describe('/shared-link', () => {
}); });
}); });
describe('POST /shared-link', () => { describe('POST /shared-links', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-link') .post('/shared-links')
.send({ type: SharedLinkType.Album, albumId: uuidDto.notFound }); .send({ type: SharedLinkType.Album, albumId: uuidDto.notFound });
expect(status).toBe(401); expect(status).toBe(401);
@@ -284,7 +284,7 @@ describe('/shared-link', () => {
it('should require a type and the correspondent asset/album id', async () => { it('should require a type and the correspondent asset/album id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-link') .post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -293,7 +293,7 @@ describe('/shared-link', () => {
it('should require an asset/album id', async () => { it('should require an asset/album id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-link') .post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album }); .send({ type: SharedLinkType.Album });
@@ -303,7 +303,7 @@ describe('/shared-link', () => {
it('should require a valid asset id', async () => { it('should require a valid asset id', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-link') .post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound }); .send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
@@ -313,7 +313,7 @@ describe('/shared-link', () => {
it('should create a shared link', async () => { it('should create a shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post('/shared-link') .post('/shared-links')
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ type: SharedLinkType.Album, albumId: album.id }); .send({ type: SharedLinkType.Album, albumId: album.id });
@@ -327,10 +327,10 @@ describe('/shared-link', () => {
}); });
}); });
describe('PATCH /shared-link/:id', () => { describe('PATCH /shared-links/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`) .patch(`/shared-links/${linkWithAlbum.id}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
expect(status).toBe(401); expect(status).toBe(401);
@@ -339,7 +339,7 @@ describe('/shared-link', () => {
it('should fail if invalid link', async () => { it('should fail if invalid link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-link/${uuidDto.notFound}`) .patch(`/shared-links/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
@@ -349,7 +349,7 @@ describe('/shared-link', () => {
it('should update shared link', async () => { it('should update shared link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.patch(`/shared-link/${linkWithAlbum.id}`) .patch(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'foo' }); .send({ description: 'foo' });
@@ -364,10 +364,10 @@ describe('/shared-link', () => {
}); });
}); });
describe('PUT /shared-link/:id/assets', () => { describe('PUT /shared-links/:id/assets', () => {
it('should not add assets to shared link (album)', async () => { it('should not add assets to shared link (album)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/shared-link/${linkWithAlbum.id}/assets`) .put(`/shared-links/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -377,7 +377,7 @@ describe('/shared-link', () => {
it('should add an assets to a shared link (individual)', async () => { it('should add an assets to a shared link (individual)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/shared-link/${linkWithAssets.id}/assets`) .put(`/shared-links/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -386,10 +386,10 @@ describe('/shared-link', () => {
}); });
}); });
describe('DELETE /shared-link/:id/assets', () => { describe('DELETE /shared-links/:id/assets', () => {
it('should not remove assets from a shared link (album)', async () => { it('should not remove assets from a shared link (album)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}/assets`) .delete(`/shared-links/${linkWithAlbum.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -399,7 +399,7 @@ describe('/shared-link', () => {
it('should remove assets from a shared link (individual)', async () => { it('should remove assets from a shared link (individual)', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-link/${linkWithAssets.id}/assets`) .delete(`/shared-links/${linkWithAssets.id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`) .set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset2.id] }); .send({ assetIds: [asset2.id] });
@@ -408,9 +408,9 @@ describe('/shared-link', () => {
}); });
}); });
describe('DELETE /shared-link/:id', () => { describe('DELETE /shared-links/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`); const { status, body } = await request(app).delete(`/shared-links/${linkWithAlbum.id}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
@@ -418,7 +418,7 @@ describe('/shared-link', () => {
it('should fail if invalid link', async () => { it('should fail if invalid link', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/shared-link/${uuidDto.notFound}`) .delete(`/shared-links/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400); expect(status).toBe(400);
@@ -427,7 +427,7 @@ describe('/shared-link', () => {
it('should delete a shared link', async () => { it('should delete a shared link', async () => {
const { status } = await request(app) const { status } = await request(app)
.delete(`/shared-link/${linkWithAlbum.id}`) .delete(`/shared-links/${linkWithAlbum.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`); .set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -86,6 +86,26 @@ describe('/system-config', () => {
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should always return the new config', async () => {
const config = await getSystemConfig(admin.accessToken);
const response1 = await request(app)
.put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: false } });
expect(response1.status).toBe(200);
expect(response1.body).toEqual({ ...config, newVersionCheck: { enabled: false } });
const response2 = await request(app)
.put('/system-config')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...config, newVersionCheck: { enabled: true } });
expect(response2.status).toBe(200);
expect(response2.body).toEqual({ ...config, newVersionCheck: { enabled: true } });
});
it('should reject an invalid config entry', async () => { it('should reject an invalid config entry', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put('/system-config') .put('/system-config')
+9 -10
View File
@@ -1,4 +1,4 @@
import { LoginResponseDto, getAllAssets } from '@immich/sdk'; import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
import { Socket } from 'socket.io-client'; import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils'; import { app, asBearerAuth, utils } from 'src/utils';
@@ -31,17 +31,16 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken); const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]); await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
expect(before.length).toBeGreaterThanOrEqual(1);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId }); await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) }); const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.length).toBe(0); expect(after.total).toBe(0);
}); });
}); });
@@ -57,14 +56,14 @@ describe('/trash', () => {
const { id: assetId } = await utils.createAsset(admin.accessToken); const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]); await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await utils.getAssetInfo(admin.accessToken, assetId); const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before.isTrashed).toBe(true); expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true }));
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`); const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204); expect(status).toBe(204);
const after = await utils.getAssetInfo(admin.accessToken, assetId); const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(after.isTrashed).toBe(false); expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
}); });
}); });
+28 -45
View File
@@ -6,7 +6,7 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/user', () => { describe('/users', () => {
let websocket: Socket; let websocket: Socket;
let admin: LoginResponseDto; let admin: LoginResponseDto;
@@ -34,15 +34,15 @@ describe('/user', () => {
utils.disconnectWebsocket(websocket); utils.disconnectWebsocket(websocket);
}); });
describe('GET /user', () => { describe('GET /users', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get('/user'); const { status, body } = await request(app).get('/users');
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should get users', async () => { it('should get users', async () => {
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get('/users').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200); expect(status).toEqual(200);
expect(body).toHaveLength(5); expect(body).toHaveLength(5);
expect(body).toEqual( expect(body).toEqual(
@@ -58,7 +58,7 @@ describe('/user', () => {
it('should hide deleted users', async () => { it('should hide deleted users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/user`) .get(`/users`)
.query({ isAll: true }) .query({ isAll: true })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -75,7 +75,7 @@ describe('/user', () => {
it('should include deleted users', async () => { it('should include deleted users', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/user`) .get(`/users`)
.query({ isAll: false }) .query({ isAll: false })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -93,15 +93,15 @@ describe('/user', () => {
}); });
}); });
describe('GET /user/info/:id', () => { describe('GET /users/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status } = await request(app).get(`/user/info/${admin.userId}`); const { status } = await request(app).get(`/users/${admin.userId}`);
expect(status).toEqual(401); expect(status).toEqual(401);
}); });
it('should get the user info', async () => { it('should get the user info', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.get(`/user/info/${admin.userId}`) .get(`/users/${admin.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
@@ -111,15 +111,15 @@ describe('/user', () => {
}); });
}); });
describe('GET /user/me', () => { describe('GET /users/me', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).get(`/user/me`); const { status, body } = await request(app).get(`/users/me`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should get my info', async () => { it('should get my info', async () => {
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`); const { status, body } = await request(app).get(`/users/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
expect(body).toMatchObject({ expect(body).toMatchObject({
id: admin.userId, id: admin.userId,
@@ -128,9 +128,9 @@ describe('/user', () => {
}); });
}); });
describe('POST /user', () => { describe('POST /users', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1); const { status, body } = await request(app).post(`/users`).send(createUserDto.user1);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -138,7 +138,7 @@ describe('/user', () => {
for (const key of Object.keys(createUserDto.user1)) { for (const key of Object.keys(createUserDto.user1)) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/user`) .post(`/users`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null }); .send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
@@ -148,7 +148,7 @@ describe('/user', () => {
it('should ignore `isAdmin`', async () => { it('should ignore `isAdmin`', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/user`) .post(`/users`)
.send({ .send({
isAdmin: true, isAdmin: true,
email: 'user5@immich.cloud', email: 'user5@immich.cloud',
@@ -166,7 +166,7 @@ describe('/user', () => {
it('should create a user without memories enabled', async () => { it('should create a user without memories enabled', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.post(`/user`) .post(`/users`)
.send({ .send({
email: 'no-memories@immich.cloud', email: 'no-memories@immich.cloud',
password: 'Password123', password: 'Password123',
@@ -182,16 +182,16 @@ describe('/user', () => {
}); });
}); });
describe('DELETE /user/:id', () => { describe('DELETE /users/:id', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`); const { status, body } = await request(app).delete(`/users/${userToDelete.userId}`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
it('should delete user', async () => { it('should delete user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/user/${userToDelete.userId}`) .delete(`/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200); expect(status).toBe(200);
@@ -204,7 +204,7 @@ describe('/user', () => {
it('should hard delete user', async () => { it('should hard delete user', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.delete(`/user/${userToHardDelete.userId}`) .delete(`/users/${userToHardDelete.userId}`)
.send({ force: true }) .send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -219,9 +219,9 @@ describe('/user', () => {
}); });
}); });
describe('PUT /user', () => { describe('PUT /users', () => {
it('should require authentication', async () => { it('should require authentication', async () => {
const { status, body } = await request(app).put(`/user`); const { status, body } = await request(app).put(`/users`);
expect(status).toBe(401); expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized); expect(body).toEqual(errorDto.unauthorized);
}); });
@@ -229,7 +229,7 @@ describe('/user', () => {
for (const key of Object.keys(userDto.admin)) { for (const key of Object.keys(userDto.admin)) {
it(`should not allow null ${key}`, async () => { it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/users`)
.set('Authorization', `Bearer ${admin.accessToken}`) .set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...userDto.admin, [key]: null }); .send({ ...userDto.admin, [key]: null });
expect(status).toBe(400); expect(status).toBe(400);
@@ -239,7 +239,7 @@ describe('/user', () => {
it('should not allow a non-admin to become an admin', async () => { it('should not allow a non-admin to become an admin', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/users`)
.send({ isAdmin: true, id: nonAdmin.userId }) .send({ isAdmin: true, id: nonAdmin.userId })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -249,7 +249,7 @@ describe('/user', () => {
it('ignores updates to profileImagePath', async () => { it('ignores updates to profileImagePath', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/users`)
.send({ id: admin.userId, profileImagePath: 'invalid.jpg' }) .send({ id: admin.userId, profileImagePath: 'invalid.jpg' })
.set('Authorization', `Bearer ${admin.accessToken}`); .set('Authorization', `Bearer ${admin.accessToken}`);
@@ -257,28 +257,11 @@ describe('/user', () => {
expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' }); expect(body).toMatchObject({ id: admin.userId, profileImagePath: '' });
}); });
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({
id: admin.userId,
createdAt: '2023-01-01T00:00:00.000Z',
updatedAt: '2023-01-01T00:00:00.000Z',
deletedAt: '2023-01-01T00:00:00.000Z',
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toStrictEqual(before);
});
it('should update first and last name', async () => { it('should update first and last name', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/users`)
.send({ .send({
id: admin.userId, id: admin.userId,
name: 'Name', name: 'Name',
@@ -297,7 +280,7 @@ describe('/user', () => {
it('should update memories enabled', async () => { it('should update memories enabled', async () => {
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) }); const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/user`) .put(`/users`)
.send({ .send({
id: admin.userId, id: admin.userId,
memoriesEnabled: false, memoriesEnabled: false,
+41 -41
View File
@@ -1,4 +1,4 @@
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk'; import { LoginResponseDto, getAllAlbums, getAssetStatistics } from '@immich/sdk';
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { mkdir, readdir, rm, symlink } from 'node:fs/promises'; import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils'; import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
@@ -28,8 +28,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1); expect(assets.total).toBe(1);
}); });
it('should skip a duplicate file', async () => { it('should skip a duplicate file', async () => {
@@ -40,8 +40,8 @@ describe(`immich upload`, () => {
); );
expect(first.exitCode).toBe(0); expect(first.exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1); expect(assets.total).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
expect(second.stderr).toBe(''); expect(second.stderr).toBe('');
@@ -60,8 +60,8 @@ describe(`immich upload`, () => {
expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')])); expect(stdout.split('\n')).toEqual(expect.arrayContaining([expect.stringContaining('No files found, exiting')]));
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -76,8 +76,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
}); });
it('dry run should handle duplicates', async () => { it('dry run should handle duplicates', async () => {
@@ -88,8 +88,8 @@ describe(`immich upload`, () => {
); );
expect(first.exitCode).toBe(0); expect(first.exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1); expect(assets.total).toBe(1);
const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']); const second = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--dry-run']);
expect(second.stderr).toBe(''); expect(second.stderr).toBe('');
@@ -112,8 +112,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9); expect(assets.total).toBe(9);
}); });
}); });
@@ -135,8 +135,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9); expect(assets.total).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1); expect(albums.length).toBe(1);
@@ -151,8 +151,8 @@ describe(`immich upload`, () => {
expect(response1.stderr).toBe(''); expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0); expect(response1.exitCode).toBe(0);
const assets1 = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets1 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets1.length).toBe(9); expect(assets1.total).toBe(9);
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0); expect(albums1.length).toBe(0);
@@ -167,8 +167,8 @@ describe(`immich upload`, () => {
expect(response2.stderr).toBe(''); expect(response2.stderr).toBe('');
expect(response2.exitCode).toBe(0); expect(response2.exitCode).toBe(0);
const assets2 = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets2 = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets2.length).toBe(9); expect(assets2.total).toBe(9);
const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums2 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums2.length).toBe(1); expect(albums2.length).toBe(1);
@@ -193,8 +193,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0); expect(albums.length).toBe(0);
@@ -219,8 +219,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9); expect(assets.total).toBe(9);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(1); expect(albums.length).toBe(1);
@@ -245,8 +245,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
const albums = await getAllAlbums({}, { headers: asKeyAuth(key) }); const albums = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums.length).toBe(0); expect(albums.length).toBe(0);
@@ -276,8 +276,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9); expect(assets.total).toBe(9);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -302,8 +302,8 @@ describe(`immich upload`, () => {
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
}); });
}); });
@@ -328,8 +328,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1); expect(assets.total).toBe(1);
}); });
it('should throw an error if attempting dry run', async () => { it('should throw an error if attempting dry run', async () => {
@@ -344,8 +344,8 @@ describe(`immich upload`, () => {
expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`); expect(stderr).toEqual(`error: option '-n, --dry-run' cannot be used with option '-h, --skip-hash'`);
expect(exitCode).not.toBe(0); expect(exitCode).not.toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
}); });
}); });
@@ -367,8 +367,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(9); expect(assets.total).toBe(9);
}); });
it('should reject string argument', async () => { it('should reject string argument', async () => {
@@ -408,8 +408,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(8); expect(assets.total).toBe(8);
}); });
it('should ignore assets matching glob pattern', async () => { it('should ignore assets matching glob pattern', async () => {
@@ -429,8 +429,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(1); expect(assets.total).toBe(1);
}); });
it('should have accurate dry run', async () => { it('should have accurate dry run', async () => {
@@ -451,8 +451,8 @@ describe(`immich upload`, () => {
); );
expect(exitCode).toBe(0); expect(exitCode).toBe(0);
const assets = await getAllAssets({}, { headers: asKeyAuth(key) }); const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
expect(assets.length).toBe(0); expect(assets.total).toBe(0);
}); });
}); });
}); });
-5
View File
@@ -51,11 +51,6 @@ export const errorDto = {
statusCode: 400, statusCode: 400,
message: 'The server already has an admin', message: 'The server already has an admin',
}, },
noDeleteUploadLibrary: {
error: 'Bad Request',
statusCode: 400,
message: 'Cannot delete the last upload library',
},
}; };
export const signupResponseDto = { export const signupResponseDto = {
+1 -1
View File
@@ -17,7 +17,7 @@ const setup = async () => {
child.stdout.on('data', (data) => { child.stdout.on('data', (data) => {
const input = data.toString(); const input = data.toString();
console.log(input); console.log(input);
if (input.includes('Immich Microservices is listening')) { if (input.includes('Immich Microservices is running')) {
_resolve(); _resolve();
} }
}); });
+7 -8
View File
@@ -13,17 +13,17 @@ import {
createAlbum, createAlbum,
createApiKey, createApiKey,
createLibrary, createLibrary,
createPartner,
createPerson, createPerson,
createSharedLink, createSharedLink,
createUser, createUser,
defaults,
deleteAssets, deleteAssets,
getAllAssets,
getAllJobsStatus, getAllJobsStatus,
getAssetInfo, getAssetInfo,
getConfigDefaults, getConfigDefaults,
login, login,
searchMetadata, searchMetadata,
setBaseUrl,
signUpAdmin, signUpAdmin,
updateAdminOnboarding, updateAdminOnboarding,
updateAlbumUser, updateAlbumUser,
@@ -145,7 +145,6 @@ export const utils = {
'sessions', 'sessions',
'users', 'users',
'system_metadata', 'system_metadata',
'system_config',
]; ];
const sql: string[] = []; const sql: string[] = [];
@@ -256,8 +255,8 @@ export const utils = {
}); });
}, },
setApiEndpoint: () => { initSdk: () => {
defaults.baseUrl = app; setBaseUrl(app);
}, },
adminSetup: async (options?: AdminSetupOptions) => { adminSetup: async (options?: AdminSetupOptions) => {
@@ -341,8 +340,6 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }), getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
getAllAssets: (accessToken: string) => getAllAssets({}, { 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) });
}, },
@@ -389,6 +386,8 @@ export const utils = {
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) => validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }), validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string) => setAuthCookies: async (context: BrowserContext, accessToken: string) =>
await context.addCookies([ await context.addCookies([
{ {
@@ -463,7 +462,7 @@ export const utils = {
}, },
}; };
utils.setApiEndpoint(); utils.initSdk();
if (!existsSync(`${testAssetDir}/albums`)) { if (!existsSync(`${testAssetDir}/albums`)) {
throw new Error( throw new Error(
@@ -0,0 +1,60 @@
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test('can be opened for shared links', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toBeVisible();
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('cannot be opened for shared links with hidden metadata', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await expect(page.getByRole('button', { name: 'Info' })).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
await page.keyboard.press('i');
await expect(page.locator('#detail-panel')).toHaveCount(0);
});
test('description is visible for owner on shared links', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
const textarea = page.getByRole('textbox', { name: 'Add a description' });
await page.getByRole('button', { name: 'Info' }).click();
await expect(textarea).toBeVisible();
await expect(textarea).not.toBeDisabled();
});
});
@@ -0,0 +1,52 @@
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Asset Viewer Navbar', () => {
let admin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
});
test.describe('shared link without metadata', () => {
test('visible guest actions', async ({ page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
test('visible owner actions', async ({ context, page }) => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
showMetadata: false,
});
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/share/${sharedLink.key}/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
const expected = ['Share', 'Zoom Image', 'Copy Image', 'Download'];
const buttons = await page.getByTestId('asset-viewer-navbar-actions').getByRole('button').all();
for (const [i, button] of buttons.entries()) {
await expect(button).toHaveAccessibleName(expected[i]);
}
});
});
});
+1 -1
View File
@@ -3,7 +3,7 @@ import { utils } from 'src/utils';
test.describe('Registration', () => { test.describe('Registration', () => {
test.beforeAll(() => { test.beforeAll(() => {
utils.setApiEndpoint(); utils.initSdk();
}); });
test.beforeEach(async () => { test.beforeEach(async () => {
+1 -1
View File
@@ -17,7 +17,7 @@ test.describe('Shared Links', () => {
let sharedLinkPassword: SharedLinkResponseDto; let sharedLinkPassword: SharedLinkResponseDto;
test.beforeAll(async () => { test.beforeAll(async () => {
utils.setApiEndpoint(); utils.initSdk();
await utils.resetDatabase(); await utils.resetDatabase();
admin = await utils.adminSetup(); admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken); asset = await utils.createAsset(admin.accessToken);
+40 -15
View File
@@ -2,14 +2,15 @@
set -o nounset set -o nounset
set -o pipefail set -o pipefail
create_immich_directory() { local -r Tgt='./immich-app' create_immich_directory() {
local -r Tgt='./immich-app'
echo "Creating Immich directory..." echo "Creating Immich directory..."
if [[ -e $Tgt ]]; then if [[ -e $Tgt ]]; then
echo "Found existing directory $Tgt, will overwrite YAML files" echo "Found existing directory $Tgt, will overwrite YAML files"
else else
mkdir "$Tgt" || return mkdir "$Tgt" || return
fi fi
cd "$Tgt" || return cd "$Tgt" || return 1
} }
download_docker_compose_file() { download_docker_compose_file() {
@@ -22,6 +23,16 @@ download_dot_env_file() {
"${Curl[@]}" "$RepoUrl"/example.env -o ./.env "${Curl[@]}" "$RepoUrl"/example.env -o ./.env
} }
generate_random_password() {
echo "Generate random password for .env file..."
rand_pass=$(echo "$RANDOM$(date)$RANDOM" | sha256sum | base64 | head -c10)
if [ -z "$rand_pass" ]; then
sed -i -e "s/DB_PASSWORD=postgres/DB_PASSWORD=postgres${RANDOM}${RANDOM}/" ./.env
else
sed -i -e "s/DB_PASSWORD=postgres/DB_PASSWORD=${rand_pass}/" ./.env
fi
}
start_docker_compose() { start_docker_compose() {
echo "Starting Immich's docker containers" echo "Starting Immich's docker containers"
@@ -40,16 +51,16 @@ start_docker_compose() {
show_friendly_message() { show_friendly_message() {
local ip_address local ip_address
ip_address=$(hostname -I | awk '{print $1}') ip_address=$(hostname -I | awk '{print $1}')
cat << EOF cat <<EOF
Successfully deployed Immich! Successfully deployed Immich!
You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api
--------------------------------------------------- ---------------------------------------------------
If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc. If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
1. First bring down the containers with the command 'docker compose down' in the immich-app directory, 1. First bring down the containers with the command 'docker compose down' in the immich-app directory,
2. Then change the information that fits your needs in the '.env' file, 2. Then change the information that fits your needs in the '.env' file,
3. Finally, bring the containers back up with the command 'docker compose up --remove-orphans -d' in the immich-app directory 3. Finally, bring the containers back up with the command 'docker compose up --remove-orphans -d' in the immich-app directory
EOF EOF
} }
@@ -66,11 +77,25 @@ main() {
return 14 return 14
fi fi
create_immich_directory || { echo 'error creating Immich directory'; return 10; } create_immich_directory || {
download_docker_compose_file || { echo 'error downloading Docker Compose file'; return 11; } echo 'error creating Immich directory'
download_dot_env_file || { echo 'error downloading .env'; return 12; } return 10
start_docker_compose || { echo 'error starting Docker'; return 13; } }
return 0; } download_docker_compose_file || {
echo 'error downloading Docker Compose file'
return 11
}
download_dot_env_file || {
echo 'error downloading .env'
return 12
}
generate_random_password
start_docker_compose || {
echo 'error starting Docker'
return 13
}
return 0
}
main main
Exit=$? Exit=$?
+8 -4
View File
@@ -1,6 +1,6 @@
ARG DEVICE=cpu ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:adc02660aaaa7e7b482a2689237c5fa88d5b2218aebfa003f6af8fa7c8113378 as builder-cpu FROM python:3.11-bookworm@sha256:96de1ea4821d73fd2c1853d1fdc3cf794ccfe2fae4c3f08579e846de51760a61 as builder-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
USER root USER root
@@ -36,10 +36,13 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./ COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:6d2502238109c929569ae99355e28890c438cb11bc88ef02cd189c173b3db07c as prod-cpu FROM python:3.11-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b83dbade835e2a171ca27ef as prod-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
USER root USER root
# TODO: remove this once the image has the fix for https://github.com/intel/compute-runtime/issues/710
ENV NEOReadDebugKeys=1 \
OverrideGpuAddressSpace=48
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
@@ -74,8 +77,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV TRANSFORMERS_CACHE=/cache \
TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \ PATH="/opt/venv/bin:$PATH" \
@@ -93,3 +95,5 @@ COPY start.sh log_conf.json ./
COPY app . COPY app .
ENTRYPOINT ["tini", "--"] ENTRYPOINT ["tini", "--"]
CMD ["./start.sh"] CMD ["./start.sh"]
HEALTHCHECK CMD python3 healthcheck.py
+2 -2
View File
@@ -41,7 +41,7 @@ class Settings(BaseSettings):
class LogSettings(BaseSettings): class LogSettings(BaseSettings):
log_level: str = "info" immich_log_level: str = "info"
no_color: bool = False no_color: bool = False
class Config: class Config:
@@ -77,7 +77,7 @@ LOG_LEVELS: dict[str, int] = {
settings = Settings() settings = Settings()
log_settings = LogSettings() log_settings = LogSettings()
LOG_LEVEL = LOG_LEVELS.get(log_settings.log_level.lower(), logging.INFO) LOG_LEVEL = LOG_LEVELS.get(log_settings.immich_log_level.lower(), logging.INFO)
class CustomRichHandler(RichHandler): class CustomRichHandler(RichHandler):
+14
View File
@@ -0,0 +1,14 @@
import os
import sys
import requests
port = os.getenv("IMMICH_PORT", 3003)
try:
response = requests.get(f"http://localhost:{port}/ping", timeout=2)
if response.status_code == 200:
sys.exit(0)
sys.exit(1)
except requests.RequestException:
sys.exit(1)
+2 -3
View File
@@ -1,7 +1,6 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:abcb3ae7e3521d08e1fdeaff63131765b34e4f29b6a8a2c28660036b53841569 as builder FROM mambaorg/micromamba:bookworm-slim@sha256:d5b82811074b396275ef69aadbf31098257dd8836e231371e9cdb393128e571c as builder
ENV NODE_ENV=production \ ENV TRANSFORMERS_CACHE=/cache \
TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
PATH="/opt/venv/bin:$PATH" \ PATH="/opt/venv/bin:$PATH" \
+46 -56
View File
@@ -679,14 +679,14 @@ files = [
test = ["pytest (>=6)"] test = ["pytest (>=6)"]
[[package]] [[package]]
name = "fastapi" name = "fastapi-slim"
version = "0.110.3" version = "0.111.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "fastapi-0.110.3-py3-none-any.whl", hash = "sha256:fd7600612f755e4050beb74001310b5a7e1796d149c2ee363124abdfa0289d32"}, {file = "fastapi_slim-0.111.0-py3-none-any.whl", hash = "sha256:6e4b04a555496e5a2590031fcae3ef8e364ad4901b340033e2e1d8136471aca2"},
{file = "fastapi-0.110.3.tar.gz", hash = "sha256:555700b0159379e94fdbfc6bb66a0f1c43f4cf7060f25239af3d84b63a656626"}, {file = "fastapi_slim-0.111.0.tar.gz", hash = "sha256:100720e4362ec4de97dee83a579b970e79fb5bf48073b37c9ce9b0e63dda4bec"},
] ]
[package.dependencies] [package.dependencies]
@@ -695,7 +695,8 @@ starlette = ">=0.37.2,<0.38.0"
typing-extensions = ">=4.8.0" typing-extensions = ">=4.8.0"
[package.extras] [package.extras]
all = ["email_validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] all = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
standard = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.7)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
[[package]] [[package]]
name = "filelock" name = "filelock"
@@ -737,13 +738,13 @@ dotenv = ["python-dotenv"]
[[package]] [[package]]
name = "flask-cors" name = "flask-cors"
version = "4.0.0" version = "4.0.1"
description = "A Flask extension adding a decorator for CORS support" description = "A Flask extension adding a decorator for CORS support"
optional = false optional = false
python-versions = "*" python-versions = "*"
files = [ files = [
{file = "Flask-Cors-4.0.0.tar.gz", hash = "sha256:f268522fcb2f73e2ecdde1ef45e2fd5c71cc48fe03cffb4b441c6d1b40684eb0"}, {file = "Flask_Cors-4.0.1-py2.py3-none-any.whl", hash = "sha256:f2a704e4458665580c074b714c4627dd5a306b333deb9074d0b1794dfa2fb677"},
{file = "Flask_Cors-4.0.0-py2.py3-none-any.whl", hash = "sha256:bc3492bfd6368d27cfe79c7821df5a8a319e1a6d5eab277a3794be19bdc51783"}, {file = "flask_cors-4.0.1.tar.gz", hash = "sha256:eeb69b342142fdbf4766ad99357a7f3876a2ceb77689dc10ff912aac06c389e4"},
] ]
[package.dependencies] [package.dependencies]
@@ -1235,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]] [[package]]
name = "huggingface-hub" name = "huggingface-hub"
version = "0.22.2" version = "0.23.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false optional = false
python-versions = ">=3.8.0" python-versions = ">=3.8.0"
files = [ files = [
{file = "huggingface_hub-0.22.2-py3-none-any.whl", hash = "sha256:3429e25f38ccb834d310804a3b711e7e4953db5a9e420cc147a5e194ca90fd17"}, {file = "huggingface_hub-0.23.0-py3-none-any.whl", hash = "sha256:075c30d48ee7db2bba779190dc526d2c11d422aed6f9044c5e2fdc2c432fdb91"},
{file = "huggingface_hub-0.22.2.tar.gz", hash = "sha256:32e9a9a6843c92f253ff9ca16b9985def4d80a93fb357af5353f770ef74a81be"}, {file = "huggingface_hub-0.23.0.tar.gz", hash = "sha256:7126dedd10a4c6fac796ced4d87a8cf004efc722a5125c2c09299017fa366fa9"},
] ]
[package.dependencies] [package.dependencies]
@@ -1254,16 +1255,16 @@ tqdm = ">=4.42.1"
typing-extensions = ">=3.7.4.3" typing-extensions = ">=3.7.4.3"
[package.extras] [package.extras]
all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
cli = ["InquirerPy (==0.3.4)"] cli = ["InquirerPy (==0.3.4)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "mypy (==1.5.1)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.3.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
hf-transfer = ["hf-transfer (>=0.1.4)"] hf-transfer = ["hf-transfer (>=0.1.4)"]
inference = ["aiohttp", "minijinja (>=1.0)"] inference = ["aiohttp", "minijinja (>=1.0)"]
quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"] quality = ["mypy (==1.5.1)", "ruff (>=0.3.0)"]
tensorflow = ["graphviz", "pydot", "tensorflow"] tensorflow = ["graphviz", "pydot", "tensorflow"]
tensorflow-testing = ["keras (<3.0)", "tensorflow"] tensorflow-testing = ["keras (<3.0)", "tensorflow"]
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "fastapi", "gradio", "jedi", "minijinja (>=1.0)", "numpy", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
torch = ["safetensors", "torch"] torch = ["safetensors", "torch"]
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
@@ -1373,13 +1374,13 @@ files = [
[[package]] [[package]]
name = "jinja2" name = "jinja2"
version = "3.1.3" version = "3.1.4"
description = "A very fast and expressive template engine." description = "A very fast and expressive template engine."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
{file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
] ]
[package.dependencies] [package.dependencies]
@@ -1529,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]] [[package]]
name = "locust" name = "locust"
version = "2.26.0" version = "2.27.0"
description = "Developer friendly load testing framework" description = "Developer friendly load testing framework"
optional = false optional = false
python-versions = ">=3.9" python-versions = ">=3.9"
files = [ files = [
{file = "locust-2.26.0-py3-none-any.whl", hash = "sha256:7957d8346e5830ba35e3a7a9c1eebe0fb73b0be117e54213c61ef3bc658a1ae6"}, {file = "locust-2.27.0-py3-none-any.whl", hash = "sha256:c4db5747eb9a3851216deae8147143d335db41978a9291ac32e113fa9ec8ad39"},
{file = "locust-2.26.0.tar.gz", hash = "sha256:a5cb4c96b8fa1ae5c20876ab8ca9d1e980d56148ed3c187df610cc2546705bff"}, {file = "locust-2.27.0.tar.gz", hash = "sha256:0c6d3d2523976dafe734012c41b2f7d9ad7120cbcea76d47d80cec5d6d139905"},
] ]
[package.dependencies] [package.dependencies]
@@ -1550,7 +1551,6 @@ psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""} pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
pyzmq = ">=25.0.0" pyzmq = ">=25.0.0"
requests = ">=2.26.0" requests = ">=2.26.0"
roundrobin = ">=0.0.2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
Werkzeug = ">=2.0.0" Werkzeug = ">=2.0.0"
@@ -2797,40 +2797,30 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras] [package.extras]
jupyter = ["ipywidgets (>=7.5.1,<9)"] jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "roundrobin"
version = "0.0.4"
description = "Collection of roundrobin utilities"
optional = false
python-versions = "*"
files = [
{file = "roundrobin-0.0.4.tar.gz", hash = "sha256:7e9d19a5bd6123d99993fb935fa86d25c88bb2096e493885f61737ed0f5e9abd"},
]
[[package]] [[package]]
name = "ruff" name = "ruff"
version = "0.4.2" version = "0.4.4"
description = "An extremely fast Python linter and code formatter, written in Rust." description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:8d14dc8953f8af7e003a485ef560bbefa5f8cc1ad994eebb5b12136049bbccc5"}, {file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:24016ed18db3dc9786af103ff49c03bdf408ea253f3cb9e3638f39ac9cf2d483"}, {file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e2e06459042ac841ed510196c350ba35a9b24a643e23db60d79b2db92af0c2b"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3afabaf7ba8e9c485a14ad8f4122feff6b2b93cc53cd4dad2fd24ae35112d5c5"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:799eb468ea6bc54b95527143a4ceaf970d5aa3613050c6cff54c85fda3fde480"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ec4ba9436a51527fb6931a8839af4c36a5481f8c19e8f5e42c2f7ad3a49f5069"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6a2243f8f434e487c2a010c7252150b1fdf019035130f41b77626f5655c9ca22"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8772130a063f3eebdf7095da00c0b9898bd1774c43b336272c3e98667d4fb8fa"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ab165ef5d72392b4ebb85a8b0fbd321f69832a632e07a74794c0e598e7a8376"}, {file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f32cadf44c2020e75e0c56c3408ed1d32c024766bd41aedef92aa3ca28eef68"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:22e306bf15e09af45ca812bc42fa59b628646fa7c26072555f278994890bc7ac"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:82986bb77ad83a1719c90b9528a9dd663c9206f7c0ab69282af8223566a0c34e"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:652e4ba553e421a6dc2a6d4868bc3b3881311702633eb3672f9f244ded8908cd"}, {file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.2-py3-none-win32.whl", hash = "sha256:7891ee376770ac094da3ad40c116258a381b86c7352552788377c6eb16d784fe"}, {file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.2-py3-none-win_amd64.whl", hash = "sha256:5ec481661fb2fd88a5d6cf1f83403d388ec90f9daaa36e40e2c003de66751798"}, {file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.2-py3-none-win_arm64.whl", hash = "sha256:cbd1e87c71bca14792948c4ccb51ee61c3296e164019d2d484f3eaa2d360dfaf"}, {file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.2.tar.gz", hash = "sha256:33bcc160aee2520664bc0859cfeaebc84bb7323becff3f303b8f1f2d81cb4edc"}, {file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
] ]
[[package]] [[package]]
@@ -3197,13 +3187,13 @@ files = [
[[package]] [[package]]
name = "tqdm" name = "tqdm"
version = "4.66.1" version = "4.66.3"
description = "Fast, Extensible Progress Meter" description = "Fast, Extensible Progress Meter"
optional = false optional = false
python-versions = ">=3.7" python-versions = ">=3.7"
files = [ files = [
{file = "tqdm-4.66.1-py3-none-any.whl", hash = "sha256:d302b3c5b53d47bce91fea46679d9c3c6508cf6332229aa1e7d8653723793386"}, {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"},
{file = "tqdm-4.66.1.tar.gz", hash = "sha256:d88e651f9db8d8551a62556d3cff9e3034274ca5d66e93197cf2490e2dcb69c7"}, {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"},
] ]
[package.dependencies] [package.dependencies]
@@ -3493,13 +3483,13 @@ files = [
[[package]] [[package]]
name = "werkzeug" name = "werkzeug"
version = "3.0.1" version = "3.0.3"
description = "The comprehensive WSGI web application library." description = "The comprehensive WSGI web application library."
optional = false optional = false
python-versions = ">=3.8" python-versions = ">=3.8"
files = [ files = [
{file = "werkzeug-3.0.1-py3-none-any.whl", hash = "sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10"}, {file = "werkzeug-3.0.3-py3-none-any.whl", hash = "sha256:fc9645dc43e03e4d630d23143a04a7f947a9a3b5727cd535fdfe155a17cc48c8"},
{file = "werkzeug-3.0.1.tar.gz", hash = "sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc"}, {file = "werkzeug-3.0.3.tar.gz", hash = "sha256:097e5bfda9f0aba8da6b8545146def481d06aa7d3266e7448e2cccf67dd8bd18"},
] ]
[package.dependencies] [package.dependencies]
@@ -3582,4 +3572,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.10,<3.12" python-versions = ">=3.10,<3.12"
content-hash = "1b014276ec94f9389459a70d31f0d96d1dd5a138bcc988900865e5f07a72bc62" content-hash = "db51ad1e631b569e106927683a13124252bd80974def1f2edbe23ac87d89c461"
+2 -2
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.103.1" version = "1.105.1"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
@@ -11,7 +11,7 @@ python = ">=3.10,<3.12"
insightface = ">=0.7.3,<1.0" insightface = ">=0.7.3,<1.0"
opencv-python-headless = ">=4.7.0.72,<5.0" opencv-python-headless = ">=4.7.0.72,<5.0"
pillow = ">=9.5.0,<11.0" pillow = ">=9.5.0,<11.0"
fastapi = ">=0.95.2,<1.0" fastapi-slim = ">=0.95.2,<1.0"
uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"} uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"}
pydantic = "^1.10.8" pydantic = "^1.10.8"
aiocache = ">=0.12.1,<1.0" aiocache = ">=0.12.1,<1.0"
+4 -4
View File
@@ -2,20 +2,20 @@
lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2" lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
# mimalloc seems to increase memory usage dramatically with openvino, need to investigate # mimalloc seems to increase memory usage dramatically with openvino, need to investigate
if ! [ "$DEVICE" = "openvino" ]; then if ! [ "$DEVICE" = "openvino" ]; then
export LD_PRELOAD="$lib_path" export LD_PRELOAD="$lib_path"
export LD_BIND_NOW=1 export LD_BIND_NOW=1
fi fi
: "${MACHINE_LEARNING_HOST:=[::]}" : "${IMMICH_HOST:=[::]}"
: "${MACHINE_LEARNING_PORT:=3003}" : "${IMMICH_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}" : "${MACHINE_LEARNING_WORKERS:=1}"
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}" : "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
gunicorn app.main:app \ gunicorn app.main:app \
-k app.config.CustomUvicornWorker \ -k app.config.CustomUvicornWorker \
-b "$IMMICH_HOST":"$IMMICH_PORT" \
-w "$MACHINE_LEARNING_WORKERS" \ -w "$MACHINE_LEARNING_WORKERS" \
-b "$MACHINE_LEARNING_HOST":"$MACHINE_LEARNING_PORT" \
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \ -t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
--log-config-json log_conf.json \ --log-config-json log_conf.json \
--graceful-timeout 0 --graceful-timeout 0
+2
View File
@@ -62,6 +62,8 @@ fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER" echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
npm --prefix server version "$SERVER_PUMP" npm --prefix server version "$SERVER_PUMP"
npm --prefix server ci
npm --prefix server run build
make open-api make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP" npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP" npm --prefix web version "$SERVER_PUMP"
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"flutter": "3.19.6" "flutter": "3.22.0"
} }
+1 -1
View File
@@ -1,5 +1,5 @@
{ {
"dart.flutterSdkPath": ".fvm/versions/3.19.3", "dart.flutterSdkPath": ".fvm/versions/3.22.0",
"search.exclude": { "search.exclude": {
"**/.fvm": true "**/.fvm": true
}, },
+8 -6
View File
@@ -10,16 +10,16 @@ GEM
artifactory (3.0.17) artifactory (3.0.17)
atomos (0.1.3) atomos (0.1.3)
aws-eventstream (1.3.0) aws-eventstream (1.3.0)
aws-partitions (1.925.0) aws-partitions (1.932.0)
aws-sdk-core (3.194.1) aws-sdk-core (3.196.1)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0) aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.80.0) aws-sdk-kms (1.81.0)
aws-sdk-core (~> 3, >= 3.193.0) aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1) aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.149.1) aws-sdk-s3 (1.151.0)
aws-sdk-core (~> 3, >= 3.194.0) aws-sdk-core (~> 3, >= 3.194.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.8) aws-sigv4 (~> 1.8)
@@ -155,7 +155,7 @@ GEM
mini_magick (4.12.0) mini_magick (4.12.0)
mini_mime (1.1.5) mini_mime (1.1.5)
multi_json (1.15.0) multi_json (1.15.0)
multipart-post (2.4.0) multipart-post (2.4.1)
nanaimo (0.3.0) nanaimo (0.3.0)
naturally (2.2.1) naturally (2.2.1)
nkf (0.2.0) nkf (0.2.0)
@@ -169,7 +169,8 @@ GEM
trailblazer-option (>= 0.1.1, < 0.2.0) trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0) uber (< 0.2.0)
retriable (3.1.2) retriable (3.1.2)
rexml (3.2.6) rexml (3.2.8)
strscan (>= 3.0.9)
rouge (2.0.7) rouge (2.0.7)
ruby2_keywords (0.0.5) ruby2_keywords (0.0.5)
rubyzip (2.3.2) rubyzip (2.3.2)
@@ -182,6 +183,7 @@ GEM
simctl (1.6.10) simctl (1.6.10)
CFPropertyList CFPropertyList
naturally naturally
strscan (3.1.0)
terminal-notifier (2.0.0) terminal-notifier (2.0.0)
terminal-table (1.8.0) terminal-table (1.8.0)
unicode-display_width (~> 1.1, >= 1.1.1) unicode-display_width (~> 1.1, >= 1.1.1)
+2 -2
View File
@@ -81,8 +81,8 @@ flutter {
} }
dependencies { dependencies {
def kotlin_version = '1.9.23' def kotlin_version = '1.9.24'
def kotlin_coroutines_version = '1.8.0' def kotlin_coroutines_version = '1.8.1'
def work_version = '2.9.0' def work_version = '2.9.0'
def concurrent_version = '1.1.0' def concurrent_version = '1.1.0'
def guava_version = '33.2.0-android' def guava_version = '33.2.0-android'
+2
View File
@@ -1,4 +1,6 @@
allprojects { allprojects {
ext.kotlin_version = '1.9.24'
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 137, "android.injected.version.code" => 140,
"android.injected.version.name" => "1.103.1", "android.injected.version.name" => "1.105.1",
} }
) )
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')
+3 -3
View File
@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000261"> <testcase classname="fastlane.lanes" name="0: default_platform" time="0.000374">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="32.48099"> <testcase classname="fastlane.lanes" name="1: bundleRelease" time="84.292464">
</testcase> </testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="30.236974"> <testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.336934">
</testcase> </testcase>
+2 -2
View File
@@ -19,8 +19,8 @@ pluginManagement {
plugins { plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.4.2" apply false id "com.android.application" version "7.4.2" apply false
id "org.jetbrains.kotlin.android" version "1.9.23" apply false id "org.jetbrains.kotlin.android" version "1.9.24" apply false
id "org.jetbrains.kotlin.kapt" version "1.9.23" apply false id "org.jetbrains.kotlin.kapt" version "1.9.24" apply false
} }
include ":app" include ":app"
+1
View File
@@ -410,6 +410,7 @@
"share_add": "يضيف", "share_add": "يضيف",
"share_add_photos": "إضافة الصور", "share_add_photos": "إضافة الصور",
"share_add_title": "إضافة عنوان", "share_add_title": "إضافة عنوان",
"share_assets_selected": "{} selected",
"share_create_album": "إنشاء ألبوم", "share_create_album": "إنشاء ألبوم",
"shared_album_activities_input_disable": "التعليق معطل", "shared_album_activities_input_disable": "التعليق معطل",
"shared_album_activities_input_hint": "قل شيئا", "shared_album_activities_input_hint": "قل شيئا",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Přidat", "share_add": "Přidat",
"share_add_photos": "Přidat fotografie", "share_add_photos": "Přidat fotografie",
"share_add_title": "Přidat název", "share_add_title": "Přidat název",
"share_assets_selected": "{} vybráno",
"share_create_album": "Vytvořit album", "share_create_album": "Vytvořit album",
"shared_album_activities_input_disable": "Komentář je vypnutý", "shared_album_activities_input_disable": "Komentář je vypnutý",
"shared_album_activities_input_hint": "Řekněte něco", "shared_album_activities_input_hint": "Řekněte něco",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Tilføj", "share_add": "Tilføj",
"share_add_photos": "Tilføj billeder", "share_add_photos": "Tilføj billeder",
"share_add_title": "Tilføj en titel", "share_add_title": "Tilføj en titel",
"share_assets_selected": "{} valgt",
"share_create_album": "Opret album", "share_create_album": "Opret album",
"shared_album_activities_input_disable": "Kommentarer er deaktiveret", "shared_album_activities_input_disable": "Kommentarer er deaktiveret",
"shared_album_activities_input_hint": "Skriv noget", "shared_album_activities_input_hint": "Skriv noget",
+5
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Original laden", "setting_image_viewer_original_title": "Original laden",
"setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.", "setting_image_viewer_preview_subtitle": "Aktivieren, um ein Bild mit mittlerer Auflösung zu laden. Deaktivieren, um entweder das Original direkt zu laden oder nur die Miniaturansicht zu verwenden.",
"setting_image_viewer_preview_title": "Vorschaubild laden", "setting_image_viewer_preview_title": "Vorschaubild laden",
"setting_image_viewer_title": "Bilder",
"setting_languages_apply": "Anwenden", "setting_languages_apply": "Anwenden",
"setting_languages_title": "Sprachen", "setting_languages_title": "Sprachen",
"setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}", "setting_notifications_notify_failures_grace_period": "Benachrichtigung über Fehler bei der Hintergrundsicherung: {}",
@@ -406,10 +407,14 @@
"setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)", "setting_notifications_total_progress_subtitle": "Gesamter Upload-Fortschritt (abgeschlossen/Anzahl Elemente)",
"setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung", "setting_notifications_total_progress_title": "Zeige Gesamtfortschritt bei der Hintergrundsicherung",
"setting_pages_app_bar_settings": "Einstellungen", "setting_pages_app_bar_settings": "Einstellungen",
"setting_video_viewer_looping_subtitle": "Aktivieren, damit sich ein Video in der Detailansicht automatisch wiederholt.",
"setting_video_viewer_looping_title": "Wiederholen",
"setting_video_viewer_title": "Videos",
"settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.", "settings_require_restart": "Bitte starte Immich neu, um diese Einstellung anzuwenden.",
"share_add": "Hinzufügen", "share_add": "Hinzufügen",
"share_add_photos": "Fotos hinzufügen", "share_add_photos": "Fotos hinzufügen",
"share_add_title": "Titel hinzufügen", "share_add_title": "Titel hinzufügen",
"share_assets_selected": "{} ausgewählt",
"share_create_album": "Album erstellen", "share_create_album": "Album erstellen",
"shared_album_activities_input_disable": "Kommentare sind deaktiviert.", "shared_album_activities_input_disable": "Kommentare sind deaktiviert.",
"shared_album_activities_input_hint": "Sag etwas", "shared_album_activities_input_hint": "Sag etwas",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
"share_assets_selected": "{} selected",
"share_create_album": "Create album", "share_create_album": "Create album",
"shared_album_activities_input_disable": "Το σχόλιο είναι απενεργοποιημένο", "shared_album_activities_input_disable": "Το σχόλιο είναι απενεργοποιημένο",
"shared_album_activities_input_hint": "Say something", "shared_album_activities_input_hint": "Say something",
+5 -1
View File
@@ -391,6 +391,7 @@
"setting_image_viewer_original_title": "Load original image", "setting_image_viewer_original_title": "Load original image",
"setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.", "setting_image_viewer_preview_subtitle": "Enable to load a medium-resolution image. Disable to either directly load the original or only use the thumbnail.",
"setting_image_viewer_preview_title": "Load preview image", "setting_image_viewer_preview_title": "Load preview image",
"setting_image_viewer_title": "Images",
"setting_languages_apply": "Apply", "setting_languages_apply": "Apply",
"setting_languages_title": "Languages", "setting_languages_title": "Languages",
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}", "setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
@@ -406,12 +407,15 @@
"setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)",
"setting_notifications_total_progress_title": "Show background backup total progress", "setting_notifications_total_progress_title": "Show background backup total progress",
"setting_pages_app_bar_settings": "Settings", "setting_pages_app_bar_settings": "Settings",
"setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.",
"setting_video_viewer_looping_title": "Looping",
"setting_video_viewer_title": "Videos",
"settings_require_restart": "Please restart Immich to apply this setting", "settings_require_restart": "Please restart Immich to apply this setting",
"share_add": "Add", "share_add": "Add",
"share_add_photos": "Add photos", "share_add_photos": "Add photos",
"share_add_title": "Add a title", "share_add_title": "Add a title",
"share_create_album": "Create album",
"share_assets_selected": "{} selected", "share_assets_selected": "{} selected",
"share_create_album": "Create album",
"shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_disable": "Comment is disabled",
"shared_album_activities_input_hint": "Say something", "shared_album_activities_input_hint": "Say something",
"shared_album_activity_remove_content": "Do you want to delete this activity?", "shared_album_activity_remove_content": "Do you want to delete this activity?",
+33 -32
View File
@@ -7,7 +7,7 @@
"add_to_album_bottom_sheet_added": "Agregado a {album}", "add_to_album_bottom_sheet_added": "Agregado a {album}",
"add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}", "add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
"advanced_settings_log_level_title": "Nivel de registro: {}", "advanced_settings_log_level_title": "Nivel de registro: {}",
"advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.", "advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
"advanced_settings_prefer_remote_title": "Preferir imágenes remotas", "advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
"advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados", "advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
"advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados", "advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados",
@@ -26,7 +26,7 @@
"album_viewer_appbar_share_delete": "Eliminar álbum ", "album_viewer_appbar_share_delete": "Eliminar álbum ",
"album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum", "album_viewer_appbar_share_err_delete": "No ha podido eliminar el álbum",
"album_viewer_appbar_share_err_leave": "No se ha podido abandonar el álbum", "album_viewer_appbar_share_err_leave": "No se ha podido abandonar el álbum",
"album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los archivos del álbum", "album_viewer_appbar_share_err_remove": "Hay problemas para eliminar los elementos del álbum",
"album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ", "album_viewer_appbar_share_err_title": "Error al cambiar el título del álbum ",
"album_viewer_appbar_share_leave": "Abandonar álbum ", "album_viewer_appbar_share_leave": "Abandonar álbum ",
"album_viewer_appbar_share_remove": "Eliminar del álbum ", "album_viewer_appbar_share_remove": "Eliminar del álbum ",
@@ -37,14 +37,14 @@
"app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?", "app_bar_signout_dialog_content": "¿Estás seguro que quieres cerrar sesión?",
"app_bar_signout_dialog_ok": "Sí", "app_bar_signout_dialog_ok": "Sí",
"app_bar_signout_dialog_title": "Cerrar sesión", "app_bar_signout_dialog_title": "Cerrar sesión",
"archive_page_no_archived_assets": "No se encontraron recursos archivados", "archive_page_no_archived_assets": "No se encontraron elementos archivados",
"archive_page_title": "Archivo ({})", "archive_page_title": "Archivo ({})",
"asset_action_delete_err_read_only": "No se pueden borrar el archivo(s) de solo lectura, omitiendo", "asset_action_delete_err_read_only": "No se pueden borrar el archivo(s) de solo lectura, omitiendo",
"asset_action_share_err_offline": "No se pudo obtener el archivo(s) sin conexión, omitiendo", "asset_action_share_err_offline": "No se pudo obtener el archivo(s) sin conexión, omitiendo",
"asset_list_group_by_sub_title": "Agrupar por", "asset_list_group_by_sub_title": "Agrupar por",
"asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico", "asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico",
"asset_list_layout_settings_group_automatically": "Automatico", "asset_list_layout_settings_group_automatically": "Automatico",
"asset_list_layout_settings_group_by": "Agrupar recursos por", "asset_list_layout_settings_group_by": "Agrupar elementos por",
"asset_list_layout_settings_group_by_month": "Mes", "asset_list_layout_settings_group_by_month": "Mes",
"asset_list_layout_settings_group_by_month_day": "Mes + día", "asset_list_layout_settings_group_by_month_day": "Mes + día",
"asset_list_layout_sub_title": "Disposición", "asset_list_layout_sub_title": "Disposición",
@@ -53,17 +53,17 @@
"asset_viewer_settings_title": "Visor de Archivos", "asset_viewer_settings_title": "Visor de Archivos",
"backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir",
"backup_album_selection_page_assets_scatter": "Los archivos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.", "backup_album_selection_page_assets_scatter": "Los elementos pueden dispersarse en varios álbumes. De este modo, los álbumes pueden ser incluidos o excluidos durante el proceso de copia de seguridad.",
"backup_album_selection_page_select_albums": "Seleccionar Álbumes", "backup_album_selection_page_select_albums": "Seleccionar Álbumes",
"backup_album_selection_page_selection_info": "Información sobre la Selección", "backup_album_selection_page_selection_info": "Información sobre la Selección",
"backup_album_selection_page_total_assets": "Total de archivos únicos", "backup_album_selection_page_total_assets": "Total de elementos únicos",
"backup_all": "Todos", "backup_all": "Todos",
"backup_background_service_backup_failed_message": "Error al copiar archivos. Reintentando...", "backup_background_service_backup_failed_message": "Error al copiar elementos. Reintentando...",
"backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando...", "backup_background_service_connection_failed_message": "Error al conectar con el servidor. Reintentando...",
"backup_background_service_current_upload_notification": "Cargando {}", "backup_background_service_current_upload_notification": "Cargando {}",
"backup_background_service_default_notification": "Verificando si hay nuevos archivos", "backup_background_service_default_notification": "Verificando si hay nuevos elementos",
"backup_background_service_error_title": "Error de copia de seguridad", "backup_background_service_error_title": "Error de copia de seguridad",
"backup_background_service_in_progress_notification": "Creando copia de seguridad de tus archivos...", "backup_background_service_in_progress_notification": "Creando copia de seguridad de tus elementos...",
"backup_background_service_upload_failure_notification": "Error al cargar {}", "backup_background_service_upload_failure_notification": "Error al cargar {}",
"backup_controller_page_albums": "Álbumes de copia de seguridad", "backup_controller_page_albums": "Álbumes de copia de seguridad",
"backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.", "backup_controller_page_background_app_refresh_disabled_content": "Activa la actualización en segundo plano de la aplicación en Configuración > General > Actualización en segundo plano para usar la copia de seguridad en segundo plano.",
@@ -76,7 +76,7 @@
"backup_controller_page_background_charging": "Solo mientras se carga", "backup_controller_page_background_charging": "Solo mientras se carga",
"backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano", "backup_controller_page_background_configure_error": "Error al configurar el servicio en segundo plano",
"backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}", "backup_controller_page_background_delay": "Retraso en la copia de seguridad de nuevos elementos: {}",
"backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos archivos sin necesidad de abrir la aplicación.", "backup_controller_page_background_description": "Activa el servicio en segundo plano para copiar automáticamente cualquier nuevos elementos sin necesidad de abrir la aplicación.",
"backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada", "backup_controller_page_background_is_off": "La copia de seguridad en segundo plano automática está desactivada",
"backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada", "backup_controller_page_background_is_on": "La copia de seguridad en segundo plano automática está activada",
"backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano", "backup_controller_page_background_turn_off": "Desactivar el servicio en segundo plano",
@@ -109,28 +109,28 @@
"backup_controller_page_turn_on": "Activar la copia de seguridad", "backup_controller_page_turn_on": "Activar la copia de seguridad",
"backup_controller_page_uploading_file_info": "Cargando información del archivo", "backup_controller_page_uploading_file_info": "Cargando información del archivo",
"backup_err_only_album": "No se puede eliminar el único álbum", "backup_err_only_album": "No se puede eliminar el único álbum",
"backup_info_card_assets": "archivos", "backup_info_card_assets": "elementos",
"backup_manual_cancelled": "Cancelado", "backup_manual_cancelled": "Cancelado",
"backup_manual_failed": "Fallido", "backup_manual_failed": "Fallido",
"backup_manual_in_progress": "Subida en progreso. Espere", "backup_manual_in_progress": "Subida en progreso. Espere",
"backup_manual_success": "Éxito", "backup_manual_success": "Éxito",
"backup_manual_title": "Estado de la subida", "backup_manual_title": "Estado de la subida",
"backup_options_page_title": "Opciones de Copia de Seguridad", "backup_options_page_title": "Opciones de Copia de Seguridad",
"cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} archivos)", "cache_settings_album_thumbnails": "Miniaturas de la página de la biblioteca ({} elementos)",
"cache_settings_clear_cache_button": "Borrar caché", "cache_settings_clear_cache_button": "Borrar caché",
"cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.", "cache_settings_clear_cache_button_title": "Borra la caché de la aplicación. Esto afectará significativamente el rendimiento de la aplicación hasta que se reconstruya la caché.",
"cache_settings_duplicated_assets_clear_button": "LIMPIAR", "cache_settings_duplicated_assets_clear_button": "LIMPIAR",
"cache_settings_duplicated_assets_subtitle": "Fotos y vídeos en la lista negra de la app", "cache_settings_duplicated_assets_subtitle": "Fotos y vídeos en la lista negra de la app",
"cache_settings_duplicated_assets_title": "Archivos duplicados ({})", "cache_settings_duplicated_assets_title": "Elementos duplicados ({})",
"cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} archivos)", "cache_settings_image_cache_size": "Tamaño de la caché de imágenes ({} elementos)",
"cache_settings_statistics_album": "Miniaturas de la biblioteca", "cache_settings_statistics_album": "Miniaturas de la biblioteca",
"cache_settings_statistics_assets": "{} archivos ({})", "cache_settings_statistics_assets": "{} elementos ({})",
"cache_settings_statistics_full": "Imágenes completas", "cache_settings_statistics_full": "Imágenes completas",
"cache_settings_statistics_shared": "Miniaturas de álbumes compartidos", "cache_settings_statistics_shared": "Miniaturas de álbumes compartidos",
"cache_settings_statistics_thumbnail": "Miniaturas", "cache_settings_statistics_thumbnail": "Miniaturas",
"cache_settings_statistics_title": "Uso de caché", "cache_settings_statistics_title": "Uso de caché",
"cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich", "cache_settings_subtitle": "Controla el comportamiento del almacenamiento en caché de la aplicación móvil Immich",
"cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} archivos)", "cache_settings_thumbnail_size": "Tamaño de la caché de miniaturas ({} elementos)",
"cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local", "cache_settings_tile_subtitle": "Controla el comportamiento del almacenamiento local",
"cache_settings_tile_title": "Almacenamiento local", "cache_settings_tile_title": "Almacenamiento local",
"cache_settings_title": "Configuración de la caché", "cache_settings_title": "Configuración de la caché",
@@ -165,7 +165,7 @@
"create_album_page_untitled": "Sin título", "create_album_page_untitled": "Sin título",
"create_shared_album_page_create": "Crear", "create_shared_album_page_create": "Crear",
"create_shared_album_page_share": "Compartir", "create_shared_album_page_share": "Compartir",
"create_shared_album_page_share_add_assets": "AGREGAR ARCHIVOS", "create_shared_album_page_share_add_assets": "AGREGAR ELEMENTOS",
"create_shared_album_page_share_select_photos": "Seleccionar Fotos", "create_shared_album_page_share_select_photos": "Seleccionar Fotos",
"curated_location_page_title": "Lugares", "curated_location_page_title": "Lugares",
"curated_object_page_title": "Objetos", "curated_object_page_title": "Objetos",
@@ -199,26 +199,26 @@
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental", "experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad", "experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
"experimental_settings_title": "Experimental", "experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No se encontraron recursos marcados como favoritos", "favorites_page_no_favorites": "No se encontraron elementos marcados como favoritos",
"favorites_page_title": "Favoritos", "favorites_page_title": "Favoritos",
"haptic_feedback_switch": "Activar respuesta háptica", "haptic_feedback_switch": "Activar respuesta háptica",
"haptic_feedback_title": "Respuesta Háptica", "haptic_feedback_title": "Respuesta Háptica",
"home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.", "home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.",
"home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo", "home_page_add_to_album_err_local": "Aún no se pueden agregar elementos locales a álbumes, omitiendo",
"home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ", "home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ",
"home_page_album_err_partner": "Aún no se pueden agregar activos a un album de un compañero, omitiendo", "home_page_album_err_partner": "Aún no se pueden agregar elementos a un álbum de un compañero, omitiendo",
"home_page_archive_err_local": "Los recursos locales no pueden ser archivados, omitiendo", "home_page_archive_err_local": "Los elementos locales no pueden ser archivados, omitiendo",
"home_page_archive_err_partner": "No se pueden archivar activos de un compañero, omitiendo", "home_page_archive_err_partner": "No se pueden archivar elementos de un compañero, omitiendo",
"home_page_building_timeline": "Construyendo la línea de tiempo", "home_page_building_timeline": "Construyendo la línea de tiempo",
"home_page_delete_err_partner": "No se pueden eliminar activos de un compañero, omitiendo", "home_page_delete_err_partner": "No se pueden eliminar elementos de un compañero, omitiendo",
"home_page_delete_remote_err_local": "Archivos locales en eliminación de selección remota, omitiendo", "home_page_delete_remote_err_local": "Elementos locales en la selección de eliminación remota, omitiendo",
"home_page_favorite_err_local": "Aún no se pueden archivar recursos locales, omitiendo", "home_page_favorite_err_local": "Aún no se pueden archivar elementos locales, omitiendo",
"home_page_favorite_err_partner": "Aún no se pueden marcar recursos de compañeros como favoritos, omitiendo", "home_page_favorite_err_partner": "Aún no se pueden marcar elementos de compañeros como favoritos, omitiendo",
"home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.", "home_page_first_time_notice": "Si esta es la primera vez que usas la app, por favor, asegúrate de elegir un álbum de respaldo para que la línea de tiempo pueda cargar fotos y videos en los álbumes.",
"home_page_share_err_local": "No se pueden compartir activos locales a través de un enlace, omitiendo", "home_page_share_err_local": "No se pueden compartir elementos locales a través de un enlace, omitiendo",
"home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo", "home_page_upload_err_limit": "Solo se pueden subir 30 elementos simultáneamente, omitiendo",
"image_viewer_page_state_provider_download_error": "Error de descarga", "image_viewer_page_state_provider_download_error": "Error de descarga",
"image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_started": "Descarga Iniciada",
"image_viewer_page_state_provider_download_success": "Descarga exitosa", "image_viewer_page_state_provider_download_success": "Descarga exitosa",
"image_viewer_page_state_provider_share_error": "Error al compartir", "image_viewer_page_state_provider_share_error": "Error al compartir",
"library_page_albums": "Álbumes", "library_page_albums": "Álbumes",
@@ -227,7 +227,7 @@
"library_page_favorites": "Favoritos", "library_page_favorites": "Favoritos",
"library_page_new_album": "Nuevo álbum", "library_page_new_album": "Nuevo álbum",
"library_page_sharing": "Compartiendo", "library_page_sharing": "Compartiendo",
"library_page_sort_asset_count": "Número de archivos", "library_page_sort_asset_count": "Número de elementos",
"library_page_sort_created": "Creado más recientemente", "library_page_sort_created": "Creado más recientemente",
"library_page_sort_last_modified": "Última modificación", "library_page_sort_last_modified": "Última modificación",
"library_page_sort_most_oldest_photo": "Foto más antigua", "library_page_sort_most_oldest_photo": "Foto más antigua",
@@ -299,7 +299,7 @@
"motion_photos_page_title": "Foto en Movimiento", "motion_photos_page_title": "Foto en Movimiento",
"multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo",
"multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.", "multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.",
"no_assets_to_show": "No assets to show", "no_assets_to_show": "No hay elementos a mostrar",
"notification_permission_dialog_cancel": "Cancelar", "notification_permission_dialog_cancel": "Cancelar",
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.", "notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
"notification_permission_dialog_settings": "Ajustes", "notification_permission_dialog_settings": "Ajustes",
@@ -403,13 +403,14 @@
"setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano", "setting_notifications_single_progress_title": "Mostrar progreso detallado de copia de seguridad en segundo plano",
"setting_notifications_subtitle": "Ajusta tus preferencias de notificación", "setting_notifications_subtitle": "Ajusta tus preferencias de notificación",
"setting_notifications_title": "Notificaciones", "setting_notifications_title": "Notificaciones",
"setting_notifications_total_progress_subtitle": "Progreso general de subida (archivos completados/total)", "setting_notifications_total_progress_subtitle": "Progreso general de subida (elementos completados/total)",
"setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano", "setting_notifications_total_progress_title": "Mostrar progreso total de copia de seguridad en segundo plano",
"setting_pages_app_bar_settings": "Ajustes", "setting_pages_app_bar_settings": "Ajustes",
"settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste",
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
"share_assets_selected": "{} selected",
"share_create_album": "Crear álbum", "share_create_album": "Crear álbum",
"shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
"shared_album_activities_input_hint": "Comenta algo", "shared_album_activities_input_hint": "Comenta algo",
@@ -462,7 +463,7 @@
"shared_link_expires_never": "Caduca ∞", "shared_link_expires_never": "Caduca ∞",
"shared_link_expires_second": "Caduca en {} segundo", "shared_link_expires_second": "Caduca en {} segundo",
"shared_link_expires_seconds": "Caduca en {} segundos", "shared_link_expires_seconds": "Caduca en {} segundos",
"shared_link_individual_shared": "Individual shared", "shared_link_individual_shared": "Compartido individualmente",
"shared_link_info_chip_download": "Descargar", "shared_link_info_chip_download": "Descargar",
"shared_link_info_chip_metadata": "EXIF", "shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Subir", "shared_link_info_chip_upload": "Subir",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
"share_assets_selected": "{} selected",
"share_create_album": "Crear álbum", "share_create_album": "Crear álbum",
"shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
"shared_album_activities_input_hint": "Say something", "shared_album_activities_input_hint": "Say something",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
"share_assets_selected": "{} selected",
"share_create_album": "Crear álbum", "share_create_album": "Crear álbum",
"shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
"shared_album_activities_input_hint": "Comenta algo", "shared_album_activities_input_hint": "Comenta algo",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Agregar", "share_add": "Agregar",
"share_add_photos": "Agregar fotos", "share_add_photos": "Agregar fotos",
"share_add_title": "Agregar un título", "share_add_title": "Agregar un título",
"share_assets_selected": "{} selected",
"share_create_album": "Crear álbum", "share_create_album": "Crear álbum",
"shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activities_input_disable": "Los comentarios están deshabilitados",
"shared_album_activities_input_hint": "Di algo", "shared_album_activities_input_hint": "Di algo",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Lisää", "share_add": "Lisää",
"share_add_photos": "Lisää kuvia", "share_add_photos": "Lisää kuvia",
"share_add_title": "Lisää nimi", "share_add_title": "Lisää nimi",
"share_assets_selected": "{} selected",
"share_create_album": "Luo albumi", "share_create_album": "Luo albumi",
"shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä", "shared_album_activities_input_disable": "Kommentointi on kytketty pois päältä",
"shared_album_activities_input_hint": "Sano jotain", "shared_album_activities_input_hint": "Sano jotain",
+1
View File
@@ -410,6 +410,7 @@
"share_add": "Ajouter", "share_add": "Ajouter",
"share_add_photos": "Ajouter des photos", "share_add_photos": "Ajouter des photos",
"share_add_title": "Ajouter un titre", "share_add_title": "Ajouter un titre",
"share_assets_selected": "{} selected",
"share_create_album": "Créer un album", "share_create_album": "Créer un album",
"shared_album_activities_input_disable": "Les commentaires sont désactivés", "shared_album_activities_input_disable": "Les commentaires sont désactivés",
"shared_album_activities_input_hint": "Dire quelque chose", "shared_album_activities_input_hint": "Dire quelque chose",

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