Merge branch 'main' of github.com:immich-app/immich into web/automation-ui
|
@ -6,6 +6,14 @@ body:
|
|||
attributes:
|
||||
value: |
|
||||
Please use this form to request new feature for Immich
|
||||
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: I have searched the existing feature requests to make sure this is not a duplicate request.
|
||||
options:
|
||||
- label: "Yes"
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: feature
|
||||
attributes:
|
||||
|
|
10
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
|
@ -87,6 +87,16 @@ body:
|
|||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: logs
|
||||
attributes:
|
||||
label: Relevant log output
|
||||
description: Please copy and paste any relevant logs below. (code formatting is
|
||||
enabled, no need for backticks)
|
||||
render: shell
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Additional information
|
||||
|
|
2
.github/workflows/cli.yml
vendored
|
@ -58,7 +58,7 @@ jobs:
|
|||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
uses: docker/setup-buildx-action@v3.3.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
|
|
4
.github/workflows/docker-cleanup.yml
vendored
|
@ -35,7 +35,7 @@ jobs:
|
|||
steps:
|
||||
- name: Clean temporary images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.5.0
|
||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
@ -64,7 +64,7 @@ jobs:
|
|||
steps:
|
||||
- name: Clean untagged images
|
||||
if: "${{ env.TOKEN != '' }}"
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.5.0
|
||||
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
|
||||
with:
|
||||
token: "${{ env.TOKEN }}"
|
||||
owner: "immich-app"
|
||||
|
|
2
.github/workflows/docker.yml
vendored
|
@ -66,7 +66,7 @@ jobs:
|
|||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.2.0
|
||||
uses: docker/setup-buildx-action@v3.3.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
|
|
13
.github/workflows/test.yml
vendored
|
@ -10,19 +10,6 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
server-e2e-jobs:
|
||||
name: Server (e2e-jobs)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make server-e2e-jobs
|
||||
|
||||
doc-tests:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
|
5
CODEOWNERS
Normal file
|
@ -0,0 +1,5 @@
|
|||
/.github/ @bo0tzz
|
||||
/docker/ @bo0tzz
|
||||
/server/ @danieldietzler
|
||||
/machine-learning/ @mertalev
|
||||
/e2e/ @danieldietzler
|
3
Makefile
|
@ -16,9 +16,6 @@ stage:
|
|||
pull-stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||
|
||||
server-e2e-jobs:
|
||||
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
|
|
@ -21,6 +21,7 @@ module.exports = {
|
|||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/no-process-exit': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
},
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM node:20-alpine3.19@sha256:ef3f47741e161900ddd07addcaca7e76534a9205e4cd73b2ed091ba339004a75 as core
|
||||
FROM node:20-alpine3.19@sha256:7e227295e96f5b00aa79555ae166f50610940d888fc2e321cf36304cbd17d7d6 as core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
|
253
cli/package-lock.json
generated
|
@ -30,7 +30,7 @@
|
|||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
|
@ -54,8 +54,8 @@
|
|||
"@oazapfts/runtime": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.0",
|
||||
"typescript": "^5.3.3"
|
||||
"@types/node": "^20.12.7",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
},
|
||||
"node_modules/@aashutoshrathi/word-wrap": {
|
||||
|
@ -1193,12 +1193,6 @@
|
|||
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/istanbul-lib-coverage": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz",
|
||||
"integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==",
|
||||
"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",
|
||||
|
@ -1230,9 +1224,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "20.11.30",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.30.tgz",
|
||||
"integrity": "sha512-dHM6ZxwlmuZaRmUPfv1p+KrdD1Dci04FbdEm/9wEMouFqxYoFl5aMkt0VMAUtYRQDyYvD41WJLukhq/ha3YuTw==",
|
||||
"version": "20.12.7",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz",
|
||||
"integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
|
@ -1251,22 +1245,22 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.4.0.tgz",
|
||||
"integrity": "sha512-yHMQ/oFaM7HZdVrVm/M2WHaNPgyuJH4WelkSVEWSSsir34kxW2kDJCxlXRhhGWEsMN0WAW/vLpKfKVcm8k+MPw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.6.0.tgz",
|
||||
"integrity": "sha512-gKmTNwZnblUdnTIJu3e9kmeRRzV2j1a/LUO27KNNAnIC5zjy1aSvXSRp4rVNlmAoHlQ7HzX42NbKpcSr4jF80A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.5.1",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/type-utils": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/type-utils": "7.6.0",
|
||||
"@typescript-eslint/utils": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"graphemer": "^1.4.0",
|
||||
"ignore": "^5.2.4",
|
||||
"ignore": "^5.3.1",
|
||||
"natural-compare": "^1.4.0",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1286,15 +1280,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.4.0.tgz",
|
||||
"integrity": "sha512-ZvKHxHLusweEUVwrGRXXUVzFgnWhigo4JurEj0dGF1tbcGh6buL+ejDdjxOQxv6ytcY1uhun1p2sm8iWStlgLQ==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.6.0.tgz",
|
||||
"integrity": "sha512-usPMPHcwX3ZoPWnBnhhorc14NJw9J4HpSXQX4urF2TPKG0au0XhJoZyX62fmvdHONUkmyUe74Hzm1//XA+BoYg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -1314,13 +1308,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.4.0.tgz",
|
||||
"integrity": "sha512-68VqENG5HK27ypafqLVs8qO+RkNc7TezCduYrx8YJpXq2QGZ30vmNZGJJJC48+MVn4G2dCV8m5ZTVnzRexTVtw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.6.0.tgz",
|
||||
"integrity": "sha512-ngttyfExA5PsHSx0rdFgnADMYQi+Zkeiv4/ZxGYUWd0nLs63Ha0ksmp8VMxAIC0wtCFxMos7Lt3PszJssG/E6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0"
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1331,15 +1325,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.4.0.tgz",
|
||||
"integrity": "sha512-247ETeHgr9WTRMqHbbQdzwzhuyaJ8dPTuyuUEMANqzMRB1rj/9qFIuIXK7l0FX9i9FXbHeBQl/4uz6mYuCE7Aw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.6.0.tgz",
|
||||
"integrity": "sha512-NxAfqAPNLG6LTmy7uZgpK8KcuiS2NZD/HlThPXQRGwz6u7MDBWRVliEEl1Gj6U7++kVJTpehkhZzCJLMK66Scw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"@typescript-eslint/utils": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"@typescript-eslint/utils": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1358,9 +1352,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.4.0.tgz",
|
||||
"integrity": "sha512-mjQopsbffzJskos5B4HmbsadSJQWaRK0UxqQ7GuNA9Ga4bEKeiO6b2DnB6cM6bpc8lemaPseh0H9B/wyg+J7rw==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.6.0.tgz",
|
||||
"integrity": "sha512-h02rYQn8J+MureCvHVVzhl69/GAfQGPQZmOMjG1KfCl7o3HtMSlPaPUAPu6lLctXI5ySRGIYk94clD/AUMCUgQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1371,19 +1365,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.4.0.tgz",
|
||||
"integrity": "sha512-A99j5AYoME/UBQ1ucEbbMEmGkN7SE0BvZFreSnTd1luq7yulcHdyGamZKizU7canpGDWGJ+Q6ZA9SyQobipePg==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.6.0.tgz",
|
||||
"integrity": "sha512-+7Y/GP9VuYibecrCQWSKgl3GvUM5cILRttpWtnAu8GNL9j11e4tbuGZmZjJ8ejnKYyBRb2ddGQ3rEFCq3QjMJw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/visitor-keys": "7.4.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/visitor-keys": "7.6.0",
|
||||
"debug": "^4.3.4",
|
||||
"globby": "^11.1.0",
|
||||
"is-glob": "^4.0.3",
|
||||
"minimatch": "9.0.3",
|
||||
"semver": "^7.5.4",
|
||||
"ts-api-utils": "^1.0.1"
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"ts-api-utils": "^1.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1399,18 +1393,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.4.0.tgz",
|
||||
"integrity": "sha512-NQt9QLM4Tt8qrlBVY9lkMYzfYtNz8/6qwZg8pI3cMGlPnj6mOpRxxAm7BMJN9K0AiY+1BwJ5lVC650YJqYOuNg==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.6.0.tgz",
|
||||
"integrity": "sha512-x54gaSsRRI+Nwz59TXpCsr6harB98qjXYzsRxGqvA5Ue3kQH+FxS7FYU81g/omn22ML2pZJkisy6Q+ElK8pBCA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@types/json-schema": "^7.0.12",
|
||||
"@types/semver": "^7.5.0",
|
||||
"@typescript-eslint/scope-manager": "7.4.0",
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"@typescript-eslint/typescript-estree": "7.4.0",
|
||||
"semver": "^7.5.4"
|
||||
"@types/json-schema": "^7.0.15",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@typescript-eslint/scope-manager": "7.6.0",
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"@typescript-eslint/typescript-estree": "7.6.0",
|
||||
"semver": "^7.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1424,13 +1418,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "7.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.4.0.tgz",
|
||||
"integrity": "sha512-0zkC7YM0iX5Y41homUUeW1CHtZR01K3ybjM1l6QczoMuay0XKtrb93kv95AxUGwdjGr64nNqnOCwmEl616N8CA==",
|
||||
"version": "7.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.6.0.tgz",
|
||||
"integrity": "sha512-4eLB7t+LlNUmXzfOu1VAIAdkjbu5xNSerURS9X/S5TUKWFRpXRQZbmtPqgKmYx8bj3J0irtQXSiWAOY82v+cgw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "7.4.0",
|
||||
"eslint-visitor-keys": "^3.4.1"
|
||||
"@typescript-eslint/types": "7.6.0",
|
||||
"eslint-visitor-keys": "^3.4.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || >=20.0.0"
|
||||
|
@ -1447,9 +1441,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/@vitest/coverage-v8": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.4.0.tgz",
|
||||
"integrity": "sha512-4hDGyH1SvKpgZnIByr9LhGgCEuF9DKM34IBLCC/fVfy24Z3+PZ+Ii9hsVBsHvY1umM1aGPEjceRkzxCfcQ10wg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.5.0.tgz",
|
||||
"integrity": "sha512-1igVwlcqw1QUMdfcMlzzY4coikSIBN944pkueGi0pawrX5I5Z+9hxdTR+w3Sg6Q3eZhvdMAs8ZaF9JuTG1uYOQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.1",
|
||||
|
@ -1464,24 +1458,23 @@
|
|||
"picocolors": "^1.0.0",
|
||||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"test-exclude": "^6.0.0",
|
||||
"v8-to-istanbul": "^9.2.0"
|
||||
"test-exclude": "^6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/vitest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vitest": "1.4.0"
|
||||
"vitest": "1.5.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@vitest/expect": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.4.0.tgz",
|
||||
"integrity": "sha512-Jths0sWCJZ8BxjKe+p+eKsoqev1/T8lYcrjavEaz8auEJ4jAVY0GwW3JKmdVU4mmNPLPHixh4GNXP7GFtAiDHA==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.5.0.tgz",
|
||||
"integrity": "sha512-0pzuCI6KYi2SIC3LQezmxujU9RK/vwC1U9R0rLuGlNGcOuDWxqWKu6nUdFsX9tH1WU0SXtAxToOsEjeUn1s3hA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/spy": "1.4.0",
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"chai": "^4.3.10"
|
||||
},
|
||||
"funding": {
|
||||
|
@ -1489,12 +1482,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/runner": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.4.0.tgz",
|
||||
"integrity": "sha512-EDYVSmesqlQ4RD2VvWo3hQgTJ7ZrFQ2VSJdfiJiArkCerDAGeyF1i6dHkmySqk573jLp6d/cfqCN+7wUB5tLgg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.5.0.tgz",
|
||||
"integrity": "sha512-7HWwdxXP5yDoe7DTpbif9l6ZmDwCzcSIK38kTSIt6CFEpMjX4EpCgT6wUmS0xTXqMI6E/ONmfgRKmaujpabjZQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"p-limit": "^5.0.0",
|
||||
"pathe": "^1.1.1"
|
||||
},
|
||||
|
@ -1530,9 +1523,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/snapshot": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.4.0.tgz",
|
||||
"integrity": "sha512-saAFnt5pPIA5qDGxOHxJ/XxhMFKkUSBJmVt5VgDsAqPTX6JP326r5C/c9UuCMPoXNzuudTPsYDZCoJ5ilpqG2A==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.5.0.tgz",
|
||||
"integrity": "sha512-qpv3fSEuNrhAO3FpH6YYRdaECnnRjg9VxbhdtPwPRnzSfHVXnNzzrpX4cJxqiwgRMo7uRMWDFBlsBq4Cr+rO3A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"magic-string": "^0.30.5",
|
||||
|
@ -1544,9 +1537,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/spy": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.4.0.tgz",
|
||||
"integrity": "sha512-Ywau/Qs1DzM/8Uc+yA77CwSegizMlcgTJuYGAi0jujOteJOUf1ujunHThYo243KG9nAyWT3L9ifPYZ5+As/+6Q==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.5.0.tgz",
|
||||
"integrity": "sha512-vu6vi6ew5N5MMHJjD5PoakMRKYdmIrNJmyfkhRpQt5d9Ewhw9nZ5Aqynbi3N61bvk9UvZ5UysMT6ayIrZ8GA9w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tinyspy": "^2.2.0"
|
||||
|
@ -1556,9 +1549,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@vitest/utils": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.4.0.tgz",
|
||||
"integrity": "sha512-mx3Yd1/6e2Vt/PUC98DcqTirtfxUyAZ32uK82r8rZzbtBeBo+nqgnjx/LvqQdWsrvNtm14VmurNgcf4nqY5gJg==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.5.0.tgz",
|
||||
"integrity": "sha512-BDU0GNL8MWkRkSRdNFvCUCAVOeHaUlVJ9Tx0TYBZyXaaOTmGtUFObzchCivIBrIwKzvZA7A9sCejVhXM2aY98A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"diff-sequences": "^29.6.3",
|
||||
|
@ -1909,12 +1902,6 @@
|
|||
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/convert-source-map": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
|
||||
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.36.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.36.0.tgz",
|
||||
|
@ -2194,9 +2181,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn": {
|
||||
"version": "51.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-51.0.1.tgz",
|
||||
"integrity": "sha512-MuR/+9VuB0fydoI0nIn2RDA5WISRn4AsJyNSaNKLVwie9/ONvQhxOBbkfSICBPnzKrB77Fh6CZZXjgTt/4Latw==",
|
||||
"version": "52.0.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-52.0.0.tgz",
|
||||
"integrity": "sha512-1Yzm7/m+0R4djH0tjDjfVei/ju2w3AzUGjG6q8JnuNIL5xIwsflyCooW5sfBvQp2pMYQFSWWCFONsjCax1EHng==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
|
@ -3139,9 +3126,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.3",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz",
|
||||
"integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==",
|
||||
"version": "9.0.4",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
|
||||
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
|
@ -4258,9 +4245,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/tinypool": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.2.tgz",
|
||||
"integrity": "sha512-SUszKYe5wgsxnNOVlBYO6IC+8VGWdVGZWAqUxp3UErNBtptZvWbwyUOyzNL59zigz2rCA92QiL3wvG+JDSdJdQ==",
|
||||
"version": "0.8.4",
|
||||
"resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz",
|
||||
"integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
|
@ -4368,9 +4355,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
@ -4431,20 +4418,6 @@
|
|||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/v8-to-istanbul": {
|
||||
"version": "9.2.0",
|
||||
"resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.2.0.tgz",
|
||||
"integrity": "sha512-/EH/sDgxU2eGxajKdwLCDmQ4FWq+kpi3uCmBGpw1xJtnAxEjlD8j8PEiGWpCIMIs3ciNAgH0d3TTJiUkYzyZjA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/trace-mapping": "^0.3.12",
|
||||
"@types/istanbul-lib-coverage": "^2.0.1",
|
||||
"convert-source-map": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
|
@ -4456,9 +4429,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "5.2.7",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.7.tgz",
|
||||
"integrity": "sha512-k14PWOKLI6pMaSzAuGtT+Cf0YmIx12z9YGon39onaJNy8DLBfBJrzg9FQEmkAM5lpHBZs9wksWAsyF/HkpEwJA==",
|
||||
"version": "5.2.8",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.8.tgz",
|
||||
"integrity": "sha512-OyZR+c1CE8yeHw5V5t59aXsUPPVTHMDjEZz8MgguLL/Q7NblxhZUlTu9xSPqlsUO/y+X7dlU05jdhvyycD55DA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.20.1",
|
||||
|
@ -4511,9 +4484,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vite-node": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.4.0.tgz",
|
||||
"integrity": "sha512-VZDAseqjrHgNd4Kh8icYHWzTKSCZMhia7GyHfhtzLW33fZlG9SwsB6CEhgyVOWkJfJ2pFLrp/Gj1FSfAiqH9Lw==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.5.0.tgz",
|
||||
"integrity": "sha512-tV8h6gMj6vPzVCa7l+VGq9lwoJjW8Y79vst8QZZGiuRAfijU+EEWuc0kFpmndQrWhMMhet1jdSF+40KSZUqIIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"cac": "^6.7.14",
|
||||
|
@ -4552,16 +4525,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/vitest": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.4.0.tgz",
|
||||
"integrity": "sha512-gujzn0g7fmwf83/WzrDTnncZt2UiXP41mHuFYFrdwaLRVQ6JYQEiME2IfEjU3vcFL3VKa75XhI3lFgn+hfVsQw==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-1.5.0.tgz",
|
||||
"integrity": "sha512-d8UKgR0m2kjdxDWX6911uwxout6GHS0XaGH1cksSIVVG8kRlE7G7aBw7myKQCvDI5dT4j7ZMa+l706BIORMDLw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@vitest/expect": "1.4.0",
|
||||
"@vitest/runner": "1.4.0",
|
||||
"@vitest/snapshot": "1.4.0",
|
||||
"@vitest/spy": "1.4.0",
|
||||
"@vitest/utils": "1.4.0",
|
||||
"@vitest/expect": "1.5.0",
|
||||
"@vitest/runner": "1.5.0",
|
||||
"@vitest/snapshot": "1.5.0",
|
||||
"@vitest/spy": "1.5.0",
|
||||
"@vitest/utils": "1.5.0",
|
||||
"acorn-walk": "^8.3.2",
|
||||
"chai": "^4.3.10",
|
||||
"debug": "^4.3.4",
|
||||
|
@ -4573,9 +4546,9 @@
|
|||
"std-env": "^3.5.0",
|
||||
"strip-literal": "^2.0.0",
|
||||
"tinybench": "^2.5.1",
|
||||
"tinypool": "^0.8.2",
|
||||
"tinypool": "^0.8.3",
|
||||
"vite": "^5.0.0",
|
||||
"vite-node": "1.4.0",
|
||||
"vite-node": "1.5.0",
|
||||
"why-is-node-running": "^2.2.2"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -4590,8 +4563,8 @@
|
|||
"peerDependencies": {
|
||||
"@edge-runtime/vm": "*",
|
||||
"@types/node": "^18.0.0 || >=20.0.0",
|
||||
"@vitest/browser": "1.4.0",
|
||||
"@vitest/ui": "1.4.0",
|
||||
"@vitest/browser": "1.5.0",
|
||||
"@vitest/ui": "1.5.0",
|
||||
"happy-dom": "*",
|
||||
"jsdom": "*"
|
||||
},
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.0",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"glob": "^10.3.1",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.2.5",
|
||||
|
|
|
@ -56,14 +56,13 @@ class UploadFile extends File {
|
|||
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
||||
await authenticate(baseOptions);
|
||||
|
||||
const files = await scan(paths, options);
|
||||
if (files.length === 0) {
|
||||
const scanFiles = await scan(paths, options);
|
||||
if (scanFiles.length === 0) {
|
||||
console.log('No files found, exiting');
|
||||
return;
|
||||
}
|
||||
|
||||
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
||||
|
||||
const { newFiles, duplicates } = await checkForDuplicates(scanFiles, options);
|
||||
const newAssets = await uploadFiles(newFiles, options);
|
||||
await updateAlbums([...newAssets, ...duplicates], options);
|
||||
await deleteFiles(newFiles, options);
|
||||
|
@ -84,7 +83,12 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
|||
return files;
|
||||
};
|
||||
|
||||
const checkForDuplicates = async (files: string[], { concurrency }: UploadOptionsDto) => {
|
||||
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
|
||||
if (skipHash) {
|
||||
console.log('Skipping hash check, assuming all files are new');
|
||||
return { newFiles: files, duplicates: [] };
|
||||
}
|
||||
|
||||
const progressBar = new SingleBar(
|
||||
{ format: 'Checking files | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||
Presets.shades_classic,
|
||||
|
@ -147,17 +151,32 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
|
|||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
||||
let totalSizeUploaded = 0;
|
||||
let duplicateCount = 0;
|
||||
let duplicateSize = 0;
|
||||
let successCount = 0;
|
||||
let successSize = 0;
|
||||
|
||||
const newAssets: Asset[] = [];
|
||||
|
||||
try {
|
||||
for (const items of chunk(files, concurrency)) {
|
||||
await Promise.all(
|
||||
items.map(async (filepath) => {
|
||||
const stats = statsMap.get(filepath) as Stats;
|
||||
const response = await uploadFile(filepath, stats);
|
||||
totalSizeUploaded += stats.size ?? 0;
|
||||
uploadProgress.update(totalSizeUploaded, { value_formatted: byteSize(totalSizeUploaded) });
|
||||
|
||||
newAssets.push({ id: response.id, filepath });
|
||||
|
||||
if (response.duplicate) {
|
||||
duplicateCount++;
|
||||
duplicateSize += stats.size ?? 0;
|
||||
} else {
|
||||
successCount++;
|
||||
successSize += stats.size ?? 0;
|
||||
}
|
||||
|
||||
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||
|
||||
return response;
|
||||
}),
|
||||
);
|
||||
|
@ -166,7 +185,10 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
|
|||
uploadProgress.stop();
|
||||
}
|
||||
|
||||
console.log(`Successfully uploaded ${newAssets.length} asset${s(newAssets.length)} (${byteSize(totalSizeUploaded)})`);
|
||||
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
|
||||
if (duplicateCount > 0) {
|
||||
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
|
||||
}
|
||||
return newAssets;
|
||||
};
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ services:
|
|||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:dec2018ae55885fed717f25c289b8c9cff0bf5fbb9e619fb49b6161ac493c016
|
||||
image: prom/prometheus@sha256:4f6c47e39a9064028766e8c95890ed15690c30f00c4ba14e7ce6ae1ded0295b1
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
|
@ -88,7 +88,7 @@ services:
|
|||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:10.4.1-ubuntu@sha256:65e0e7d0f0b001cb0478bce5093bff917677dc308dd27a0aa4b3ac38e4fd877c
|
||||
image: grafana/grafana:10.4.2-ubuntu@sha256:4f55071b556fb03f12b41423c98a185ed6695ed9ff2558e35805f0dd765fd958
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
|
|
@ -69,9 +69,8 @@ services:
|
|||
POSTGRES_USER: ${DB_USERNAME}
|
||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||
volumes:
|
||||
- pgdata:/var/lib/postgresql/data
|
||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||
restart: always
|
||||
|
||||
volumes:
|
||||
pgdata:
|
||||
model-cache:
|
||||
|
|
|
@ -14,5 +14,6 @@ DB_PASSWORD=postgres
|
|||
DB_HOSTNAME=immich_postgres
|
||||
DB_USERNAME=postgres
|
||||
DB_DATABASE_NAME=immich
|
||||
DB_DATA_LOCATION=./postgres
|
||||
|
||||
REDIS_HOSTNAME=immich_redis
|
||||
|
|
|
@ -27,8 +27,6 @@ services:
|
|||
count: 1
|
||||
capabilities:
|
||||
- gpu
|
||||
- compute
|
||||
- video
|
||||
|
||||
openvino:
|
||||
device_cgroup_rules:
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
# Website
|
||||
|
||||
This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator.
|
||||
This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator.
|
||||
|
||||
### Installation
|
||||
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
|
||||
The admin password can be reset by running the [reset-admin-password](/docs/administration/server-commands.md) command on the immich-server.
|
||||
|
||||
### How can I see list of all users in Immich?
|
||||
### How can I see a list of all users in Immich?
|
||||
|
||||
You can see the list of all users by running [list-users](/docs/administration/server-commands.md) Command on the Immich-server.
|
||||
|
||||
|
@ -24,14 +24,14 @@ You can see the list of all users by running [list-users](/docs/administration/s
|
|||
|
||||
### I cannot log into the application after an update. What can I do?
|
||||
|
||||
First, verify that the mobile app and server are both running the same version (major and minor).
|
||||
Verify that the mobile app and server are both running the same version (major and minor).
|
||||
|
||||
:::note
|
||||
App store updates sometimes take longer because the stores (Google play store and Apple app store)
|
||||
need to approve the update first which may take some time.
|
||||
App store updates sometimes take longer because the stores (Google Play Store and Apple App Store)
|
||||
need to approve the update first, and it can take some time.
|
||||
:::
|
||||
|
||||
If you still cannot login to the app, try the following:
|
||||
If you still cannot log in to the app, try the following:
|
||||
|
||||
- Check the mobile logs
|
||||
- Make sure login credentials are correct by logging in on the web app
|
||||
|
@ -40,6 +40,11 @@ If you still cannot login to the app, try the following:
|
|||
|
||||
## Assets
|
||||
|
||||
### Does Immich change the file?
|
||||
|
||||
No, Immich does not touch the original file under any circumstances,
|
||||
all edited metadata are saved in the companion sidecar file and the database.
|
||||
|
||||
### Can I add my existing photo library?
|
||||
|
||||
Yes, with an [External Library](/docs/features/libraries.md).
|
||||
|
@ -50,11 +55,11 @@ Template changes will only apply to _new_ assets. To retroactively apply the tem
|
|||
|
||||
### Why are only photos and not videos being uploaded to Immich?
|
||||
|
||||
This often happens when using a reverse proxy (such as nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also check the disk space of your reverse proxy, in some cases proxies cache requests to disk before passing them on, and if disk space runs out the request fails.
|
||||
This often happens when using a reverse proxy (such as Nginx or Cloudflare tunnel) in front of Immich. Make sure to set your reverse proxy to allow large `POST` requests. In `nginx`, set `client_max_body_size 50000M;` or similar. Also, check the disk space of your reverse proxy. In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
||||
|
||||
### Why are some photos stored in the file system with the wrong date?
|
||||
|
||||
There are a few different scenarios that can lead to this situation. The solution is to run the storage migration job again. The job is only _automatically_ run once per asset, after upload. If metadata extraction originally failed, the jobs were cleared/cancelled, etc. the job may not have run automatically the first time.
|
||||
There are a few different scenarios that can lead to this situation. The solution is to rerun the storage migration job. The job is only automatically run once per asset after upload. If metadata extraction originally failed, the jobs were cleared/canceled, etc., the job may not have run automatically the first time.
|
||||
|
||||
### How can I hide photos from the timeline?
|
||||
|
||||
|
@ -68,23 +73,27 @@ See [Backup and Restore](/docs/administration/backup-and-restore.md).
|
|||
|
||||
No, it currently does not. There is an [open feature request on GitHub](https://github.com/immich-app/immich/discussions/4348).
|
||||
|
||||
### Does Immich support filtering of NSFW images?
|
||||
### Does Immich support the filtering of NSFW images?
|
||||
|
||||
No, it currently does not. There is an [open feature request on Github](https://github.com/immich-app/immich/discussions/2451).
|
||||
|
||||
### Why are there so many thumbnail generation jobs?
|
||||
|
||||
There are three thubmanil jobs for each asset:
|
||||
There are three thumbnail jobs for each asset:
|
||||
|
||||
- Blurred (thumbhash)
|
||||
- Small (webp)
|
||||
- Large (jpeg)
|
||||
- Preview (Webp)
|
||||
- Thumbnail (Jpeg)
|
||||
|
||||
Also, there are additional jobs for person (face) thumbnails.
|
||||
|
||||
### Why do files from WhatsApp not appear with the correct date?
|
||||
|
||||
Files sent on WhatsApp are saved without metadata on the file. Therefore, Immich has no way of knowing the original date of the file when files are uploaded from WhatsApp, not the order of arrival on the device. [See #3527](https://github.com/immich-app/immich/issues/3527).
|
||||
|
||||
### What happens if an asset exists in more than one account?
|
||||
|
||||
There are no requirements for assets to be unique across users. If multiple users upload the same image they are processed as if they were distinct assets and jobs run and thumbnails are generated accordingly.
|
||||
There are no requirements for assets to be unique across users. If multiple users upload the same image, it is processed as if it were a distinct asset, and jobs run and thumbnails are generated accordingly.
|
||||
|
||||
### Why do HDR videos appear pale in Immich player but look normal after download?
|
||||
|
||||
|
@ -96,40 +105,36 @@ Immich always keeps your original files. Alongside that, it generates a transcod
|
|||
|
||||
### How can I delete transcoded videos without deleting the original?
|
||||
|
||||
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary, then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options, or for all assets by running transcoding jobs for all assets from the administration page.
|
||||
The transcoded version of an asset can be deleted by setting a transcode policy that makes it unnecessary and then running a transcoding job for that asset. This can be done on a per-asset basis by starting a transcoding job for a single asset with the _Refresh encoded videos_ button in the asset viewer options or for all assets by running transcoding jobs for all assets from the administration page.
|
||||
|
||||
To update the transcode policy, navigate to Administration > Video Transcoding Settings > Transcoding Policy and select a policy from the drop-down. This policy will determine whether an existing transcode will be deleted or overwritten in the transcoding job. If a video should be transcoded according to this policy, an existing transcode is overwritten. If not, then it is deleted.
|
||||
|
||||
:::note
|
||||
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format". If an asset was only transcoded due to its resolution, then running a transcoding job for it will now delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary as a result. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are all unnecessary.
|
||||
For example, say you have existing transcodes with the policy "Videos higher than normal resolution or not in the desired format" and switch to a narrower policy: "Videos not in the desired format." If an asset was only transcoded due to its resolution, running a transcoding job for it will delete the existing transcode. This is because resolution is no longer part of the transcode policy and the transcode is unnecessary. Likewise, if you set the policy to "Don't transcode any videos" and run transcoding jobs for all assets, this will delete all existing transcodes as they are unnecessary.
|
||||
:::
|
||||
|
||||
### Is it possible to compress images during backup?
|
||||
|
||||
No. Our golden rule is that the original assets should always be untouched, so we don't think this feature is a good fit for Immich.
|
||||
No. Our design principle is that the original assets should always be untouched.
|
||||
|
||||
### How can I move all data (photos, persons, albums) from one user to another?
|
||||
|
||||
This is not officially supported, but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add for example an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file, so that you can use a web-interface.
|
||||
|
||||
:::warning
|
||||
This is an advanced operation. If you can't do it with the steps described here, this is not for you.
|
||||
:::
|
||||
This is not officially supported but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add, for example, an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file so that you can use a web interface.
|
||||
|
||||
<details>
|
||||
<summary>Steps</summary>
|
||||
|
||||
1. **MAKE A BACKUP** - See [backup and restore](/docs/administration/backup-and-restore.md).
|
||||
|
||||
2. Find the id of both the 'source' and the 'destination' user (it's the id column in the users table)
|
||||
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
|
||||
|
||||
3. Three tables need to be updated:
|
||||
|
||||
```sql
|
||||
// reassign albums
|
||||
// Reassign albums
|
||||
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
|
||||
|
||||
// reassign people
|
||||
// Reassign people
|
||||
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
|
||||
|
||||
// reassign assets
|
||||
|
@ -159,7 +164,7 @@ No, not yet. For updates on this planned feature, follow the [GitHub discussion]
|
|||
|
||||
### Can I add an external library while keeping the existing album structure?
|
||||
|
||||
We haven't put in an official mechanism to create albums from external libraries at the moment, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
|
||||
We haven't implemented an official mechanism for creating albums from external libraries, but there are some [workarounds from the community](https://github.com/immich-app/immich/discussions/4279) to help you achieve that.
|
||||
|
||||
### What happens to duplicates in external libraries?
|
||||
|
||||
|
@ -171,7 +176,7 @@ Duplicate checking only exists for upload libraries, using the file hash. Furthe
|
|||
|
||||
### How does smart search work?
|
||||
|
||||
Immich uses CLIP models, for more information about CLIP and its capabilities read about it [here](https://openai.com/research/clip).
|
||||
Immich uses CLIP models. For more information about CLIP and its capabilities, read about it [here](https://openai.com/research/clip).
|
||||
|
||||
### How does facial recognition work?
|
||||
|
||||
|
@ -189,33 +194,31 @@ However, disabling all jobs will not disable the machine learning service itself
|
|||
|
||||
### I'm getting errors about models being corrupt or failing to download. What do I do?
|
||||
|
||||
You can delete the model cache volume, which is where models are downloaded to. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface](https://huggingface.co/immich-app) and place them in the cache folder.
|
||||
|
||||
### Why did Immich decide to remove object detection?
|
||||
|
||||
The feature added keywords to images for metadata search, but wasn't used for smart search. Smart search made it unnecessary as it isn't limited to exact keywords. Combined with it causing crashes on some devices, using many dependencies and causing user confusion as to how search worked, it was better to remove the job altogether.
|
||||
For more info see [here](https://github.com/immich-app/immich/pull/5903)
|
||||
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface][huggingface] and place them in the cache folder.
|
||||
|
||||
### Can I use a custom CLIP model?
|
||||
|
||||
No, this is not supported. Only models listed in the [Huggingface](https://huggingface.co/immich-app) page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
|
||||
No, this is not supported. Only models listed in the [Huggingface][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
|
||||
|
||||
### I want to be able to search in other languages besides English. How can I do that?
|
||||
|
||||
You can change to a multilingual model listed [here](https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7) by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model. Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages.
|
||||
|
||||
:::note
|
||||
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list](https://huggingface.co/immich-app).
|
||||
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list][huggingface].
|
||||
:::
|
||||
|
||||
### Does Immich support Facial Recognition for videos ?
|
||||
### Does Immich support Facial Recognition for videos?
|
||||
|
||||
Immich's machine learning feature operate on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
|
||||
Immich's machine learning feature operates on the generated thumbnail. If a face is visible in the video's thumbnail it will be picked up by facial recognition.
|
||||
Scanning the entire video for faces may be implemented in the future.
|
||||
|
||||
### Does Immich have animal recognition?
|
||||
|
||||
No.
|
||||
:::tip
|
||||
You can use [Smart Search](/docs/features/smart-search.md) for this to some extent. For example, if you have a Golden Retriever and a Chihuahua, type these words in the smart search and watch the results.
|
||||
:::
|
||||
|
||||
### I'm getting a lot of "faces" that aren't faces, what can I do?
|
||||
|
||||
|
@ -248,8 +251,8 @@ The initial backup is the most intensive due to the number of jobs running. The
|
|||
- Lower the job concurrency for these jobs to 1.
|
||||
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
|
||||
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
|
||||
- You _must_ re-run the Face Detection job for all images after this for facial recognition on new images to work properly.
|
||||
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for how you can disable machine learning.
|
||||
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
|
||||
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
|
||||
|
||||
### Can I limit the amount of CPU and RAM usage?
|
||||
|
||||
|
@ -280,13 +283,17 @@ On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond thi
|
|||
|
||||
Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server.
|
||||
|
||||
More detail can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
|
||||
More details can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
|
||||
:::
|
||||
|
||||
### Why is Immich using so much of my CPU?
|
||||
|
||||
When a large amount of assets are uploaded to Immich it makes sense that the CPU and RAM will be heavily used due to machine learning work and creating image thumbnails.
|
||||
Once this process completes, the percentage of CPU usage will drop to around 3-5% usage
|
||||
When a large number of assets are uploaded to Immich, it makes sense that the CPU and RAM will be heavily used for machine learning work and creating image thumbnails.
|
||||
Once this process is completed, the percentage of CPU usage will drop to around 3-5% usage
|
||||
|
||||
### My server shows Server Status Offline | Version Unknown what can I do?
|
||||
|
||||
You need to enable Websocket on your reverse proxy.
|
||||
|
||||
---
|
||||
|
||||
|
@ -311,7 +318,7 @@ For a further hardened system, you can add the following block to every containe
|
|||
|
||||
```yaml
|
||||
security_opt:
|
||||
# Prevent escalation of privileges after container is started
|
||||
# Prevent escalation of privileges after the container is started
|
||||
- no-new-privileges:true
|
||||
cap_drop:
|
||||
# Prevent access to raw network traffic
|
||||
|
@ -322,7 +329,7 @@ cap_drop:
|
|||
|
||||
Data for Immich comes in two forms:
|
||||
|
||||
1. **Metadata** stored in a postgres database, persisted via the `pg_data` volume
|
||||
1. **Metadata** stored in a Postgres database, persisted via the `pg_data` volume
|
||||
2. **Files** (originals, thumbs, profile, etc.), stored in the `UPLOAD_LOCATION` folder, more [info](/docs/administration/backup-and-restore#asset-types-and-storage-locations).
|
||||
|
||||
To remove the **Metadata** you can stop Immich and delete the volume.
|
||||
|
@ -359,3 +366,5 @@ If your version of Immich is below 1.92.0 and the crash occurs after logs about
|
|||
### Why does Immich log migration errors on startup?
|
||||
|
||||
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`.
|
||||
|
||||
[huggingface]: https://huggingface.co/immich-app
|
||||
|
|
|
@ -63,7 +63,7 @@ services:
|
|||
...
|
||||
backup:
|
||||
container_name: immich_db_dumper
|
||||
image: prodrigestivill/postgres-backup-local
|
||||
image: prodrigestivill/postgres-backup-local:14
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
|
@ -86,6 +86,12 @@ Then you can restore with the same command but pointed at the latest dump.
|
|||
gunzip < db_dumps/last/immich-latest.sql.gz | docker exec -i immich_postgres psql --username=postgres
|
||||
```
|
||||
|
||||
:::note
|
||||
If you see the error `ERROR: type "earth" does not exist`, or you have problems with Reverse Geocoding after a restore, add the following `sed` fragment to your restore command.
|
||||
|
||||
Example: `gunzip < "/path/to/backup/dump.sql.gz" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | docker exec -i immich_postgres psql --username=postgres`
|
||||
:::
|
||||
|
||||
## Filesystem
|
||||
|
||||
Immich stores two types of content in the filesystem: (1) original, unmodified content, and (2) generated content. Only the original content needs to be backed-up, which includes the following folders:
|
||||
|
|
|
@ -10,9 +10,11 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
|
|||
|
||||
## Prerequisites
|
||||
|
||||
You must install pgvecto.rs using their [instructions](https://docs.pgvecto.rs/getting-started/installation.html). After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||
You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||
|
||||
:::note
|
||||
Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported.
|
||||
|
||||
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. For example, if your Immich version uses the dedicated database image `tensorchord/pgvecto-rs:pg14-v0.2.1`, you must install pgvecto.rs `>= 0.2.1, < 0.3.0`.
|
||||
:::
|
||||
|
||||
|
@ -30,6 +32,10 @@ DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport
|
|||
# DB_URL='postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename?sslmode=require&sslmode=no-verify'
|
||||
```
|
||||
|
||||
:::info
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
|
||||
:::
|
||||
|
||||
## With superuser permission
|
||||
|
||||
Typically Immich expects superuser permission in the database, which you can grant by running `ALTER USER <immichdbusername> WITH SUPERUSER;` at the `psql` console. If you prefer not to grant superuser permissions, follow the instructions in the next section.
|
||||
|
@ -50,8 +56,7 @@ ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
|||
CREATE EXTENSION vectors;
|
||||
CREATE EXTENSION earthdistance CASCADE;
|
||||
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||
GRANT USAGE ON SCHEMA vectors TO <immichdbusername>;
|
||||
ALTER DEFAULT PRIVILEGES IN SCHEMA vectors GRANT SELECT ON TABLES TO <immichdbusername>;
|
||||
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
|
||||
COMMIT;
|
||||
```
|
||||
|
||||
|
@ -63,4 +68,6 @@ When installing a new version of pgvecto.rs, you will need to manually update th
|
|||
|
||||
#### Permission denied for view
|
||||
|
||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat to <immichdbusername>;`.
|
||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
||||
|
||||
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
|
||||
|
|
|
@ -20,10 +20,6 @@ In any other situation, there are 3 different options that can appear:
|
|||
|
||||
- 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).
|
||||
|
||||
:::tip
|
||||
To get rid of Offline paths you can follow this [guide](/docs/guides/remove-offline-files.md)
|
||||
:::
|
||||
|
||||
- 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.
|
||||
|
||||
In addition, you can download the information from a page, mark everything (in order to check hashing) and correct the problem if a match is found in the hashing.
|
||||
|
|
|
@ -37,9 +37,7 @@ You can set the scanning interval using the preset or cron format. For more info
|
|||
|
||||
## Logging
|
||||
|
||||
By default logs are set to record at the log level, the network administrator can choose a deeper or lower level of logs according to his decision or according to the needs required by the Immich support team.
|
||||
|
||||
Here you can [learn about the different error levels](https://sematext.com/blog/logging-levels/).
|
||||
The default Immich log level is `Log` (commonly known as `Info`). The Immich administrator can choose a higher or lower log level according to personal preference or as requested by the Immich support team.
|
||||
|
||||
## Machine Learning Settings
|
||||
|
||||
|
|
12
docs/docs/community-guides.mdx
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Community Guides
|
||||
|
||||
This page lists community guides that are written around Immich, but not officially supported by the development team.
|
||||
|
||||
:::warning
|
||||
This list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.
|
||||
:::
|
||||
|
||||
import CommunityGuides from '../src/components/community-guides.tsx';
|
||||
import React from 'react';
|
||||
|
||||
<CommunityGuides />
|
12
docs/docs/community-projects.mdx
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Community Projects
|
||||
|
||||
This page lists community projects that are built around Immich, but not officially supported by the development team.
|
||||
|
||||
:::warning
|
||||
This list comes with no guarantees about security, performance, reliability, or accuracy. Use at your own risk.
|
||||
:::
|
||||
|
||||
import CommunityProjects from '../src/components/community-projects.tsx';
|
||||
import React from 'react';
|
||||
|
||||
<CommunityProjects />
|
|
@ -9,7 +9,7 @@ When contributing code through a pull request, please check the following:
|
|||
- [ ] `npm run check:svelte` (Type checking via SvelteKit)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
:::tip AIO
|
||||
Run all web checks with `npm run check:all`
|
||||
:::
|
||||
|
||||
|
@ -20,10 +20,14 @@ Run all web checks with `npm run check:all`
|
|||
- [ ] `npm run check` (Type checking via `tsc`)
|
||||
- [ ] `npm test` (unit tests)
|
||||
|
||||
:::tip
|
||||
:::tip AIO
|
||||
Run all server checks with `npm run check:all`
|
||||
:::
|
||||
|
||||
:::info Auto Fix
|
||||
You can use `npm run __:fix` to potentially correct some issues automatically for `npm run format` and `lint`.
|
||||
:::
|
||||
|
||||
## OpenAPI
|
||||
|
||||
The OpenAPI client libraries need to be regenerated whenever there are changes to the `immich-openapi-specs.json` file. Note that you should not modify this file directly as it is auto-generated. See [OpenAPI](/docs/developer/open-api.md) for more details.
|
||||
|
|
|
@ -16,7 +16,7 @@ Thanks for being interested in contributing 😊
|
|||
|
||||
## Environment
|
||||
|
||||
### Server and web app
|
||||
### Services
|
||||
|
||||
This environment includes the services below. Additional details are available in each service's README.
|
||||
|
||||
|
@ -28,7 +28,7 @@ This environment includes the services below. Additional details are available i
|
|||
|
||||
All the services are packaged to run as with single Docker Compose command.
|
||||
|
||||
### Instructions
|
||||
### Server and web apps
|
||||
|
||||
1. Clone the project repo.
|
||||
2. Run `cp docker/example.env docker/.env`.
|
||||
|
@ -47,13 +47,7 @@ You can access the web from `http://your-machine-ip:2283` or `http://localhost:2
|
|||
|
||||
**Note:** the "web" development container runs with uid 1000. If that uid does not have read/write permissions on the mounted volumes, you may encounter errors
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
### Connect to a remote backend
|
||||
#### Connect web to a remote backend
|
||||
|
||||
If you only want to do web development connected to an existing, remote backend, follow these steps:
|
||||
|
||||
|
@ -66,13 +60,21 @@ If you only want to do web development connected to an existing, remote backend,
|
|||
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
|
||||
```
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
The mobile app asks you what backend to connect to. You can utilize the demo backend (https://demo.immich.app/) if you don't need to change server code or upload photos. Alternatively, you can run the server yourself per the instructions above.
|
||||
|
||||
## IDE setup
|
||||
|
||||
### Lint / format extensions
|
||||
|
||||
Setting these in the IDE give a better developer experience, auto-formatting code on save, and providing instant feedback on lint issues.
|
||||
|
||||
### Dart Code Metris
|
||||
### Dart Code Metrics
|
||||
|
||||
The mobile app uses DCM (Dart Code Metrics) for linting and metrics calculation. Please refer to the [Getting Started](https://dcm.dev/docs/getting-started/#installation) page for more information on setting up DCM
|
||||
|
||||
|
|
|
@ -8,15 +8,24 @@ Unit are run by calling `npm run test` from the `server` directory.
|
|||
|
||||
### End to end tests
|
||||
|
||||
The backend has two end-to-end test suites that can be called with the following two commands from the project root directory:
|
||||
The e2e tests can be run by first starting up a test production environment via:
|
||||
|
||||
- `make server-e2e-api`
|
||||
- `make server-e2e-jobs`
|
||||
```bash
|
||||
make e2e
|
||||
```
|
||||
|
||||
#### API (e2e)
|
||||
Once the test environment is running, the e2e tests can be run via:
|
||||
|
||||
The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints.
|
||||
```bash
|
||||
cd e2e/
|
||||
npm test
|
||||
```
|
||||
|
||||
#### Jobs (e2e)
|
||||
The tests check various things including:
|
||||
|
||||
The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated.
|
||||
- Authentication and authorization
|
||||
- Query param, body, and url validation
|
||||
- Response codes
|
||||
- Thumbnail generation
|
||||
- Metadata extraction
|
||||
- Library scanning
|
||||
|
|
BIN
docs/docs/features/img/advanced-search-filters.webp
Normal file
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/docs/features/img/library-custom-scan-interval.png
Normal file
After Width: | Height: | Size: 31 KiB |
BIN
docs/docs/features/img/moblie-smart-serach.webp
Normal file
After Width: | Height: | Size: 4.9 MiB |
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 183 KiB After Width: | Height: | Size: 236 KiB |
BIN
docs/docs/features/img/partner-sharing-4.png
Normal file
After Width: | Height: | Size: 30 KiB |
BIN
docs/docs/features/img/partner-sharing-5.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
docs/docs/features/img/partner-sharing-6.png
Normal file
After Width: | Height: | Size: 1.6 MiB |
BIN
docs/docs/features/img/partner-sharing-7.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
docs/docs/features/img/search-ex-1.png
Normal file
After Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 162 KiB |
Before Width: | Height: | Size: 59 KiB |
|
@ -161,7 +161,7 @@ The christmas trip library will now be scanned in the background. In the meantim
|
|||
|
||||
- Click on Create External Library.
|
||||
|
||||
:::info Note
|
||||
:::note
|
||||
If you get an error here, please rename the other external library to something else. This is a bug that will be fixed in a future release.
|
||||
:::
|
||||
|
||||
|
@ -175,3 +175,14 @@ If you get an error here, please rename the other external library to something
|
|||
- Click on Scan Library Files
|
||||
|
||||
Within seconds, the assets from the old-pics and videos folders should show up in the main timeline.
|
||||
|
||||
### Set Custom Scan Interval
|
||||
|
||||
:::note
|
||||
Only an admin can do this.
|
||||
:::
|
||||
|
||||
You can define a custom interval for the trigger external library rescan under Administration -> Settings -> Library.
|
||||
You can set the scanning interval using the preset or cron format. For more information you can refer to [Crontab Guru](https://crontab.guru/).
|
||||
|
||||
<img src={require('./img/library-custom-scan-interval.png').default} width="75%" title='Set custom scan interval for external library' />
|
||||
|
|
|
@ -10,7 +10,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
|||
## Supported Backends
|
||||
|
||||
- ARM NN (Mali)
|
||||
- CUDA (NVIDIA)
|
||||
- CUDA (NVIDIA) Note: It is supported with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher
|
||||
- OpenVINO (Intel)
|
||||
|
||||
## Limitations
|
||||
|
@ -43,7 +43,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
|||
|
||||
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||
2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
||||
3. Redeploy the `immich-machine-learning` container with these updated settings.
|
||||
3. Still in `immich-machine-learning`, add one of -[armnn, cuda, openvino] to the `image` section's tag at the end of the line.
|
||||
4. Redeploy the `immich-machine-learning` container with these updated settings.
|
||||
|
||||
#### Single Compose File
|
||||
|
||||
|
@ -60,8 +61,6 @@ deploy:
|
|||
count: 1
|
||||
capabilities:
|
||||
- gpu
|
||||
- compute
|
||||
- video
|
||||
```
|
||||
|
||||
You can add this to the `immich-machine-learning` service instead of extending from `hwaccel.ml.yml`:
|
||||
|
@ -69,6 +68,7 @@ You can add this to the `immich-machine-learning` service instead of extending f
|
|||
```yaml
|
||||
immich-machine-learning:
|
||||
container_name: immich_machine_learning
|
||||
# Note the `-cuda` at the end
|
||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}-cuda
|
||||
# Note the lack of an `extends` section
|
||||
deploy:
|
||||
|
@ -79,8 +79,6 @@ immich-machine-learning:
|
|||
count: 1
|
||||
capabilities:
|
||||
- gpu
|
||||
- compute
|
||||
- video
|
||||
volumes:
|
||||
- model-cache:/cache
|
||||
env_file:
|
||||
|
|
|
@ -1,17 +1,57 @@
|
|||
# Partner Sharing
|
||||
|
||||
Immich allows you to share your library with other users. They can then view your library and download the assets.
|
||||
|
||||
You can manage one or multiple users to have access to your library from the [User Settings](docs/features/user-settings.md) page.
|
||||
|
||||
<img src={require('./img/partner-sharing-1.png').default} title='Partner Sharing 1' />
|
||||
|
||||
<img src={require('./img/partner-sharing-2.png').default} title='Partner Sharing 2' />
|
||||
|
||||
Accessing the shared library can be done from the Sharing page.
|
||||
|
||||
<img src={require('./img/partner-sharing-3.png').default} title='Partner Sharing 3' />
|
||||
|
||||
:::tip Sharing specific assets
|
||||
For sharing a specific set of assets, you can use the shared album feature of Immich.
|
||||
:::
|
||||
|
||||
Immich allows you to share your library with other users. They can then view your library and download the assets. You can manage Partner Sharing from the [User Settings](docs/features/user-settings.md) page on the web.
|
||||
|
||||
Partner Sharing includes:
|
||||
|
||||
- Access to all non-archived and trashed photos and videos.
|
||||
- Access to all metadata, including GPS information.
|
||||
- Access to share assets via shared links, albums, etc.
|
||||
|
||||
:::info
|
||||
Partner sharing is one-way. To view your partner's assets, they must also share them with you.
|
||||
:::
|
||||
|
||||
## Sharing with a Partner
|
||||
|
||||
:::note Duplicates
|
||||
Partner sharing may result in displaying duplicate assets on the main timeline.
|
||||
:::
|
||||
|
||||
<img src={require('./img/partner-sharing-1.png').default} width="70%" title='Add Partner 1' />
|
||||
|
||||
<img src={require('./img/partner-sharing-2.png').default} width="70%" title='Add Partner 2' />
|
||||
|
||||
<img src={require('./img/partner-sharing-4.png').default} width="70%" title='Add Partner 4' />
|
||||
|
||||
## Viewing Partner Assets
|
||||
|
||||
Access partner assets via the Sharing page.
|
||||
|
||||
<img src={require('./img/partner-sharing-3.png').default} width="70%" title='Access to the Shared Library' />
|
||||
|
||||
## Timeline Integration
|
||||
|
||||
Partner shared photos can be displayed in the main timeline. This feature can be enabled on a per-partner basis and can be viewed and updated on both the web and mobile app.
|
||||
|
||||
### Web:
|
||||
|
||||
Account Settings -> Sharing -> Show in timeline
|
||||
|
||||
<img src={require('./img/partner-sharing-5.png').default} width="70%" title='Partner Sharing for the web interface' />
|
||||
|
||||
### Mobile App:
|
||||
|
||||
From the partner’s view, on the top right corner of the app bar
|
||||
|
||||
<img src={require('./img/partner-sharing-6.png').default} width="30%" title='Partner Sharing for the mobile app' />
|
||||
|
||||
## Removing Access
|
||||
|
||||
In order to remove a partner, you can go to User -> Account Settings -> Sharing and click on the X button.
|
||||
|
||||
<img src={require('./img/partner-sharing-7.png').default} width="70%" title='Remove Partner' />
|
||||
|
|
|
@ -8,7 +8,7 @@ During Exif Extraction, assets with latitudes and longitudes are reverse geocode
|
|||
|
||||
## Usage
|
||||
|
||||
Data from a reverse geocode is displayed in the image details, and used in [Search](/docs/features/search.md).
|
||||
Data from a reverse geocode is displayed in the image details, and used in [Smart Search](/docs/features/smart-search.md).
|
||||
|
||||
<img src={require('./img/reverse-geocoding-mobile1.png').default} width='33%' title='Reverse Geocoding' />
|
||||
<img src={require('./img/reverse-geocoding-mobile2.png').default} width='33%' title='Reverse Geocoding' />
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
# Search
|
||||
|
||||
Immich uses Postgres as its search database for both metadata and smart search.
|
||||
|
||||
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like CLIP to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
|
||||
|
||||
Metadata search (prefixed with `m:`) can search specifically by text without the use of a model.
|
||||
|
||||
Archived photos are not included in search results by default. To include them, add the query parameter `withArchived=true` to the url.
|
||||
|
||||
Some search examples:
|
||||
<img src={require('./img/search-ex-2.webp').default} title='Search Example 1' />
|
||||
|
||||
<img src={require('./img/search-ex-3.webp').default} title='Search Example 2' />
|
49
docs/docs/features/smart-search.md
Normal file
|
@ -0,0 +1,49 @@
|
|||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
# Smart Search
|
||||
|
||||
Immich uses Postgres as its search database for both metadata and smart search.
|
||||
|
||||
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
|
||||
|
||||
Archived photos are not included in search results by default. To include them, mark the checkbox in [advanced search filters](/docs/features/smart-search#advanced-search-filters).
|
||||
|
||||
:::tip Alternative CLIP Models
|
||||
More powerful models can be used for more accurate search results. For more information, see the related [FAQ](/docs/FAQ#can-i-use-a-custom-clip-model).
|
||||
:::
|
||||
|
||||
:::info
|
||||
Smart Search is currently limited to 5,000 results for a single search on the web.
|
||||
:::
|
||||
|
||||
## Advanced Search Filters
|
||||
|
||||
In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app).
|
||||
|
||||
Smart search features include:
|
||||
|
||||
- Search for one or more faces (with or without context search).
|
||||
- Search by Country or State or City or by all three.
|
||||
- Search by camera make and model.
|
||||
- Search by date range.
|
||||
- Search by file name.
|
||||
- Search by media types: image, video or all (**Note:** Image includes live images).
|
||||
- Search by condition: not in any album or archive or Favorite or all conditions.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="Computer" label="Computer" default>
|
||||
|
||||
Some search examples:
|
||||
|
||||
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
|
||||
|
||||
<img src={require('./img/search-ex-1.png').default} width="70%" title='Search Example 1' />
|
||||
|
||||
</TabItem>
|
||||
<TabItem value="Mobile" label="Mobile">
|
||||
|
||||
<img src={require('./img/moblie-smart-serach.webp').default} width="30%" title='Smart search on mobile' />
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
|
@ -3,7 +3,7 @@
|
|||
Immich supports a number of image and video formats, the most common of which are outlined here.
|
||||
|
||||
:::note
|
||||
For the full list, you can refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
|
||||
For the full list, refer to the [Immich source code](https://github.com/immich-app/immich/blob/main/server/src/utils/mime-types.ts).
|
||||
:::
|
||||
|
||||
## Image formats
|
||||
|
|
|
@ -1,130 +0,0 @@
|
|||
# API Album Sync (Python Script)
|
||||
|
||||
This is an example of a python script for syncing an album to a local folder. This was used for a digital photoframe so the displayed photos could be managed from the immich web or app UI.
|
||||
|
||||
The script is copied below in it's current form. A repository is hosted [here](https://git.orenit.solutions/open/immichalbumpull).
|
||||
|
||||
:::danger
|
||||
This guide uses a generated API key. This key gives the same access to your immich instance as the user it is attached to, so be careful how the config file is stored and transferred.
|
||||
:::
|
||||
|
||||
### Prerequisites
|
||||
|
||||
- Python 3.7+
|
||||
- [requests library](https://pypi.org/project/requests/)
|
||||
|
||||
### Installing
|
||||
|
||||
Copy the contents of 'pull.py' (shown below) to your chosen location or clone the repository:
|
||||
|
||||
```bash
|
||||
git clone https://git.orenit.solutions/open/immichalbumpull
|
||||
```
|
||||
|
||||
Edit or create the 'config.ini' file in the same directory as the script with the necessary details:
|
||||
|
||||
```ini title='config.ini'
|
||||
[immich]
|
||||
# URL of target immich instance
|
||||
url = https://photo.example.com
|
||||
# API key from Account Settings -> API Keys
|
||||
apikey = xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
# Full local path to target directory
|
||||
destination = /home/photo/photos
|
||||
# immich album name
|
||||
album = Photoframe
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
Run the script directly:
|
||||
|
||||
```bash
|
||||
./pull.py
|
||||
```
|
||||
|
||||
Or from cron (every 5 minutes):
|
||||
|
||||
```bash
|
||||
*/5 * * * * /usr/bin/python /home/user/immichalbumpull/pull.py
|
||||
```
|
||||
|
||||
### Python Script
|
||||
|
||||
```python title='pull.py'
|
||||
#!/usr/bin/env python
|
||||
|
||||
import requests
|
||||
import configparser
|
||||
import os
|
||||
import shutil
|
||||
|
||||
# Read config file
|
||||
config = configparser.ConfigParser()
|
||||
config.read('config.ini')
|
||||
|
||||
url = config['immich']['url']
|
||||
apikey = config['immich']['apikey']
|
||||
photodir = config['immich']['destination']
|
||||
albumname = config['immich']['album']
|
||||
|
||||
headers = {
|
||||
'Accept': 'application/json',
|
||||
'x-api-key': apikey
|
||||
}
|
||||
|
||||
# Set up the directory for the downloaded images
|
||||
os.makedirs(photodir, exist_ok=True)
|
||||
|
||||
# Get the list of albums from the API
|
||||
response = requests.get(url + "/api/album", headers=headers)
|
||||
|
||||
# Parse the JSON response
|
||||
data = response.json()
|
||||
|
||||
# Find the chosen album id
|
||||
for item in data:
|
||||
if item['albumName'] == albumname:
|
||||
albumid = item['id']
|
||||
|
||||
# Get the list of photos from the API using the albumid
|
||||
response = requests.get(url + "/api/album/" + albumid, headers=headers)
|
||||
|
||||
# Parse the JSON response and extract the URLs of the images
|
||||
data = response.json()
|
||||
image_urls = data['assets']
|
||||
|
||||
# Download each image from the URL and save it to the directory
|
||||
headers = {
|
||||
'Accept': 'application/octet-stream',
|
||||
'x-api-key': apikey
|
||||
}
|
||||
|
||||
photolist = []
|
||||
|
||||
for id in image_urls:
|
||||
# Query asset info endpoint for correct extension
|
||||
assetinfourl = url + "/api/asset/" + str(id['id'])
|
||||
response = requests.get(assetinfourl, headers=headers)
|
||||
assetinfo = response.json()
|
||||
ext = os.path.splitext(assetinfo['originalFileName'])
|
||||
|
||||
asseturl = url + "/api/download/asset/" + str(id['id'])
|
||||
response = requests.post(asseturl, headers=headers, stream=True)
|
||||
|
||||
# Build current photo list for deletions below
|
||||
photo = os.path.basename(asseturl) + ext[1]
|
||||
photolist.append(photo)
|
||||
|
||||
photofullpath = photodir + '/' + os.path.basename(asseturl) + ext[1]
|
||||
# Only download file if it doesn't already exist
|
||||
if not os.path.exists(photofullpath):
|
||||
with open(photofullpath, 'wb') as f:
|
||||
for chunk in response.iter_content(1024):
|
||||
f.write(chunk)
|
||||
|
||||
# Delete old photos removed from album
|
||||
for filename in os.listdir(photodir):
|
||||
if filename not in photolist:
|
||||
os.unlink(os.path.join(photodir, filename))
|
||||
```
|
50
docs/docs/guides/custom-locations.md
Normal file
|
@ -0,0 +1,50 @@
|
|||
# Files Custom Locations
|
||||
|
||||
This guide explains storing generated and raw files with docker's volume mount in different locations.
|
||||
|
||||
:::note Backup
|
||||
It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools.
|
||||
:::
|
||||
|
||||
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
|
||||
|
||||
```diff title=".env"
|
||||
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
|
||||
|
||||
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
|
||||
- {UPLOAD_LOCATION}=./library
|
||||
+ {UPLOAD_LOCATION}=/custom/location/on/your/system/
|
||||
+ {THUMB_LOCATION}=/custom/location/on/your/system/
|
||||
+ {ENCODED_VIDEO_LOCATION}=/custom/location/on/your/system/
|
||||
...
|
||||
```
|
||||
|
||||
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers.
|
||||
|
||||
```diff title="docker-compose.yml"
|
||||
services:
|
||||
immich-server:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
|
||||
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
|
||||
...
|
||||
|
||||
immich-microservices:
|
||||
volumes:
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
|
||||
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
```
|
||||
|
||||
Restart Immich to register the changes.
|
||||
|
||||
```
|
||||
docker compose down
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
|
|
@ -45,5 +45,5 @@ Open pgAdmin and click "Add New Server".
|
|||
Click on "Save" to connect to the Immich database.
|
||||
|
||||
:::tip
|
||||
View [Database Queries](https://immich.app/docs/guides/database-queries/) for common database queries.
|
||||
View [Database Queries](/docs/guides/database-queries/) for common database queries.
|
||||
:::
|
||||
|
|
|
@ -17,7 +17,7 @@ The `"originalFileName"` column is the name of the file at time of upload, inclu
|
|||
:::
|
||||
|
||||
```sql title="Find by original filename"
|
||||
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848';
|
||||
SELECT * FROM "assets" WHERE "originalFileName" = 'PXL_20230903_232542848.jpg';
|
||||
SELECT * FROM "assets" WHERE "originalFileName" LIKE 'PXL_%'; -- all files starting with PXL_
|
||||
SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files with _2023_ in the middle
|
||||
```
|
||||
|
@ -27,21 +27,30 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
|
|||
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
|
||||
```
|
||||
|
||||
```sql title="Find by checksum" (sha1)
|
||||
:::note
|
||||
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
|
||||
:::
|
||||
|
||||
```sql title="Find by checksum (SHA-1)"
|
||||
SELECT encode("checksum", 'hex') FROM "assets";
|
||||
SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e033bf74dd1', 'hex');
|
||||
```
|
||||
|
||||
```sql title="Live photos"
|
||||
SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
|
||||
SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL;
|
||||
```
|
||||
|
||||
```sql title="Without metadata"
|
||||
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
|
||||
SELECT "assets".* FROM "exif"
|
||||
LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId"
|
||||
WHERE "exif"."assetId" IS NULL;
|
||||
```
|
||||
|
||||
```sql title="size < 100,000 bytes, smallest to largest"
|
||||
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
|
||||
SELECT * FROM "assets"
|
||||
JOIN "exif" ON "assets"."id" = "exif"."assetId"
|
||||
WHERE "exif"."fileSizeInByte" < 100000
|
||||
ORDER BY "exif"."fileSizeInByte" ASC;
|
||||
```
|
||||
|
||||
```sql title="Without thumbnails"
|
||||
|
@ -54,20 +63,14 @@ SELECT * FROM "assets" WHERE "assets"."type" = 'IMAGE';
|
|||
```
|
||||
|
||||
```sql title="Count by type"
|
||||
SELECT "assets"."type", count(*) FROM "assets" GROUP BY "assets"."type";
|
||||
SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type";
|
||||
```
|
||||
|
||||
```sql title="Count by type (per user)"
|
||||
SELECT
|
||||
"users"."email", "assets"."type", COUNT(*)
|
||||
FROM
|
||||
"assets"
|
||||
JOIN
|
||||
"users" ON "assets"."ownerId" = "users"."id"
|
||||
GROUP BY
|
||||
"assets"."type", "users"."email"
|
||||
ORDER BY
|
||||
"users"."email";
|
||||
SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets"
|
||||
JOIN "users" ON "assets"."ownerId" = "users"."id"
|
||||
GROUP BY "assets"."type", "users"."email"
|
||||
ORDER BY "users"."email";
|
||||
```
|
||||
|
||||
```sql title="Failed file movements"
|
||||
|
@ -76,7 +79,7 @@ SELECT * FROM "move_history";
|
|||
|
||||
## Users
|
||||
|
||||
```sql title="List"
|
||||
```sql title="List all users"
|
||||
SELECT * FROM "users";
|
||||
```
|
||||
|
||||
|
@ -87,3 +90,9 @@ SELECT "key", "value" FROM "system_config";
|
|||
```
|
||||
|
||||
(Only used when not using the [config file](/docs/install/config-file))
|
||||
|
||||
## Persons
|
||||
|
||||
```sql title="Delete person and unset it for the faces it was associated with"
|
||||
DELETE FROM person WHERE name = 'PersonNameHere';
|
||||
```
|
||||
|
|
|
@ -14,12 +14,6 @@ Edit `docker-compose.yml` to add two new mount points under `volumes:`
|
|||
- ${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:`.
|
||||
|
||||
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
|
||||
|
|
|
@ -4,12 +4,16 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
|
|||
|
||||
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
|
||||
- Copy the following `docker-compose.yml` to your ML system.
|
||||
- Start the container by running `docker-compose up -d` or `docker compose up -d` (depending on your Docker version).
|
||||
- Start the container by running `docker compose up -d`.
|
||||
|
||||
:::note Info
|
||||
:::info
|
||||
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
|
||||
:::
|
||||
|
||||
:::note
|
||||
The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be in the same folder if trying to use [hardware acceleration](/docs/features/ml-hardware-acceleration).
|
||||
:::
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
|
|
|
@ -1,176 +0,0 @@
|
|||
# Remove Offline Files [Community]
|
||||
|
||||
import Tabs from '@theme/Tabs';
|
||||
import TabItem from '@theme/TabItem';
|
||||
|
||||
:::note
|
||||
**Before running the script**, please make sure you have a [backup](/docs/administration/backup-and-restore) of your assets and database.
|
||||
:::
|
||||
|
||||
:::info
|
||||
**None** of the scripts can delete orphaned files from the external library.
|
||||
:::
|
||||
|
||||
This page is a guide to get rid of offline files from the repair page.
|
||||
|
||||
<Tabs>
|
||||
|
||||
<TabItem value="Python script (Best way)" label="Python script (Best way)">
|
||||
|
||||
This way works by retrieving a file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files.
|
||||
|
||||
1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard.
|
||||
2. Copy and save the code to file -> `Immich Remove Offline Files.py`.
|
||||
3. Run the script and follow the instructions.
|
||||
|
||||
:::note
|
||||
You might need to run `pip install halo tabulate tqdm` if these dependencies are missing on your machine.
|
||||
:::
|
||||
|
||||
```bash title='Python'
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Note: you might need to run "pip install halo tabulate tqdm" if these dependencies are missing on your machine
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import requests
|
||||
|
||||
from datetime import datetime
|
||||
from halo import Halo
|
||||
from tabulate import tabulate
|
||||
from tqdm import tqdm
|
||||
from urllib.parse import urlparse
|
||||
|
||||
def parse_arguments():
|
||||
parser = argparse.ArgumentParser(description='Fetch file report and delete orphaned media assets from Immich.')
|
||||
parser.add_argument('--apikey', help='Immich API key for authentication')
|
||||
parser.add_argument('--immichaddress', help='Full address for Immich, including protocol and port')
|
||||
parser.add_argument('--no_prompt', action='store_true', help='Delete orphaned media assets without confirmation')
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
def filter_entities(response_json, entity_type):
|
||||
return [
|
||||
{'pathValue': entity['pathValue'], 'entityId': entity['entityId'], 'entityType': entity['entityType']}
|
||||
for entity in response_json.get('orphans', []) if entity.get('entityType') == entity_type
|
||||
]
|
||||
|
||||
def main():
|
||||
args = parse_arguments()
|
||||
try:
|
||||
if args.apikey:
|
||||
api_key = args.apikey
|
||||
else:
|
||||
api_key = input('Enter the Immich API key: ')
|
||||
|
||||
if args.immichaddress:
|
||||
immich_server = args.immichaddress
|
||||
else:
|
||||
immich_server = input('Enter the full web address for Immich, including protocol and port: ')
|
||||
immich_parsed_url = urlparse(immich_server)
|
||||
base_url = f'{immich_parsed_url.scheme}://{immich_parsed_url.netloc}'
|
||||
api_url = f'{base_url}/api'
|
||||
file_report_url = api_url + '/audit/file-report'
|
||||
headers = {'x-api-key': api_key}
|
||||
|
||||
print()
|
||||
spinner = Halo(text='Retrieving list of orphaned media assets...', spinner='dots')
|
||||
spinner.start()
|
||||
|
||||
try:
|
||||
response = requests.get(file_report_url, headers=headers)
|
||||
response.raise_for_status()
|
||||
spinner.succeed('Success!')
|
||||
except requests.exceptions.RequestException as e:
|
||||
spinner.fail(f'Failed to fetch assets: {str(e)}')
|
||||
|
||||
person_assets = filter_entities(response.json(), 'person')
|
||||
orphan_media_assets = filter_entities(response.json(), 'asset')
|
||||
|
||||
num_entries = len(orphan_media_assets)
|
||||
|
||||
if num_entries == 0:
|
||||
print('No orphaned media assets found; exiting.')
|
||||
return
|
||||
|
||||
else:
|
||||
if not args.no_prompt:
|
||||
table_data = []
|
||||
for asset in orphan_media_assets:
|
||||
table_data.append([asset['pathValue'], asset['entityId']])
|
||||
print(tabulate(table_data, headers=['Path Value', 'Entity ID'], tablefmt='pretty'))
|
||||
print()
|
||||
|
||||
if person_assets:
|
||||
print('Found orphaned person assets! Please run the "RECOGNIZE FACES > ALL" job in Immich after running this tool to correct this.')
|
||||
print()
|
||||
|
||||
if num_entries > 0:
|
||||
summary = f'There {"is" if num_entries == 1 else "are"} {num_entries} orphaned media asset{"s" if num_entries != 1 else ""}. Would you like to delete {"them" if num_entries != 1 else "it"} from Immich? (yes/no): '
|
||||
user_input = input(summary).lower()
|
||||
print()
|
||||
|
||||
if user_input not in ('y', 'yes'):
|
||||
print('Exiting without making any changes.')
|
||||
return
|
||||
|
||||
with tqdm(total=num_entries, desc="Deleting orphaned media assets", unit="asset") as progress_bar:
|
||||
for asset in orphan_media_assets:
|
||||
entity_id = asset['entityId']
|
||||
asset_url = f'{api_url}/asset'
|
||||
delete_payload = json.dumps({'force': True, 'ids': [entity_id]})
|
||||
headers = {'Content-Type': 'application/json', 'x-api-key': api_key}
|
||||
response = requests.delete(asset_url, headers=headers, data=delete_payload)
|
||||
response.raise_for_status()
|
||||
progress_bar.set_postfix_str(entity_id)
|
||||
progress_bar.update(1)
|
||||
print()
|
||||
print('Orphaned media assets deleted successfully!')
|
||||
except Exception as e:
|
||||
print()
|
||||
print(f"An error occurred: {str(e)}")
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
```
|
||||
|
||||
Thanks to [DooMRunneR](https://discord.com/channels/979116623879368755/1179655214870040596/1194308198413373482) and [Sircharlo](https://discord.com/channels/979116623879368755/1179655214870040596/1195038609812758639) for writing this script.
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="Bash and PowerShell script" label="Bash and PowerShell script" default>
|
||||
|
||||
This way works by downloading a JSON file that contains a list of all the files that are defined as offline files, running a script that uses the [Immich API](/docs/api/delete-assets) in order to remove the offline files.
|
||||
|
||||
1. Create an API key under Admin User -> Account Settings -> API Keys -> New API Key -> Copy to clipboard.
|
||||
2. Download the JSON file under Administration -> repair -> Export.
|
||||
3. Replace `YOUR_IP_HERE` and `YOUR_API_KEY_HERE` with your actual IP address and API key in the script.
|
||||
4. Run the script in the same folder where the JSON file is located.
|
||||
|
||||
## Script for Linux based systems:
|
||||
|
||||
```bash title='Bash'
|
||||
awk -F\" '/entityId/ {print $4}' orphans.json | while read line; do curl --location --request DELETE 'http://YOUR_IP_HERE:2283/api/asset' --header 'Content- Type: application/json' --header 'x-api-key: YOUR_API_KEY_HERE' --data '{ "force": true, "ids": ["'"$line"'"]}';done
|
||||
```
|
||||
|
||||
## Script for the Windows system (run through PowerShell):
|
||||
|
||||
```powershell title='PowerShell'
|
||||
Get-Content orphans.json | Select-String -Pattern 'entityId' | ForEach-Object {
|
||||
$line = $_ -split '"' | Select-Object -Index 3
|
||||
$body = [pscustomobject]@{
|
||||
'ids' = @($line)
|
||||
'force' = (' true ' | ConvertFrom-Json)
|
||||
} | ConvertTo-Json -Depth 3
|
||||
Invoke-RestMethod -Uri 'http://YOUR_IP_HERE:2283/api/asset' -Method Delete -Headers @{
|
||||
'Content-Type' = 'application/json'
|
||||
'x-api-key' = 'YOUR_API_KEY_HERE'
|
||||
} -Body $body
|
||||
}
|
||||
```
|
||||
|
||||
Thanks to [DooMRunneR](https://discord.com/channels/979116623879368755/1179655214870040596/1194308198413373482) for writing this script.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
|
@ -7,7 +7,8 @@ import ExampleEnv from '!!raw-loader!../../../docker/example.env';
|
|||
|
||||
# Docker Compose [Recommended]
|
||||
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||
Immich requires Docker Compose version 2.x.
|
||||
|
||||
### Step 1 - Download the required files
|
||||
|
||||
|
@ -66,7 +67,7 @@ docker compose up -d
|
|||
```
|
||||
|
||||
:::info Docker version
|
||||
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository).
|
||||
If you get an error `unknown shorthand flag: 'd' in -d`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by `apt remove`ing Ubuntu's docker.io package and installing docker and docker-compose via [Docker's official repository][docker-repo].
|
||||
|
||||
Note that the correct command really is `docker compose`, not `docker-compose`. If you try the latter on vanilla Ubuntu 22.04 it will fail in a different way:
|
||||
|
||||
|
@ -83,27 +84,31 @@ For more information on how to use the application, please refer to the [Post In
|
|||
:::
|
||||
|
||||
:::note GitHub Authentication
|
||||
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry)).
|
||||
Downloading container images might require you to authenticate to the GitHub Container Registry ([steps here][container-auth]).
|
||||
:::
|
||||
|
||||
### Step 4 - Upgrading
|
||||
|
||||
:::danger Breaking Changes
|
||||
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created).
|
||||
It is important to follow breaking updates to avoid problems. You can see versions that had breaking changes [here][breaking].
|
||||
:::
|
||||
|
||||
If `IMMICH_VERSION` is set, it will need to be updated to the latest or desired version.
|
||||
|
||||
When a new version of Immich is [released](https://github.com/immich-app/immich/releases), the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
When a new version of Immich is [released][releases], the application can be upgraded with the following commands, run in the directory with the `docker-compose.yml` file:
|
||||
|
||||
```bash title="Upgrade Immich"
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
:::caution Automatic Updates
|
||||
Immich is currently under heavy development, which means you can expect [breaking changes](https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created) and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
Immich is currently under heavy development, which means you can expect [breaking changes][breaking] and bugs. Therefore, we recommend reading the release notes prior to updating and to take special care when using automated tools like [Watchtower][watchtower].
|
||||
:::
|
||||
|
||||
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||
[watchtower]: https://containrrr.dev/watchtower/
|
||||
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created
|
||||
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
|
||||
[releases]: https://github.com/immich-app/immich/releases
|
||||
[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
|
||||
|
|
|
@ -41,19 +41,20 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
|
||||
|
||||
:::tip
|
||||
`TZ` should be set to a `TZ identifier` from [this list](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List). For example, `TZ="Etc/UTC"`.
|
||||
`TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||
|
||||
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
|
||||
:::
|
||||
|
||||
## Ports
|
||||
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------- | :-------------------- | :-------: | :--------------- |
|
||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
||||
| `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 |
|
||||
| Variable | Description | Default | Services |
|
||||
| :---------------------- | :-------------------- | :-------: | :-------------------- |
|
||||
| `HOST` | Host | `0.0.0.0` | server, microservices |
|
||||
| `SERVER_PORT` | Server Port | `3001` | server |
|
||||
| `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
|
||||
|
||||
|
@ -71,7 +72,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||
|
||||
:::info
|
||||
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
|
||||
When `DB_URL` is defined, the other database (`DB_*`) variables are ignored, with the exception of `DB_VECTOR_EXTENSION`.
|
||||
|
||||
:::
|
||||
|
||||
|
@ -90,7 +91,7 @@ When `DB_URL` is defined, the other database (`DB_*`) variables are ignored.
|
|||
:::info
|
||||
|
||||
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
||||
More info can be found in the upstream [ioredis](https://ioredis.readthedocs.io/en/latest/API/) documentation.
|
||||
More info can be found in the upstream [ioredis][redis-api] documentation.
|
||||
|
||||
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
|
||||
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
|
||||
|
@ -158,7 +159,7 @@ Other machine learning parameters can be tuned from the admin UI.
|
|||
|
||||
## Docker Secrets
|
||||
|
||||
The following variables support the use of [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) for additional security.
|
||||
The following variables support the use of [Docker secrets][docker-secrets] for additional security.
|
||||
|
||||
To use any of these, replace the regular environment variable with the equivalent `_FILE` environment variable. The value of
|
||||
the `_FILE` variable should be set to the path of a file containing the variable value.
|
||||
|
@ -172,8 +173,14 @@ the `_FILE` variable should be set to the path of a file containing the variable
|
|||
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
|
||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
||||
|
||||
\*1: See the [official documentation](https://github.com/docker-library/docs/tree/master/postgres#docker-secrets) for
|
||||
\*1: See the [official documentation][docker-secrets-docs] for
|
||||
details on how to use Docker Secrets in the Postgres image.
|
||||
|
||||
\*2: See [this comment](https://github.com/docker-library/redis/issues/46#issuecomment-335326234) for an example of how
|
||||
\*2: See [this comment][docker-secrets-example] for an example of how
|
||||
to use use a Docker secret for the password in the Redis container.
|
||||
|
||||
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
||||
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
||||
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
||||
[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
|
||||
|
|
|
@ -6,7 +6,7 @@ sidebar_position: 40
|
|||
|
||||
You can deploy Immich on Kubernetes using [the official Helm chart](https://github.com/immich-app/immich-charts/tree/main/charts/immich).
|
||||
|
||||
If you want examples of how other people run Immich on Kubernetes, using the official chart or otherwise, you can find them at https://nanne.dev/k8s-at-home-search/#/immich.
|
||||
You can view some [examples](https://kubesearch.dev/#/immich) of how other people run Immich on Kubernetes, using the official chart or otherwise.
|
||||
|
||||
:::caution DNS in Alpine containers
|
||||
Immich makes use of Alpine container images. These can encounter [a DNS resolution bug](https://stackoverflow.com/a/65593511) on Kubernetes clusters if the host
|
||||
|
|
|
@ -11,6 +11,10 @@ Hardware and software requirements for Immich
|
|||
- [Docker](https://docs.docker.com/get-docker/)
|
||||
- [Docker Compose](https://docs.docker.com/compose/install/)
|
||||
|
||||
:::note
|
||||
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.
|
||||
:::
|
||||
|
|
|
@ -17,12 +17,11 @@ curl -o- https://raw.githubusercontent.com/immich-app/immich/main/install.sh | b
|
|||
The script will perform the following actions:
|
||||
|
||||
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
|
||||
2. Populate the `.env` file with necessary information based on the current directory path.
|
||||
3. Start the containers.
|
||||
2. Start the containers.
|
||||
|
||||
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
|
||||
|
||||
The directory which is used to store the library files is `./immich-data` relative to the current directory.
|
||||
The directory which is used to store the library files is `./immich-app` relative to the current directory.
|
||||
|
||||
:::tip
|
||||
For common next steps, see [Post Install Steps](/docs/install/post-install.mdx).
|
||||
|
|
|
@ -98,7 +98,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
|||
|
||||
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
|
||||
|
||||
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_web` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
||||
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_server` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
||||
|
||||
<img
|
||||
src={require('./img/unraid06.webp').default}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user can change their password after logging in to the application for the first time.
|
||||
If you have friends or family members who want to use the application as well, you can create addition accounts. The default password is `password`, and the user has to change their password after logging in to the application for the first time. The system administrator can disable this option by unchecking the option "Require user to change password on first login" in the user registration interface.
|
||||
|
||||
<img src={require('./img/create-new-user.png').default} title='Admin Registration' />
|
||||
<img src={require('./img/create-new-user.png').default} width="90%" title='New User Registration' />
|
||||
|
|
|
@ -144,6 +144,10 @@ const config = {
|
|||
label: 'Discord',
|
||||
href: 'https://discord.com/invite/D8JsnBEuKb',
|
||||
},
|
||||
{
|
||||
label: 'Reddit',
|
||||
href: 'https://www.reddit.com/r/immich/',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -157,6 +161,10 @@ const config = {
|
|||
label: 'GitHub',
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
},
|
||||
{
|
||||
label: 'YouTube',
|
||||
href: 'https://www.youtube.com/@immich-app',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
|
|
402
docs/package-lock.json
generated
|
@ -2185,9 +2185,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/core": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.1.1.tgz",
|
||||
"integrity": "sha512-2nQfKFcf+MLEM7JXsXwQxPOmQAR6ytKMZVSx7tVi9HEm9WtfwBH1fp6bn8Gj4zLUhjWKCLoysQ9/Wm+EZCQ4yQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.2.1.tgz",
|
||||
"integrity": "sha512-ZeMAqNvy0eBv2dThEeMuNzzuu+4thqMQakhxsgT5s02A8LqRcdkg+rbcnuNqUIpekQ4GRx3+M5nj0ODJhBXo9w==",
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.23.3",
|
||||
"@babel/generator": "^7.23.3",
|
||||
|
@ -2199,14 +2199,13 @@
|
|||
"@babel/runtime": "^7.22.6",
|
||||
"@babel/runtime-corejs3": "^7.22.6",
|
||||
"@babel/traverse": "^7.22.8",
|
||||
"@docusaurus/cssnano-preset": "3.1.1",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/cssnano-preset": "3.2.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/react-loadable": "5.5.2",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-common": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@slorber/static-site-generator-webpack-plugin": "^4.0.7",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"autoprefixer": "^10.4.14",
|
||||
"babel-loader": "^9.1.3",
|
||||
|
@ -2227,6 +2226,7 @@
|
|||
"detect-port": "^1.5.1",
|
||||
"escape-html": "^1.0.3",
|
||||
"eta": "^2.2.0",
|
||||
"eval": "^0.1.8",
|
||||
"file-loader": "^6.2.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
|
@ -2235,6 +2235,7 @@
|
|||
"leven": "^3.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mini-css-extract-plugin": "^2.7.6",
|
||||
"p-map": "^4.0.0",
|
||||
"postcss": "^8.4.26",
|
||||
"postcss-loader": "^7.3.3",
|
||||
"prompts": "^2.4.2",
|
||||
|
@ -2271,9 +2272,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/cssnano-preset": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.1.1.tgz",
|
||||
"integrity": "sha512-LnoIDjJWbirdbVZDMq+4hwmrTl2yHDnBf9MLG9qyExeAE3ac35s4yUhJI8yyTCdixzNfKit4cbXblzzqMu4+8g==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.2.1.tgz",
|
||||
"integrity": "sha512-wTL9KuSSbMJjKrfu385HZEzAoamUsbKqwscAQByZw4k6Ja/RWpqgVvt/CbAC+aYEH6inLzOt+MjuRwMOrD3VBA==",
|
||||
"dependencies": {
|
||||
"cssnano-preset-advanced": "^5.3.10",
|
||||
"postcss": "^8.4.26",
|
||||
|
@ -2285,9 +2286,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/logger": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.1.1.tgz",
|
||||
"integrity": "sha512-BjkNDpQzewcTnST8trx4idSoAla6zZ3w22NqM/UMcFtvYJgmoE4layuTzlfql3VFPNuivvj7BOExa/+21y4X2Q==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.2.1.tgz",
|
||||
"integrity": "sha512-0voOKJCn9RaM3np6soqEfo7SsVvf2C+CDTWhW+H/1AyBhybASpExtDEz+7ECck9TwPzFQ5tt+I3zVugUJbJWDg==",
|
||||
"dependencies": {
|
||||
"chalk": "^4.1.2",
|
||||
"tslib": "^2.6.0"
|
||||
|
@ -2297,15 +2298,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/mdx-loader": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.1.1.tgz",
|
||||
"integrity": "sha512-xN2IccH9+sv7TmxwsDJNS97BHdmlqWwho+kIVY4tcCXkp+k4QuzvWBeunIMzeayY4Fu13A6sAjHGv5qm72KyGA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.2.1.tgz",
|
||||
"integrity": "sha512-Fs8tXhXKZjNkdGaOy1xSLXSwfjCMT73J3Zfrju2U16uGedRFRjgK0ojpK5tiC7TnunsL3tOFgp1BSMBRflX9gw==",
|
||||
"dependencies": {
|
||||
"@babel/parser": "^7.22.7",
|
||||
"@babel/traverse": "^7.22.8",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"@mdx-js/mdx": "^3.0.0",
|
||||
"@slorber/remark-comment": "^1.0.0",
|
||||
"escape-html": "^1.0.3",
|
||||
|
@ -2337,12 +2336,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/module-type-aliases": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.1.1.tgz",
|
||||
"integrity": "sha512-xBJyx0TMfAfVZ9ZeIOb1awdXgR4YJMocIEzTps91rq+hJDFJgJaylDtmoRhUxkwuYmNK1GJpW95b7DLztSBJ3A==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.2.1.tgz",
|
||||
"integrity": "sha512-FyViV5TqhL1vsM7eh29nJ5NtbRE6Ra6LP1PDcPvhwPSlA7eiWGRKAn3jWwMUcmjkos5SYY+sr0/feCdbM3eQHQ==",
|
||||
"dependencies": {
|
||||
"@docusaurus/react-loadable": "5.5.2",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*",
|
||||
"@types/react-router-config": "*",
|
||||
|
@ -2356,17 +2355,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-content-blog": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.1.1.tgz",
|
||||
"integrity": "sha512-ew/3VtVoG3emoAKmoZl7oKe1zdFOsI0NbcHS26kIxt2Z8vcXKCUgK9jJJrz0TbOipyETPhqwq4nbitrY3baibg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.2.1.tgz",
|
||||
"integrity": "sha512-lOx0JfhlGZoZu6pEJfeEpSISZR5dQbJGGvb42IP13G5YThNHhG9R9uoWuo4IOimPqBC7sHThdLA3VLevk61Fsw==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-common": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"cheerio": "^1.0.0-rc.12",
|
||||
"feed": "^4.2.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
|
@ -2387,17 +2386,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-content-docs": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.1.1.tgz",
|
||||
"integrity": "sha512-lhFq4E874zw0UOH7ujzxnCayOyAt0f9YPVYSb9ohxrdCM8B4szxitUw9rIX4V9JLLHVoqIJb6k+lJJ1jrcGJ0A==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.2.1.tgz",
|
||||
"integrity": "sha512-GHe5b/lCskAR8QVbfWAfPAApvRZgqk7FN3sOHgjCtjzQACZxkHmq6QqyqZ8Jp45V7lVck4wt2Xw2IzBJ7Cz3bA==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/module-type-aliases": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/module-type-aliases": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"@types/react-router-config": "^5.0.7",
|
||||
"combine-promises": "^1.1.0",
|
||||
"fs-extra": "^11.1.1",
|
||||
|
@ -2416,15 +2416,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-content-pages": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.1.1.tgz",
|
||||
"integrity": "sha512-NQHncNRAJbyLtgTim9GlEnNYsFhuCxaCNkMwikuxLTiGIPH7r/jpb7O3f3jUMYMebZZZrDq5S7om9a6rvB/YCA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.2.1.tgz",
|
||||
"integrity": "sha512-TOqVfMVTAHqWNEGM94Drz+PUpHDbwFy6ucHFgyTx9zJY7wPNSG5EN+rd/mU7OvAi26qpOn2o9xTdUmb28QLjEQ==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"tslib": "^2.6.0",
|
||||
"webpack": "^5.88.1"
|
||||
|
@ -2438,13 +2438,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-debug": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.1.1.tgz",
|
||||
"integrity": "sha512-xWeMkueM9wE/8LVvl4+Qf1WqwXmreMjI5Kgr7GYCDoJ8zu4kD+KaMhrh7py7MNM38IFvU1RfrGKacCEe2DRRfQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.2.1.tgz",
|
||||
"integrity": "sha512-AMKq8NuUKf2sRpN1m/sIbqbRbnmk+rSA+8mNU1LNxEl9BW9F/Gng8m9HKlzeyMPrf5XidzS1jqkuTLDJ6KIrFw==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"react-json-view-lite": "^1.2.0",
|
||||
"tslib": "^2.6.0"
|
||||
|
@ -2458,13 +2458,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-google-analytics": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.1.1.tgz",
|
||||
"integrity": "sha512-+q2UpWTqVi8GdlLoSlD5bS/YpxW+QMoBwrPrUH/NpvpuOi0Of7MTotsQf9JWd3hymZxl2uu1o3PIrbpxfeDFDQ==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.2.1.tgz",
|
||||
"integrity": "sha512-/rJ+9u+Px0eTCiF4TNcNtj3kHf8cp6K1HCwOTdbsSlz6Xn21syZYcy+f1VM9wF6HrvUkXUcbM5TDCvg2IRL6bQ==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2476,13 +2476,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-google-gtag": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.1.1.tgz",
|
||||
"integrity": "sha512-0mMPiBBlQ5LFHTtjxuvt/6yzh8v7OxLi3CbeEsxXZpUzcKO/GC7UA1VOWUoBeQzQL508J12HTAlR3IBU9OofSw==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.2.1.tgz",
|
||||
"integrity": "sha512-XtuJnlMvYfppeVdUyKiDIJAa/gTJKCQU92z8CLZZ9ibJdgVjFOLS10s0hIC0eL5z0U2u2loJz2rZ63HOkNHbBA==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"@types/gtag.js": "^0.0.12",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
|
@ -2495,13 +2495,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-google-tag-manager": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.1.1.tgz",
|
||||
"integrity": "sha512-d07bsrMLdDIryDtY17DgqYUbjkswZQr8cLWl4tzXrt5OR/T/zxC1SYKajzB3fd87zTu5W5klV5GmUwcNSMXQXA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.2.1.tgz",
|
||||
"integrity": "sha512-wiS/kE0Ny5pnjTxVCs8ljRnkL1RVMj59t6jmSsgEX7piDOoaXSMIUaoIt9ogS/v132uO0xEsxHstkRUZHQyPcQ==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
"engines": {
|
||||
|
@ -2513,16 +2513,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/plugin-sitemap": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.1.1.tgz",
|
||||
"integrity": "sha512-iJ4hCaMmDaUqRv131XJdt/C/jJQx8UreDWTRqZKtNydvZVh/o4yXGRRFOplea1D9b/zpwL1Y+ZDwX7xMhIOTmg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.2.1.tgz",
|
||||
"integrity": "sha512-uWZ7AxzdeaQSTCwD2yZtOiEm9zyKU+wqCmi/Sf25kQQqqFSBZUStXfaQ8OHP9cecnw893ZpZ811rPhB/wfujJw==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-common": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"fs-extra": "^11.1.1",
|
||||
"sitemap": "^7.1.1",
|
||||
"tslib": "^2.6.0"
|
||||
|
@ -2536,23 +2536,23 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/preset-classic": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.1.1.tgz",
|
||||
"integrity": "sha512-jG4ys/hWYf69iaN/xOmF+3kjs4Nnz1Ay3CjFLDtYa8KdxbmUhArA9HmP26ru5N0wbVWhY+6kmpYhTJpez5wTyg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.2.1.tgz",
|
||||
"integrity": "sha512-E3OHSmttpEBcSMhfPBq3EJMBxZBM01W1rnaCUTXy9EHvkmB5AwgTfW1PwGAybPAX579ntE03R+2zmXdizWfKnQ==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/plugin-content-blog": "3.1.1",
|
||||
"@docusaurus/plugin-content-docs": "3.1.1",
|
||||
"@docusaurus/plugin-content-pages": "3.1.1",
|
||||
"@docusaurus/plugin-debug": "3.1.1",
|
||||
"@docusaurus/plugin-google-analytics": "3.1.1",
|
||||
"@docusaurus/plugin-google-gtag": "3.1.1",
|
||||
"@docusaurus/plugin-google-tag-manager": "3.1.1",
|
||||
"@docusaurus/plugin-sitemap": "3.1.1",
|
||||
"@docusaurus/theme-classic": "3.1.1",
|
||||
"@docusaurus/theme-common": "3.1.1",
|
||||
"@docusaurus/theme-search-algolia": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1"
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/plugin-content-blog": "3.2.1",
|
||||
"@docusaurus/plugin-content-docs": "3.2.1",
|
||||
"@docusaurus/plugin-content-pages": "3.2.1",
|
||||
"@docusaurus/plugin-debug": "3.2.1",
|
||||
"@docusaurus/plugin-google-analytics": "3.2.1",
|
||||
"@docusaurus/plugin-google-gtag": "3.2.1",
|
||||
"@docusaurus/plugin-google-tag-manager": "3.2.1",
|
||||
"@docusaurus/plugin-sitemap": "3.2.1",
|
||||
"@docusaurus/theme-classic": "3.2.1",
|
||||
"@docusaurus/theme-common": "3.2.1",
|
||||
"@docusaurus/theme-search-algolia": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
|
@ -2575,22 +2575,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-classic": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.1.1.tgz",
|
||||
"integrity": "sha512-GiPE/jbWM8Qv1A14lk6s9fhc0LhPEQ00eIczRO4QL2nAQJZXkjPG6zaVx+1cZxPFWbAsqSjKe2lqkwF3fGkQ7Q==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.2.1.tgz",
|
||||
"integrity": "sha512-+vSbnQyoWjc6vRZi4vJO2dBU02wqzynsai15KK+FANZudrYaBHtkbLZAQhgmxzBGVpxzi87gRohlMm+5D8f4tA==",
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/module-type-aliases": "3.1.1",
|
||||
"@docusaurus/plugin-content-blog": "3.1.1",
|
||||
"@docusaurus/plugin-content-docs": "3.1.1",
|
||||
"@docusaurus/plugin-content-pages": "3.1.1",
|
||||
"@docusaurus/theme-common": "3.1.1",
|
||||
"@docusaurus/theme-translations": "3.1.1",
|
||||
"@docusaurus/types": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-common": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/module-type-aliases": "3.2.1",
|
||||
"@docusaurus/plugin-content-blog": "3.2.1",
|
||||
"@docusaurus/plugin-content-docs": "3.2.1",
|
||||
"@docusaurus/plugin-content-pages": "3.2.1",
|
||||
"@docusaurus/theme-common": "3.2.1",
|
||||
"@docusaurus/theme-translations": "3.2.1",
|
||||
"@docusaurus/types": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"clsx": "^2.0.0",
|
||||
"copy-text-to-clipboard": "^3.2.0",
|
||||
|
@ -2614,17 +2614,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-common": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.1.1.tgz",
|
||||
"integrity": "sha512-38urZfeMhN70YaXkwIGXmcUcv2CEYK/2l4b05GkJPrbEbgpsIZM3Xc+Js2ehBGGZmfZq8GjjQ5RNQYG+MYzCYg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.2.1.tgz",
|
||||
"integrity": "sha512-d+adiD7L9xv6EvfaAwUqdKf4orsM3jqgeqAM+HAjgL/Ux0GkVVnfKr+tsoe+4ow4rHe6NUt+nkkW8/K8dKdilA==",
|
||||
"dependencies": {
|
||||
"@docusaurus/mdx-loader": "3.1.1",
|
||||
"@docusaurus/module-type-aliases": "3.1.1",
|
||||
"@docusaurus/plugin-content-blog": "3.1.1",
|
||||
"@docusaurus/plugin-content-docs": "3.1.1",
|
||||
"@docusaurus/plugin-content-pages": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-common": "3.1.1",
|
||||
"@docusaurus/mdx-loader": "3.2.1",
|
||||
"@docusaurus/module-type-aliases": "3.2.1",
|
||||
"@docusaurus/plugin-content-blog": "3.2.1",
|
||||
"@docusaurus/plugin-content-docs": "3.2.1",
|
||||
"@docusaurus/plugin-content-pages": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@types/history": "^4.7.11",
|
||||
"@types/react": "*",
|
||||
"@types/react-router-config": "*",
|
||||
|
@ -2643,18 +2643,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-search-algolia": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.1.1.tgz",
|
||||
"integrity": "sha512-tBH9VY5EpRctVdaAhT+b1BY8y5dyHVZGFXyCHgTrvcXQy5CV4q7serEX7U3SveNT9zksmchPyct6i1sFDC4Z5g==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.2.1.tgz",
|
||||
"integrity": "sha512-bzhCrpyXBXzeydNUH83II2akvFEGfhsNTPPWsk5N7e+odgQCQwoHhcF+2qILbQXjaoZ6B3c48hrvkyCpeyqGHw==",
|
||||
"dependencies": {
|
||||
"@docsearch/react": "^3.5.2",
|
||||
"@docusaurus/core": "3.1.1",
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/plugin-content-docs": "3.1.1",
|
||||
"@docusaurus/theme-common": "3.1.1",
|
||||
"@docusaurus/theme-translations": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/utils-validation": "3.1.1",
|
||||
"@docusaurus/core": "3.2.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/plugin-content-docs": "3.2.1",
|
||||
"@docusaurus/theme-common": "3.2.1",
|
||||
"@docusaurus/theme-translations": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-validation": "3.2.1",
|
||||
"algoliasearch": "^4.18.0",
|
||||
"algoliasearch-helper": "^3.13.3",
|
||||
"clsx": "^2.0.0",
|
||||
|
@ -2673,9 +2673,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/theme-translations": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.1.1.tgz",
|
||||
"integrity": "sha512-xvWQFwjxHphpJq5fgk37FXCDdAa2o+r7FX8IpMg+bGZBNXyWBu3MjZ+G4+eUVNpDhVinTc+j6ucL0Ain5KCGrg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.2.1.tgz",
|
||||
"integrity": "sha512-jAUMkIkFfY+OAhJhv6mV8zlwY6J4AQxJPTgLdR2l+Otof9+QdJjHNh/ifVEu9q0lp3oSPlJj9l05AaP7Ref+cg==",
|
||||
"dependencies": {
|
||||
"fs-extra": "^11.1.1",
|
||||
"tslib": "^2.6.0"
|
||||
|
@ -2685,9 +2685,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/types": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.1.1.tgz",
|
||||
"integrity": "sha512-grBqOLnubUecgKFXN9q3uit2HFbCxTWX4Fam3ZFbMN0sWX9wOcDoA7lwdX/8AmeL20Oc4kQvWVgNrsT8bKRvzg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.2.1.tgz",
|
||||
"integrity": "sha512-n/toxBzL2oxTtRTOFiGKsHypzn/Pm+sXyw+VSk1UbqbXQiHOwHwts55bpKwbcUgA530Is6kix3ELiFOv9GAMfw==",
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.0.0",
|
||||
"@types/history": "^4.7.11",
|
||||
|
@ -2705,11 +2705,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/utils": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.1.1.tgz",
|
||||
"integrity": "sha512-ZJfJa5cJQtRYtqijsPEnAZoduW6sjAQ7ZCWSZavLcV10Fw0Z3gSaPKA/B4micvj2afRZ4gZxT7KfYqe5H8Cetg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.2.1.tgz",
|
||||
"integrity": "sha512-DPkIS/EPc+pGAV798PUXgNzJFM3HJouoQXgr0KDZuJVz1EkWbDLOcQwLIz8Qx7liI9ddfkN/TXTRQdsTPZNakw==",
|
||||
"dependencies": {
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"@svgr/webpack": "^6.5.1",
|
||||
"escape-string-regexp": "^4.0.0",
|
||||
"file-loader": "^6.2.0",
|
||||
|
@ -2721,6 +2722,7 @@
|
|||
"js-yaml": "^4.1.0",
|
||||
"lodash": "^4.17.21",
|
||||
"micromatch": "^4.0.5",
|
||||
"prompts": "^2.4.2",
|
||||
"resolve-pathname": "^3.0.0",
|
||||
"shelljs": "^0.8.5",
|
||||
"tslib": "^2.6.0",
|
||||
|
@ -2740,9 +2742,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/utils-common": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.1.1.tgz",
|
||||
"integrity": "sha512-eGne3olsIoNfPug5ixjepZAIxeYFzHHnor55Wb2P57jNbtVaFvij/T+MS8U0dtZRFi50QU+UPmRrXdVUM8uyMg==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.2.1.tgz",
|
||||
"integrity": "sha512-N5vadULnRLiqX2QfTjVEU3u5vo6RG2EZTdyXvJdzDOdrLCGIZAfnf/VkssinFZ922sVfaFfQ4FnStdhn5TWdVg==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.0"
|
||||
},
|
||||
|
@ -2759,12 +2761,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@docusaurus/utils-validation": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.1.1.tgz",
|
||||
"integrity": "sha512-KlY4P9YVDnwL+nExvlIpu79abfEv6ZCHuOX4ZQ+gtip+Wxj0daccdReIWWtqxM/Fb5Cz1nQvUCc7VEtT8IBUAA==",
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.2.1.tgz",
|
||||
"integrity": "sha512-+x7IR9hNMXi62L1YAglwd0s95fR7+EtirjTxSN4kahYRWGqOi3jlQl1EV0az/yTEvKbxVvOPcdYicGu9dk4LJw==",
|
||||
"dependencies": {
|
||||
"@docusaurus/logger": "3.1.1",
|
||||
"@docusaurus/utils": "3.1.1",
|
||||
"@docusaurus/logger": "3.2.1",
|
||||
"@docusaurus/utils": "3.2.1",
|
||||
"@docusaurus/utils-common": "3.2.1",
|
||||
"joi": "^17.9.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"tslib": "^2.6.0"
|
||||
|
@ -3153,19 +3156,6 @@
|
|||
"micromark-util-symbol": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@slorber/static-site-generator-webpack-plugin": {
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@slorber/static-site-generator-webpack-plugin/-/static-site-generator-webpack-plugin-4.0.7.tgz",
|
||||
"integrity": "sha512-Ug7x6z5lwrz0WqdnNFOMYrDQNTPAprvHLSh6+/fmml3qUiz6l5eq+2MzLKWtn/q5K5NpSiFsZTP/fck/3vjSxA==",
|
||||
"dependencies": {
|
||||
"eval": "^0.1.8",
|
||||
"p-map": "^4.0.0",
|
||||
"webpack-sources": "^3.2.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/@svgr/babel-plugin-add-jsx-attribute": {
|
||||
"version": "6.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-6.5.1.tgz",
|
||||
|
@ -4300,13 +4290,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.6.7.tgz",
|
||||
"integrity": "sha512-/hDJGff6/c7u0hDkvkGxR/oy6CbCs8ziCsC7SqmhjfozqiJGc8Z11wrv9z9lYfY4K8l+H9TpjcMDX0xOZmx+RA==",
|
||||
"version": "0.26.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz",
|
||||
"integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.4",
|
||||
"form-data": "^4.0.0",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"follow-redirects": "^1.14.8"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-loader": {
|
||||
|
@ -6137,21 +6125,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/docusaurus-plugin-openapi": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.7.3.tgz",
|
||||
"integrity": "sha512-JOMlP4HarpSYQhBHgVCQecy4BXvt3bVLob1PXfBMEBYBy26Y8fwN+QktBdLJ4YLoQ8FoDO13+q5/335b790tiA==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-openapi/-/docusaurus-plugin-openapi-0.7.4.tgz",
|
||||
"integrity": "sha512-JZSAIPQFQ4FyUiHZcQ00/fIRIr1f74rxqVvzjuMmODQvUXvlOgPyxErmGJwYVf4sAxPl05IiYDPtmVeZZixPMg==",
|
||||
"dependencies": {
|
||||
"@docusaurus/mdx-loader": "^3.0.0",
|
||||
"@docusaurus/plugin-content-docs": "^3.0.0",
|
||||
"@docusaurus/utils": "^3.0.0",
|
||||
"@docusaurus/utils-validation": "^3.0.0",
|
||||
"axios": "^1.6.7",
|
||||
"@docusaurus/mdx-loader": "^3.2.0",
|
||||
"@docusaurus/plugin-content-docs": "^3.2.0",
|
||||
"@docusaurus/utils": "^3.2.0",
|
||||
"@docusaurus/utils-common": "^3.2.0",
|
||||
"@docusaurus/utils-validation": "^3.2.0",
|
||||
"axios": "^0.26.1",
|
||||
"chalk": "^4.1.2",
|
||||
"clsx": "^1.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json-refs": "^3.0.15",
|
||||
"json-schema-resolve-allof": "^1.5.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.20",
|
||||
"openapi-to-postmanv2": "^1.2.1",
|
||||
"postman-collection": "^4.1.0",
|
||||
"webpack": "^5.88.1"
|
||||
|
@ -6173,22 +6162,22 @@
|
|||
}
|
||||
},
|
||||
"node_modules/docusaurus-plugin-proxy": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-proxy/-/docusaurus-plugin-proxy-0.7.3.tgz",
|
||||
"integrity": "sha512-7DbDtPo6ZQK2kGROwSxtfMrdDiTJ2Bn+OQ591IBVduuH3dwVgzyKgzjxBknfDQjF5peZWDazVHwCV8ZZoI+6ew==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-plugin-proxy/-/docusaurus-plugin-proxy-0.7.4.tgz",
|
||||
"integrity": "sha512-utnMCZF1ewMjVHTmXqvx0O8jAy1hL6GiJWzo8U8sl5asGI78Qi8GZjVkdj9l3FGDA9LRtCK96rme3Aa0x7dFxg==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/docusaurus-preset-openapi": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-preset-openapi/-/docusaurus-preset-openapi-0.7.3.tgz",
|
||||
"integrity": "sha512-gXDhnSjl6QeJnUU6txPfNoITKyKKNQoxeiRKLh7npuI5KB3kWWL+jRneYTEph+LhtGf4LRlKNm06ZBS6MODRRw==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-preset-openapi/-/docusaurus-preset-openapi-0.7.4.tgz",
|
||||
"integrity": "sha512-O2Pje1aa7cTwhfBHTJgD3Gx+MgzhsSOCj97Ez8CWZDOJ2Mta68wFbQQV3UMWfujsvcy3ttNCgAe/LahtFo7jyg==",
|
||||
"dependencies": {
|
||||
"@docusaurus/preset-classic": "^3.0.0",
|
||||
"docusaurus-plugin-openapi": "^0.7.3",
|
||||
"docusaurus-plugin-proxy": "^0.7.3",
|
||||
"docusaurus-theme-openapi": "^0.7.3"
|
||||
"@docusaurus/preset-classic": "^3.2.0",
|
||||
"docusaurus-plugin-openapi": "^0.7.4",
|
||||
"docusaurus-plugin-proxy": "^0.7.4",
|
||||
"docusaurus-theme-openapi": "^0.7.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
|
@ -6199,11 +6188,11 @@
|
|||
}
|
||||
},
|
||||
"node_modules/docusaurus-theme-openapi": {
|
||||
"version": "0.7.3",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-theme-openapi/-/docusaurus-theme-openapi-0.7.3.tgz",
|
||||
"integrity": "sha512-KNcCgEcoNZT2uFGM0/SWIt9YOuBQYKnI10py7z8IJ4Ev02wo7c1DStVstlONIohyzFMTjqSbZ8hifZ/29AHJBQ==",
|
||||
"version": "0.7.4",
|
||||
"resolved": "https://registry.npmjs.org/docusaurus-theme-openapi/-/docusaurus-theme-openapi-0.7.4.tgz",
|
||||
"integrity": "sha512-E5nZJM/Z/YSWGtkvz8Mw3ueUt+uJcD2LMRK0He23u5ufUZPwGeRxL93DxtOL7mkEvdV6rFzH7LtzysOdqyGy3A==",
|
||||
"dependencies": {
|
||||
"@docusaurus/theme-common": "^3.0.0",
|
||||
"@docusaurus/theme-common": "^3.2.0",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
"@monaco-editor/react": "^4.3.1",
|
||||
"@reduxjs/toolkit": "^1.7.1",
|
||||
|
@ -6212,7 +6201,7 @@
|
|||
"crypto-js": "^4.1.1",
|
||||
"docusaurus-plugin-openapi": "^0.7.3",
|
||||
"immer": "^9.0.7",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.17.20",
|
||||
"marked": "^11.0.0",
|
||||
"monaco-editor": "^0.31.1",
|
||||
"postman-code-generators": "^1.0.0",
|
||||
|
@ -13611,11 +13600,6 @@
|
|||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
|
||||
},
|
||||
"node_modules/punycode": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||
|
@ -16141,9 +16125,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "5.4.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.3.tgz",
|
||||
"integrity": "sha512-KrPd3PKaCLr78MalgiwJnA25Nm8HAmdwN3mYUYZgG/wizIo9EainNVQI9/yDavtVFRN2h3k8uf3GLHuhDMgEHg==",
|
||||
"version": "5.4.5",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
|
||||
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
|
|
71
docs/src/components/community-guides.tsx
Normal file
|
@ -0,0 +1,71 @@
|
|||
import Link from '@docusaurus/Link';
|
||||
import React from 'react';
|
||||
|
||||
interface CommunityGuidesProps {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const guides: CommunityGuidesProps[] = [
|
||||
{
|
||||
title: 'Cloudflare Tunnels with SSO/OAuth',
|
||||
description: `Setting up Cloudflare Tunnels and a SaaS App for immich.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8299',
|
||||
},
|
||||
{
|
||||
title: 'Database backup in Truenas',
|
||||
description: `Create a database backup with pgAdmin in Truenas.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8809',
|
||||
},
|
||||
{
|
||||
title: 'Unraid backup scripts',
|
||||
description: `Back up your assets in Unarid with a pre-prepared script.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/8416',
|
||||
},
|
||||
{
|
||||
title: 'Sync folders with albums',
|
||||
description: `synchronize folders in imported library with albums having the folders name.`,
|
||||
url: 'https://github.com/immich-app/immich/discussions/3382',
|
||||
},
|
||||
{
|
||||
title: 'Podman/Quadlets Install',
|
||||
description: 'Documentation for simple podman setup using quadlets.',
|
||||
url: 'https://github.com/tbelway/immich-podman-quadlets/blob/main/docs/install/podman-quadlet.md',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="m-0 items-start flex gap-2">
|
||||
<span>{title}</span>
|
||||
</p>
|
||||
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">
|
||||
<a href={url}>{url}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link
|
||||
className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||
to={url}
|
||||
>
|
||||
View Guide
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityGuides(): JSX.Element {
|
||||
return (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{guides.map((guides) => (
|
||||
<CommunityGuide {...guides} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
96
docs/src/components/community-projects.tsx
Normal file
|
@ -0,0 +1,96 @@
|
|||
import Link from '@docusaurus/Link';
|
||||
import React from 'react';
|
||||
|
||||
interface CommunityProjectProps {
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
}
|
||||
|
||||
const projects: CommunityProjectProps[] = [
|
||||
{
|
||||
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.`,
|
||||
url: 'https://github.com/simulot/immich-go',
|
||||
},
|
||||
{
|
||||
title: 'ImmichFrame',
|
||||
description: 'Run an Immich slideshow in a photo frame.',
|
||||
url: 'https://github.com/3rob3/ImmichFrame',
|
||||
},
|
||||
{
|
||||
title: 'API Album Sync',
|
||||
description: 'A Python script to sync folders as albums.',
|
||||
url: 'https://git.orenit.solutions/open/immichalbumpull',
|
||||
},
|
||||
{
|
||||
title: 'Remove offline files',
|
||||
description: 'A Python script to remove offline files.',
|
||||
url: 'https://gist.github.com/Thoroslives/ca5d8e1efd15111febc1e7b34ac72668',
|
||||
},
|
||||
{
|
||||
title: 'Create albums from folders',
|
||||
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',
|
||||
},
|
||||
{
|
||||
title: 'Lightroom Publisher: mi.Immich.Publisher',
|
||||
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
|
||||
url: 'https://github.com/midzelis/mi.Immich.Publisher',
|
||||
},
|
||||
{
|
||||
title: 'Immich Duplicate Finder',
|
||||
description: 'Webapp that uses machine learning to identify near-duplicate images.',
|
||||
url: 'https://github.com/vale46n1/immich_duplicate_finder',
|
||||
},
|
||||
{
|
||||
title: 'Immich-Tiktok-Remover',
|
||||
description: 'Script to search for and remove TikTok videos from your Immich library.',
|
||||
url: 'https://github.com/mxc2/immich-tiktok-remover',
|
||||
},
|
||||
{
|
||||
title: 'Immich Android TV',
|
||||
description: 'Unofficial Immich Android TV app.',
|
||||
url: 'https://github.com/giejay/Immich-Android-TV',
|
||||
},
|
||||
{
|
||||
title: 'Powershell Module PSImmich',
|
||||
description: 'Powershell Module for the Immich API',
|
||||
url: 'https://github.com/hanpq/PSImmich',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||
return (
|
||||
<section className="flex flex-col gap-4 justify-between dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl p-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="m-0 items-start flex gap-2">
|
||||
<span>{title}</span>
|
||||
</p>
|
||||
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{description}</p>
|
||||
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">
|
||||
<a href={url}>{url}</a>
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex">
|
||||
<Link
|
||||
className="px-4 py-2 bg-immich-primary/10 dark:bg-gray-300 rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||
to={url}
|
||||
>
|
||||
View Project
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default function CommunityProjects(): JSX.Element {
|
||||
return (
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
{projects.map((project) => (
|
||||
<CommunityProject {...project} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -51,12 +51,22 @@ import {
|
|||
mdiVideo,
|
||||
mdiWeb,
|
||||
mdiScaleBalance,
|
||||
mdiMagnifyScan,
|
||||
} from '@mdi/js';
|
||||
import Layout from '@theme/Layout';
|
||||
import React from 'react';
|
||||
import Timeline, { DateType, Item } from '../components/timeline';
|
||||
|
||||
const items: Item[] = [
|
||||
{
|
||||
icon: mdiMagnifyScan,
|
||||
description: 'Advanced search with filters by date, location and more',
|
||||
title: 'Search enhancement with advanced filters',
|
||||
release: 'v1.95.0',
|
||||
tag: 'v1.95.0',
|
||||
date: new Date(2024, 1, 20),
|
||||
dateType: DateType.RELEASE,
|
||||
},
|
||||
{
|
||||
icon: mdiScaleBalance,
|
||||
description: 'Immich switches to AGPLv3 license',
|
||||
|
|
3
docs/static/_redirects
vendored
|
@ -25,3 +25,6 @@
|
|||
/docs/developer/contributing /docs/developer/pr-checklist 301
|
||||
/docs/guides/machine-learning /docs/guides/remote-machine-learning 301
|
||||
/docs/administration/password-login /docs/administration/system-settings 301
|
||||
/docs/features/search /docs/features/smart-search 301
|
||||
/docs/guides/api-album-sync /docs/community-projects 301
|
||||
/docs/guides/remove-offline-files /docs/community-projects 301
|
||||
|
|
|
@ -19,6 +19,7 @@ module.exports = {
|
|||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
|
|
924
e2e/package-lock.json
generated
|
@ -33,7 +33,7 @@
|
|||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^51.0.1",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"exiftool-vendored": "^24.5.0",
|
||||
"luxon": "^3.4.4",
|
||||
"pg": "^8.11.3",
|
||||
|
@ -43,6 +43,7 @@
|
|||
"socket.io-client": "^4.7.4",
|
||||
"supertest": "^6.3.4",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^1.3.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -148,7 +148,7 @@ describe('/activity', () => {
|
|||
});
|
||||
|
||||
it('should filter by userId', async () => {
|
||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
|
||||
|
||||
const response1 = await request(app)
|
||||
.get('/activity')
|
||||
|
@ -250,8 +250,7 @@ describe('/activity', () => {
|
|||
});
|
||||
|
||||
it('should return a 200 for a duplicate like on the album', async () => {
|
||||
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
|
||||
|
||||
const reaction = await createActivity({ albumId: album.id, type: ReactionType.Like });
|
||||
const { status, body } = await request(app)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
|
@ -261,13 +260,11 @@ describe('/activity', () => {
|
|||
});
|
||||
|
||||
it('should not confuse an album like with an asset like', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.Like,
|
||||
}),
|
||||
]);
|
||||
const reaction = await createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.Like,
|
||||
});
|
||||
const { status, body } = await request(app)
|
||||
.post('/activity')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
|
@ -314,13 +311,11 @@ describe('/activity', () => {
|
|||
});
|
||||
|
||||
it('should return a 200 for a duplicate like on an asset', async () => {
|
||||
const [reaction] = await Promise.all([
|
||||
createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.Like,
|
||||
}),
|
||||
]);
|
||||
const reaction = await createActivity({
|
||||
albumId: album.id,
|
||||
assetId: asset.id,
|
||||
type: ReactionType.Like,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post('/activity')
|
||||
|
|
|
@ -4,7 +4,9 @@ import {
|
|||
AssetOrder,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
addAssetsToAlbum,
|
||||
deleteUser,
|
||||
getAlbumInfo,
|
||||
} from '@immich/sdk';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
|
@ -65,7 +67,6 @@ describe('/album', () => {
|
|||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
sharedWithUserIds: [user1.userId],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
|
@ -77,6 +78,13 @@ describe('/album', () => {
|
|||
}),
|
||||
]);
|
||||
|
||||
await addAssetsToAlbum(
|
||||
{ id: albums[3].id, bulkIdsDto: { ids: [user1Asset1.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
);
|
||||
|
||||
albums[3] = await getAlbumInfo({ id: albums[3].id }, { headers: asBearerAuth(user2.accessToken) });
|
||||
|
||||
user1Albums = albums.slice(0, 3);
|
||||
user2Albums = albums.slice(3, 6);
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ describe('/asset', () => {
|
|||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
user2Assets = await Promise.all([utils.createAsset(user2.accessToken)]);
|
||||
user2Assets = [await utils.createAsset(user2.accessToken)];
|
||||
|
||||
await Promise.all([
|
||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||
|
@ -816,15 +816,15 @@ describe('/asset', () => {
|
|||
});
|
||||
|
||||
it('should not include gps data for webp thumbnails', async () => {
|
||||
const { status, body, type } = await request(app)
|
||||
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
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');
|
||||
|
|
|
@ -6,12 +6,13 @@ import {
|
|||
getAllLibraries,
|
||||
scanLibrary,
|
||||
} from '@immich/sdk';
|
||||
import { existsSync, rmdirSync } from 'node:fs';
|
||||
import { cpSync, existsSync } from 'node:fs';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { userDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { utimes } from 'utimes';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const scan = async (accessToken: string, id: string, dto: ScanLibraryDto = {}) =>
|
||||
|
@ -26,23 +27,21 @@ describe('/library', () => {
|
|||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
await utils.resetAdminConfig(admin.accessToken);
|
||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
utils.disconnectWebsocket(websocket);
|
||||
utils.resetTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
utils.resetEvents();
|
||||
const tempDir = `${testAssetDir}/temp`;
|
||||
if (existsSync(tempDir)) {
|
||||
rmdirSync(tempDir, { recursive: true });
|
||||
}
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryB/assetB.png`);
|
||||
});
|
||||
|
||||
describe('GET /library', () => {
|
||||
|
@ -357,95 +356,6 @@ describe('/library', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('DELETE /library/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
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 () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an external library with assets', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// ensure no files get deleted
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /library/:id/statistics', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
|
||||
|
@ -549,6 +459,150 @@ describe('/library', () => {
|
|||
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(newAssets.count).toBe(3);
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
});
|
||||
|
||||
it('should offline missing files', async () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
isOffline: true,
|
||||
originalFileName: 'assetB.png',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should scan new files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.items).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
originalFileName: 'assetC.png',
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
describe('with refreshModifiedFiles=true', () => {
|
||||
it('should reimport modified files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(1);
|
||||
});
|
||||
|
||||
it('should not reimport unmodified files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshModifiedFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with refreshAllFiles=true', () => {
|
||||
it('should reimport all files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id, { refreshAllFiles: true });
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
model: 'NIKON D750',
|
||||
});
|
||||
expect(assets.count).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -559,6 +613,72 @@ describe('/library', () => {
|
|||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should remove offline files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
});
|
||||
expect(initialAssets.count).toBe(3);
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
isOffline: true,
|
||||
});
|
||||
expect(offlineAssets.count).toBe(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/library/${library.id}/removeOffline`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets.count).toBe(2);
|
||||
});
|
||||
|
||||
it('should not remove online files', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets: assetsBefore } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/library/${library.id}/removeOffline`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
|
||||
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
expect(assets).toEqual(assetsBefore);
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /library/:id/validate', () => {
|
||||
|
@ -608,4 +728,93 @@ describe('/library', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /library/:id', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
|
||||
|
||||
expect(status).toBe(401);
|
||||
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 () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
});
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should delete an external library with assets', async () => {
|
||||
const library = await utils.createLibrary(admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.External,
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 2 });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/library/${library.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(204);
|
||||
expect(body).toEqual({});
|
||||
|
||||
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(libraries).not.toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: library.id,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
|
||||
// ensure no files get deleted
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryA/assetA.png`)).toBe(true);
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
|
@ -32,6 +32,9 @@ describe('/search', () => {
|
|||
let assetGlarus: AssetFileUploadResponseDto;
|
||||
let assetSprings: AssetFileUploadResponseDto;
|
||||
let assetLast: AssetFileUploadResponseDto;
|
||||
let cities: string[];
|
||||
let states: string[];
|
||||
let countries: string[];
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
@ -79,7 +82,7 @@ describe('/search', () => {
|
|||
}
|
||||
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const cities = [
|
||||
const coordinates = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 63.0695, longitude: -151.0074 }, // denali
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
|
@ -101,7 +104,7 @@ describe('/search', () => {
|
|||
];
|
||||
|
||||
const updates = assets.map((asset, i) =>
|
||||
updateAsset({ id: asset.id, updateAssetDto: cities[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
|
@ -133,6 +136,12 @@ describe('/search', () => {
|
|||
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
|
||||
|
||||
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
|
||||
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
|
||||
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
|
||||
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
|
||||
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
|
||||
}, 30_000);
|
||||
|
||||
afterAll(async () => {
|
||||
|
@ -452,21 +461,7 @@ describe('/search', () => {
|
|||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=country')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Cuba',
|
||||
'France',
|
||||
'Georgia',
|
||||
'Germany',
|
||||
'Ghana',
|
||||
'Japan',
|
||||
'Morocco',
|
||||
"People's Republic of China",
|
||||
'Russian Federation',
|
||||
'Singapore',
|
||||
'Spain',
|
||||
'Switzerland',
|
||||
'United States of America',
|
||||
]);
|
||||
expect(body).toEqual(countries);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
|
@ -474,23 +469,7 @@ describe('/search', () => {
|
|||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=state')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Accra, Greater Accra',
|
||||
'Berlin',
|
||||
'Glarus, Glarus',
|
||||
'Havana',
|
||||
'Marrakech, Marrakesh-Safi',
|
||||
'Mesa County, Colorado',
|
||||
'Neshoba County, Mississippi',
|
||||
'New York',
|
||||
'Page County, Virginia',
|
||||
'Paris, Île-de-France',
|
||||
'Province of Córdoba, Andalusia',
|
||||
'Shanghai Municipality, Shanghai',
|
||||
'St.-Petersburg',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(body).toEqual(states);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
|
@ -498,24 +477,7 @@ describe('/search', () => {
|
|||
const { status, body } = await request(app)
|
||||
.get('/search/suggestions?type=city')
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(body).toEqual([
|
||||
'Accra',
|
||||
'Berlin',
|
||||
'Glarus',
|
||||
'Havana',
|
||||
'Marrakesh',
|
||||
'Montalbán de Córdoba',
|
||||
'New York City',
|
||||
'Palisade',
|
||||
'Paris',
|
||||
'Philadelphia',
|
||||
'Saint Petersburg',
|
||||
'Shanghai',
|
||||
'Singapore',
|
||||
'Stanley',
|
||||
'Tbilisi',
|
||||
'Tokyo',
|
||||
]);
|
||||
expect(body).toEqual(cities);
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { LoginResponseDto, getAllAlbums, getAllAssets } from '@immich/sdk';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
|
||||
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
@ -23,7 +24,7 @@ describe(`immich upload`, () => {
|
|||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
|
@ -35,7 +36,7 @@ describe(`immich upload`, () => {
|
|||
const first = await immichCli(['upload', `${testAssetDir}/albums/nature/silver_fir.jpg`]);
|
||||
expect(first.stderr).toBe('');
|
||||
expect(first.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 asset')]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 1 new asset')]),
|
||||
);
|
||||
expect(first.exitCode).toBe(0);
|
||||
|
||||
|
@ -69,7 +70,7 @@ describe(`immich upload`, () => {
|
|||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
|
@ -88,7 +89,7 @@ describe(`immich upload`, () => {
|
|||
]);
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully uploaded 9 new assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
|
@ -107,7 +108,7 @@ describe(`immich upload`, () => {
|
|||
it('should add existing assets to albums', async () => {
|
||||
const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
|
||||
expect(response1.stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 new assets')]),
|
||||
);
|
||||
expect(response1.stderr).toBe('');
|
||||
expect(response1.exitCode).toBe(0);
|
||||
|
@ -147,7 +148,7 @@ describe(`immich upload`, () => {
|
|||
]);
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully uploaded 9 new assets'),
|
||||
expect.stringContaining('Successfully created 1 new album'),
|
||||
expect.stringContaining('Successfully updated 9 assets'),
|
||||
]),
|
||||
|
@ -180,7 +181,7 @@ describe(`immich upload`, () => {
|
|||
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.stringContaining('Successfully uploaded 9 assets'),
|
||||
expect.stringContaining('Successfully uploaded 9 new assets'),
|
||||
expect.stringContaining('Deleting assets that have been uploaded'),
|
||||
]),
|
||||
);
|
||||
|
@ -192,6 +193,32 @@ describe(`immich upload`, () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('immich upload --skip-hash', () => {
|
||||
it('should skip hashing', async () => {
|
||||
const filename = `albums/nature/silver_fir.jpg`;
|
||||
await utils.createAsset(admin.accessToken, {
|
||||
assetData: {
|
||||
bytes: readFileSync(`${testAssetDir}/${filename}`),
|
||||
filename: 'silver_fit.jpg',
|
||||
},
|
||||
});
|
||||
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/${filename}`, '--skip-hash']);
|
||||
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([
|
||||
'Skipping hash check, assuming all files are new',
|
||||
expect.stringContaining('Successfully uploaded 0 new assets'),
|
||||
expect.stringContaining('Skipped 1 duplicate asset'),
|
||||
]),
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
const assets = await getAllAssets({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('immich upload --concurrency <number>', () => {
|
||||
it('should work', async () => {
|
||||
const { stderr, stdout, exitCode } = await immichCli([
|
||||
|
@ -203,7 +230,10 @@ describe(`immich upload`, () => {
|
|||
|
||||
expect(stderr).toBe('');
|
||||
expect(stdout.split('\n')).toEqual(
|
||||
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
|
||||
expect.arrayContaining([
|
||||
'Found 9 new files and 0 duplicates',
|
||||
expect.stringContaining('Successfully uploaded 9 new assets'),
|
||||
]),
|
||||
);
|
||||
expect(exitCode).toBe(0);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { exec, spawn } from 'node:child_process';
|
||||
import { setTimeout } from 'node:timers';
|
||||
|
||||
export default async () => {
|
||||
const setup = async () => {
|
||||
let _resolve: () => unknown;
|
||||
let _reject: (error: Error) => unknown;
|
||||
|
||||
|
@ -31,3 +31,5 @@ export default async () => {
|
|||
await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
|
||||
};
|
||||
};
|
||||
|
||||
export default setup;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import {
|
||||
AllJobStatusResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetResponseDto,
|
||||
CreateAlbumDto,
|
||||
|
@ -18,11 +19,14 @@ import {
|
|||
defaults,
|
||||
deleteAssets,
|
||||
getAllAssets,
|
||||
getAllJobsStatus,
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
login,
|
||||
searchMetadata,
|
||||
setAdminOnboarding,
|
||||
signUpAdmin,
|
||||
updateConfig,
|
||||
validate,
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
|
@ -31,6 +35,7 @@ import { createHash } from 'node:crypto';
|
|||
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import path, { dirname } from 'node:path';
|
||||
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
|
||||
import { promisify } from 'node:util';
|
||||
import pg from 'pg';
|
||||
import { io, type Socket } from 'socket.io-client';
|
||||
|
@ -136,6 +141,7 @@ export const utils = {
|
|||
'user_token',
|
||||
'users',
|
||||
'system_metadata',
|
||||
'system_config',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
|
@ -145,7 +151,12 @@ export const utils = {
|
|||
}
|
||||
|
||||
for (const table of tables) {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(sql.join('\n'));
|
||||
|
@ -209,35 +220,33 @@ export const utils = {
|
|||
}
|
||||
},
|
||||
|
||||
waitForWebsocketEvent: async ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
if (!id && !count) {
|
||||
throw new Error('id or count must be provided for waitForWebsocketEvent');
|
||||
}
|
||||
|
||||
const type = id ? `id=${id}` : `count=${count}`;
|
||||
console.log(`Waiting for ${event} [${type}]`);
|
||||
const set = events[event];
|
||||
if ((id && set.has(id)) || (count && set.size >= count)) {
|
||||
return;
|
||||
}
|
||||
|
||||
waitForWebsocketEvent: ({ event, id, total: count, timeout: ms }: WaitOptions): Promise<void> => {
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
if (!id && !count) {
|
||||
reject(new Error('id or count must be provided for waitForWebsocketEvent'));
|
||||
}
|
||||
|
||||
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
|
||||
const type = id ? `id=${id}` : `count=${count}`;
|
||||
console.log(`Waiting for ${event} [${type}]`);
|
||||
const set = events[event];
|
||||
const onId = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
if ((id && set.has(id)) || (count && set.size >= count)) {
|
||||
onId();
|
||||
return;
|
||||
}
|
||||
|
||||
if (id) {
|
||||
idCallbacks[id] = () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
};
|
||||
idCallbacks[id] = onId;
|
||||
}
|
||||
|
||||
if (count) {
|
||||
countCallbacks[event] = {
|
||||
count,
|
||||
callback: () => {
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
},
|
||||
callback: onId,
|
||||
};
|
||||
}
|
||||
});
|
||||
|
@ -309,9 +318,7 @@ export const utils = {
|
|||
if (!existsSync(dirname(path))) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
}
|
||||
if (!existsSync(path)) {
|
||||
writeFileSync(path, makeRandomImage());
|
||||
}
|
||||
writeFileSync(path, makeRandomImage());
|
||||
},
|
||||
|
||||
removeImageFile: (path: string) => {
|
||||
|
@ -406,6 +413,39 @@ export const utils = {
|
|||
},
|
||||
]),
|
||||
|
||||
resetTempFolder: () => {
|
||||
rmSync(`${testAssetDir}/temp`, { recursive: true, force: true });
|
||||
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
||||
},
|
||||
|
||||
resetAdminConfig: async (accessToken: string) => {
|
||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
|
||||
const queues = await getAllJobsStatus({ headers: asBearerAuth(accessToken) });
|
||||
const jobCounts = queues[queue].jobCounts;
|
||||
return !jobCounts.active && !jobCounts.waiting;
|
||||
},
|
||||
|
||||
waitForQueueFinish: (accessToken: string, queue: keyof AllJobStatusResponseDto, ms?: number) => {
|
||||
return new Promise<void>(async (resolve, reject) => {
|
||||
const timeout = setTimeout(() => reject(new Error('Timed out waiting for queue to empty')), ms || 10_000);
|
||||
|
||||
while (true) {
|
||||
const done = await utils.isQueueEmpty(accessToken, queue);
|
||||
if (done) {
|
||||
break;
|
||||
}
|
||||
await setAsyncTimeout(200);
|
||||
}
|
||||
|
||||
clearTimeout(timeout);
|
||||
resolve();
|
||||
});
|
||||
},
|
||||
|
||||
cliLogin: async (accessToken: string) => {
|
||||
const key = await utils.createApiKey(accessToken);
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
|
|
|
@ -12,7 +12,7 @@ export default defineConfig({
|
|||
test: {
|
||||
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
|
||||
globalSetup,
|
||||
testTimeout: 10_000,
|
||||
testTimeout: 15_000,
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
|
|
80
install.sh
|
@ -1,62 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
set -o nounset
|
||||
set -o pipefail
|
||||
|
||||
echo "Starting Immich installation..."
|
||||
|
||||
ip_address=$(hostname -I | awk '{print $1}')
|
||||
|
||||
create_immich_directory() {
|
||||
create_immich_directory() { local -r Tgt='./immich-app'
|
||||
echo "Creating Immich directory..."
|
||||
mkdir -p ./immich-app
|
||||
cd ./immich-app || exit
|
||||
if [[ -e $Tgt ]]; then
|
||||
echo "Found existing directory $Tgt, will overwrite YAML files"
|
||||
else
|
||||
mkdir "$Tgt" || return
|
||||
fi
|
||||
cd "$Tgt" || return
|
||||
}
|
||||
|
||||
download_docker_compose_file() {
|
||||
echo "Downloading docker-compose.yml..."
|
||||
curl -L https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml -o ./docker-compose.yml >/dev/null 2>&1
|
||||
"${Curl[@]}" "$RepoUrl"/docker-compose.yml -o ./docker-compose.yml
|
||||
}
|
||||
|
||||
download_dot_env_file() {
|
||||
echo "Downloading .env file..."
|
||||
curl -L https://github.com/immich-app/immich/releases/latest/download/example.env -o ./.env >/dev/null 2>&1
|
||||
"${Curl[@]}" "$RepoUrl"/example.env -o ./.env
|
||||
}
|
||||
|
||||
start_docker_compose() {
|
||||
echo "Starting Immich's docker containers"
|
||||
|
||||
if docker compose >/dev/null 2>&1; then
|
||||
docker_bin="docker compose"
|
||||
elif docker-compose >/dev/null 2>&1; then
|
||||
docker_bin="docker-compose"
|
||||
else
|
||||
echo "Cannot find \`docker compose\` or \`docker-compose\`."
|
||||
exit 1
|
||||
if ! docker compose >/dev/null 2>&1; then
|
||||
echo "failed to find 'docker compose'"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if $docker_bin up --remove-orphans -d; then
|
||||
show_friendly_message
|
||||
exit 0
|
||||
else
|
||||
if ! docker compose up --remove-orphans -d; then
|
||||
echo "Could not start. Check for errors above."
|
||||
exit 1
|
||||
return 1
|
||||
fi
|
||||
show_friendly_message
|
||||
}
|
||||
|
||||
show_friendly_message() {
|
||||
echo "Successfully deployed Immich!"
|
||||
echo "You can access the website at http://$ip_address:2283 and the server URL for the mobile app is http://$ip_address:2283/api"
|
||||
echo "---------------------------------------------------"
|
||||
echo "If you want to configure custom information of the server, including the database, Redis information, or the backup (or upload) location, etc.
|
||||
local ip_address
|
||||
ip_address=$(hostname -I | awk '{print $1}')
|
||||
cat << EOF
|
||||
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
|
||||
---------------------------------------------------
|
||||
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,
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
# MAIN
|
||||
create_immich_directory
|
||||
download_docker_compose_file
|
||||
download_dot_env_file
|
||||
start_docker_compose
|
||||
main() {
|
||||
echo "Starting Immich installation..."
|
||||
local -r RepoUrl='https://github.com/immich-app/immich/releases/latest/download'
|
||||
local -a Curl
|
||||
if command -v curl >/dev/null; then
|
||||
Curl=(curl -fsSL)
|
||||
else
|
||||
echo 'no curl binary found; please install curl and try again'
|
||||
return 14
|
||||
fi
|
||||
|
||||
create_immich_directory || { echo 'error creating Immich directory'; return 10; }
|
||||
download_docker_compose_file || { echo 'error downloading Docker Compose file'; return 11; }
|
||||
download_dot_env_file || { echo 'error downloading .env'; return 12; }
|
||||
start_docker_compose || { echo 'error starting Docker'; return 13; }
|
||||
return 0; }
|
||||
|
||||
main
|
||||
Exit=$?
|
||||
[[ $Exit == 0 ]] || echo "There was an error installing Immich. Exit code: $Exit. Please provide these logs when asking for assistance."
|
||||
exit "$Exit"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
ARG DEVICE=cpu
|
||||
|
||||
FROM python:3.11-bookworm@sha256:e2ed446c899827ed992f8a5a8875fa0853fcab32581e61418b650322061aa3c4 as builder-cpu
|
||||
FROM python:3.11-bookworm@sha256:abe34d06fca0165d40375b0e840fd3296ad2d075954d3a400d4efefe0e9b3012 as builder-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
|
||||
USER root
|
||||
|
@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv
|
|||
COPY poetry.lock pyproject.toml ./
|
||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||
|
||||
FROM python:3.11-slim-bookworm@sha256:90f8795536170fd08236d2ceb74fe7065dbf74f738d8b84bfbf263656654dc9b as prod-cpu
|
||||
FROM python:3.11-slim-bookworm@sha256:dad770592ab3582ab2dabcf0e18a863df9d86bd9d23efcfa614110ce49ac20e4 as prod-cpu
|
||||
|
||||
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
|
||||
USER root
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
This project uses [Poetry](https://python-poetry.org/docs/#installation), so be sure to install it first.
|
||||
Running `poetry install --no-root --with dev --with cpu` will install everything you need in an isolated virtual environment.
|
||||
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`.
|
||||
CUDA and OpenVINO are supported as acceleration APIs. To use them, you can replace `--with cpu` with either of `--with cuda` or `--with openvino`. In the case of CUDA, a [compute capability](https://developer.nvidia.com/cuda-gpus) of 5.2 or higher is required.
|
||||
|
||||
To add or remove dependencies, you can use the commands `poetry add $PACKAGE_NAME` and `poetry remove $PACKAGE_NAME`, respectively.
|
||||
Be sure to commit the `poetry.lock` and `pyproject.toml` files with `poetry lock --no-update` to reflect any changes in dependencies.
|
||||
|
|
|
@ -7,9 +7,9 @@ _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in
|
|||
|
||||
def resize(img: Image.Image, size: int) -> Image.Image:
|
||||
if img.width < img.height:
|
||||
return img.resize((size, int((img.height / img.width) * size)), resample=Image.BICUBIC)
|
||||
return img.resize((size, int((img.height / img.width) * size)), resample=Image.Resampling.BICUBIC)
|
||||
else:
|
||||
return img.resize((int((img.width / img.height) * size), size), resample=Image.BICUBIC)
|
||||
return img.resize((int((img.width / img.height) * size), size), resample=Image.Resampling.BICUBIC)
|
||||
|
||||
|
||||
# https://stackoverflow.com/a/60883103
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM mambaorg/micromamba:bookworm-slim@sha256:3624db3aee11d2f3f00d25f691aaaf8834b8bc4ec1b340dcdb48ef37281ea604 as builder
|
||||
FROM mambaorg/micromamba:bookworm-slim@sha256:4de614588f7b3d6598a6cd571297157b57ac85b1a8ff854cf10231bb0c50e3ac as builder
|
||||
|
||||
ENV NODE_ENV=production \
|
||||
TRANSFORMERS_CACHE=/cache \
|
||||
|
|
263
machine-learning/poetry.lock
generated
|
@ -680,18 +680,18 @@ test = ["pytest (>=6)"]
|
|||
|
||||
[[package]]
|
||||
name = "fastapi"
|
||||
version = "0.110.0"
|
||||
version = "0.110.1"
|
||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"},
|
||||
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
|
||||
{file = "fastapi-0.110.1-py3-none-any.whl", hash = "sha256:5df913203c482f820d31f48e635e022f8cbfe7350e4830ef05a3163925b1addc"},
|
||||
{file = "fastapi-0.110.1.tar.gz", hash = "sha256:6feac43ec359dfe4f45b2c18ec8c94edb8dc2dfc461d417d9e626590c071baad"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
|
||||
starlette = ">=0.36.3,<0.37.0"
|
||||
starlette = ">=0.37.2,<0.38.0"
|
||||
typing-extensions = ">=4.8.0"
|
||||
|
||||
[package.extras]
|
||||
|
@ -2192,79 +2192,80 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pillow"
|
||||
version = "10.2.0"
|
||||
version = "10.3.0"
|
||||
description = "Python Imaging Library (Fork)"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
|
||||
{file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
|
||||
{file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
|
||||
{file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
|
||||
{file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
|
||||
{file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
|
||||
{file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
|
||||
{file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
|
||||
{file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
|
||||
{file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
|
||||
{file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
|
||||
{file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
|
||||
{file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
|
||||
{file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
|
||||
{file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
|
||||
{file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
|
||||
{file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
|
||||
{file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
@ -2383,47 +2384,47 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "1.10.14"
|
||||
version = "1.10.15"
|
||||
description = "Data validation and settings management using python type hints"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "pydantic-1.10.14-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7f4fcec873f90537c382840f330b90f4715eebc2bc9925f04cb92de593eae054"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e3a76f571970fcd3c43ad982daf936ae39b3e90b8a2e96c04113a369869dc87"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d886bd3c3fbeaa963692ef6b643159ccb4b4cefaf7ff1617720cbead04fd1d"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:798a3d05ee3b71967844a1164fd5bdb8c22c6d674f26274e78b9f29d81770c4e"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:23d47a4b57a38e8652bcab15a658fdb13c785b9ce217cc3a729504ab4e1d6bc9"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f9f674b5c3bebc2eba401de64f29948ae1e646ba2735f884d1594c5f675d6f2a"},
|
||||
{file = "pydantic-1.10.14-cp310-cp310-win_amd64.whl", hash = "sha256:24a7679fab2e0eeedb5a8924fc4a694b3bcaac7d305aeeac72dd7d4e05ecbebf"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9d578ac4bf7fdf10ce14caba6f734c178379bd35c486c6deb6f49006e1ba78a7"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fa7790e94c60f809c95602a26d906eba01a0abee9cc24150e4ce2189352deb1b"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aad4e10efa5474ed1a611b6d7f0d130f4aafadceb73c11d9e72823e8f508e663"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1245f4f61f467cb3dfeced2b119afef3db386aec3d24a22a1de08c65038b255f"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:21efacc678a11114c765eb52ec0db62edffa89e9a562a94cbf8fa10b5db5c046"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:412ab4a3f6dbd2bf18aefa9f79c7cca23744846b31f1d6555c2ee2b05a2e14ca"},
|
||||
{file = "pydantic-1.10.14-cp311-cp311-win_amd64.whl", hash = "sha256:e897c9f35281f7889873a3e6d6b69aa1447ceb024e8495a5f0d02ecd17742a7f"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:d604be0f0b44d473e54fdcb12302495fe0467c56509a2f80483476f3ba92b33c"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a42c7d17706911199798d4c464b352e640cab4351efe69c2267823d619a937e5"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:596f12a1085e38dbda5cbb874d0973303e34227b400b6414782bf205cc14940c"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bfb113860e9288d0886e3b9e49d9cf4a9d48b441f52ded7d96db7819028514cc"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bc3ed06ab13660b565eed80887fcfbc0070f0aa0691fbb351657041d3e874efe"},
|
||||
{file = "pydantic-1.10.14-cp37-cp37m-win_amd64.whl", hash = "sha256:ad8c2bc677ae5f6dbd3cf92f2c7dc613507eafe8f71719727cbc0a7dec9a8c01"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c37c28449752bb1f47975d22ef2882d70513c546f8f37201e0fec3a97b816eee"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:49a46a0994dd551ec051986806122767cf144b9702e31d47f6d493c336462597"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53e3819bd20a42470d6dd0fe7fc1c121c92247bca104ce608e609b59bc7a77ee"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0fbb503bbbbab0c588ed3cd21975a1d0d4163b87e360fec17a792f7d8c4ff29f"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:336709883c15c050b9c55a63d6c7ff09be883dbc17805d2b063395dd9d9d0022"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:4ae57b4d8e3312d486e2498d42aed3ece7b51848336964e43abbf9671584e67f"},
|
||||
{file = "pydantic-1.10.14-cp38-cp38-win_amd64.whl", hash = "sha256:dba49d52500c35cfec0b28aa8b3ea5c37c9df183ffc7210b10ff2a415c125c4a"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:c66609e138c31cba607d8e2a7b6a5dc38979a06c900815495b2d90ce6ded35b4"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d986e115e0b39604b9eee3507987368ff8148222da213cd38c359f6f57b3b347"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:646b2b12df4295b4c3148850c85bff29ef6d0d9621a8d091e98094871a62e5c7"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:282613a5969c47c83a8710cc8bfd1e70c9223feb76566f74683af889faadc0ea"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:466669501d08ad8eb3c4fecd991c5e793c4e0bbd62299d05111d4f827cded64f"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:13e86a19dca96373dcf3190fcb8797d40a6f12f154a244a8d1e8e03b8f280593"},
|
||||
{file = "pydantic-1.10.14-cp39-cp39-win_amd64.whl", hash = "sha256:08b6ec0917c30861e3fe71a93be1648a2aa4f62f866142ba21670b24444d7fd8"},
|
||||
{file = "pydantic-1.10.14-py3-none-any.whl", hash = "sha256:8ee853cd12ac2ddbf0ecbac1c289f95882b2d4482258048079d13be700aa114c"},
|
||||
{file = "pydantic-1.10.14.tar.gz", hash = "sha256:46f17b832fe27de7850896f3afee50ea682220dd218f7e9c88d436788419dca6"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"},
|
||||
{file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"},
|
||||
{file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"},
|
||||
{file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"},
|
||||
{file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"},
|
||||
{file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"},
|
||||
{file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"},
|
||||
{file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
@ -2846,28 +2847,28 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.3.4"
|
||||
version = "0.3.7"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
files = [
|
||||
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:60c870a7d46efcbc8385d27ec07fe534ac32f3b251e4fc44b3cbfd9e09609ef4"},
|
||||
{file = "ruff-0.3.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6fc14fa742e1d8f24910e1fff0bd5e26d395b0e0e04cc1b15c7c5e5fe5b4af91"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3ee7880f653cc03749a3bfea720cf2a192e4f884925b0cf7eecce82f0ce5854"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cf133dd744f2470b347f602452a88e70dadfbe0fcfb5fd46e093d55da65f82f7"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3f3860057590e810c7ffea75669bdc6927bfd91e29b4baa9258fd48b540a4365"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:986f2377f7cf12efac1f515fc1a5b753c000ed1e0a6de96747cdf2da20a1b369"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c4fd98e85869603e65f554fdc5cddf0712e352fe6e61d29d5a6fe087ec82b76c"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64abeed785dad51801b423fa51840b1764b35d6c461ea8caef9cf9e5e5ab34d9"},
|
||||
{file = "ruff-0.3.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df52972138318bc7546d92348a1ee58449bc3f9eaf0db278906eb511889c4b50"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:98e98300056445ba2cc27d0b325fd044dc17fcc38e4e4d2c7711585bd0a958ed"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:519cf6a0ebed244dce1dc8aecd3dc99add7a2ee15bb68cf19588bb5bf58e0488"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:bb0acfb921030d00070539c038cd24bb1df73a2981e9f55942514af8b17be94e"},
|
||||
{file = "ruff-0.3.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cf187a7e7098233d0d0c71175375c5162f880126c4c716fa28a8ac418dcf3378"},
|
||||
{file = "ruff-0.3.4-py3-none-win32.whl", hash = "sha256:af27ac187c0a331e8ef91d84bf1c3c6a5dea97e912a7560ac0cef25c526a4102"},
|
||||
{file = "ruff-0.3.4-py3-none-win_amd64.whl", hash = "sha256:de0d5069b165e5a32b3c6ffbb81c350b1e3d3483347196ffdf86dc0ef9e37dd6"},
|
||||
{file = "ruff-0.3.4-py3-none-win_arm64.whl", hash = "sha256:6810563cc08ad0096b57c717bd78aeac888a1bfd38654d9113cb3dc4d3f74232"},
|
||||
{file = "ruff-0.3.4.tar.gz", hash = "sha256:f0f4484c6541a99862b693e13a151435a279b271cff20e37101116a21e2a1ad1"},
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0e8377cccb2f07abd25e84fc5b2cbe48eeb0fea9f1719cad7caedb061d70e5ce"},
|
||||
{file = "ruff-0.3.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:15a4d1cc1e64e556fa0d67bfd388fed416b7f3b26d5d1c3e7d192c897e39ba4b"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d28bdf3d7dc71dd46929fafeec98ba89b7c3550c3f0978e36389b5631b793663"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:379b67d4f49774ba679593b232dcd90d9e10f04d96e3c8ce4a28037ae473f7bb"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c060aea8ad5ef21cdfbbe05475ab5104ce7827b639a78dd55383a6e9895b7c51"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:ebf8f615dde968272d70502c083ebf963b6781aacd3079081e03b32adfe4d58a"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d48098bd8f5c38897b03604f5428901b65e3c97d40b3952e38637b5404b739a2"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da8a4fda219bf9024692b1bc68c9cff4b80507879ada8769dc7e985755d662ea"},
|
||||
{file = "ruff-0.3.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c44e0149f1d8b48c4d5c33d88c677a4aa22fd09b1683d6a7ff55b816b5d074f"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3050ec0af72b709a62ecc2aca941b9cd479a7bf2b36cc4562f0033d688e44fa1"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a29cc38e4c1ab00da18a3f6777f8b50099d73326981bb7d182e54a9a21bb4ff7"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5b15cc59c19edca917f51b1956637db47e200b0fc5e6e1878233d3a938384b0b"},
|
||||
{file = "ruff-0.3.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e491045781b1e38b72c91247cf4634f040f8d0cb3e6d3d64d38dcf43616650b4"},
|
||||
{file = "ruff-0.3.7-py3-none-win32.whl", hash = "sha256:bc931de87593d64fad3a22e201e55ad76271f1d5bfc44e1a1887edd0903c7d9f"},
|
||||
{file = "ruff-0.3.7-py3-none-win_amd64.whl", hash = "sha256:5ef0e501e1e39f35e03c2acb1d1238c595b8bb36cf7a170e7c1df1b73da00e74"},
|
||||
{file = "ruff-0.3.7-py3-none-win_arm64.whl", hash = "sha256:789e144f6dc7019d1f92a812891c645274ed08af6037d11fc65fcbc183b7d59f"},
|
||||
{file = "ruff-0.3.7.tar.gz", hash = "sha256:d5c1aebee5162c2226784800ae031f660c350e7a3402c4d1f8ea4e97e232e3ba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3047,13 +3048,13 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "starlette"
|
||||
version = "0.36.3"
|
||||
version = "0.37.2"
|
||||
description = "The little ASGI library that shines."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"},
|
||||
{file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"},
|
||||
{file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"},
|
||||
{file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
|
@ -439,6 +439,8 @@
|
|||
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||
"shared_link_edit_expire_after_option_minute": "1 minute",
|
||||
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||
"shared_link_edit_expire_after_option_months": "{} months",
|
||||
"shared_link_edit_expire_after_option_year": "{} year",
|
||||
"shared_link_edit_expire_after_option_never": "Never",
|
||||
"shared_link_edit_password": "Password",
|
||||
"shared_link_edit_password_hint": "Enter the share password",
|
||||
|
@ -509,5 +511,7 @@
|
|||
"version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89",
|
||||
"viewer_remove_from_stack": "Remove from Stack",
|
||||
"viewer_stack_use_as_main_asset": "Use as Main Asset",
|
||||
"viewer_unstack": "Un-Stack"
|
||||
}
|
||||
"viewer_unstack": "Un-Stack",
|
||||
"haptic_feedback_title": "Haptic Feedback",
|
||||
"haptic_feedback_switch": "Enable haptic feedback"
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import 'package:immich_mobile/modules/asset_viewer/ui/gallery_app_bar.dart';
|
|||
import 'package:immich_mobile/modules/asset_viewer/views/video_viewer_page.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/shared/ui/photo_view/photo_view_gallery.dart';
|
||||
|
@ -303,7 +304,9 @@ class GalleryViewerPage extends HookConsumerWidget {
|
|||
scrollDirection: Axis.horizontal,
|
||||
onPageChanged: (value) async {
|
||||
final next = currentIndex.value < value ? value + 1 : value - 1;
|
||||
HapticFeedback.selectionClick();
|
||||
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
currentIndex.value = value;
|
||||
stackIndex.value = -1;
|
||||
isPlayingVideo.value = false;
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoCard extends HookConsumerWidget {
|
||||
|
@ -21,6 +21,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
|
||||
final bool isExcluded =
|
||||
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
|
||||
|
||||
final isDarkTheme = context.isDarkTheme;
|
||||
|
||||
ColorFilter selectedFilter = ColorFilter.mode(
|
||||
|
@ -78,7 +79,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
|
@ -87,7 +88,7 @@ class AlbumInfoCard extends HookConsumerWidget {
|
|||
}
|
||||
},
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
|
@ -8,6 +7,7 @@ import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
|||
import 'package:immich_mobile/modules/backup/models/available_album.model.dart';
|
||||
import 'package:immich_mobile/modules/backup/providers/backup.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_toast.dart';
|
||||
|
||||
class AlbumInfoListTile extends HookConsumerWidget {
|
||||
|
@ -68,7 +68,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||
|
||||
return GestureDetector(
|
||||
onDoubleTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.watch(hapticFeedbackProvider.notifier).selectionClick();
|
||||
|
||||
if (isExcluded) {
|
||||
// Remove from exclude album list
|
||||
|
@ -93,7 +93,7 @@ class AlbumInfoListTile extends HookConsumerWidget {
|
|||
tileColor: buildTileColor(),
|
||||
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||||
onTap: () {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
if (isSelected) {
|
||||
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
|
||||
} else {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/asset_grid_data_structure.dart';
|
||||
import 'package:immich_mobile/modules/settings/providers/app_settings.provider.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
|
||||
class GroupDividerTitle extends HookConsumerWidget {
|
||||
const GroupDividerTitle({
|
||||
|
@ -38,7 +38,7 @@ class GroupDividerTitle extends HookConsumerWidget {
|
|||
);
|
||||
|
||||
void handleTitleIconClick() {
|
||||
HapticFeedback.heavyImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
if (selected) {
|
||||
onDeselect();
|
||||
} else {
|
||||
|
|
|
@ -6,7 +6,7 @@ import 'package:collection/collection.dart';
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/rendering.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/extensions/collection_extensions.dart';
|
||||
import 'package:immich_mobile/modules/asset_viewer/providers/scroll_notifier.provider.dart';
|
||||
|
@ -15,6 +15,7 @@ import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_image.dart';
|
|||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/control_bottom_app_bar.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:scrollable_positioned_list/scrollable_positioned_list.dart';
|
||||
|
||||
import 'asset_grid_data_structure.dart';
|
||||
|
@ -27,7 +28,7 @@ typedef ImmichAssetGridSelectionListener = void Function(
|
|||
Set<Asset>,
|
||||
);
|
||||
|
||||
class ImmichAssetGridView extends StatefulWidget {
|
||||
class ImmichAssetGridView extends ConsumerStatefulWidget {
|
||||
final RenderList renderList;
|
||||
final int assetsPerRow;
|
||||
final double margin;
|
||||
|
@ -69,12 +70,12 @@ class ImmichAssetGridView extends StatefulWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
State<StatefulWidget> createState() {
|
||||
createState() {
|
||||
return ImmichAssetGridViewState();
|
||||
}
|
||||
}
|
||||
|
||||
class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
||||
class ImmichAssetGridViewState extends ConsumerState<ImmichAssetGridView> {
|
||||
final ItemScrollController _itemScrollController = ItemScrollController();
|
||||
final ScrollOffsetController _scrollOffsetController =
|
||||
ScrollOffsetController();
|
||||
|
@ -314,7 +315,7 @@ class ImmichAssetGridViewState extends State<ImmichAssetGridView> {
|
|||
final now = Timeline.now;
|
||||
if (now > (_hapticFeedbackTS + feedbackInterval)) {
|
||||
_hapticFeedbackTS = now;
|
||||
HapticFeedback.mediumImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,15 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_thumbnail.dart';
|
||||
import 'package:immich_mobile/utils/storage_indicator.dart';
|
||||
import 'package:isar/isar.dart';
|
||||
|
||||
class ThumbnailImage extends StatelessWidget {
|
||||
class ThumbnailImage extends ConsumerWidget {
|
||||
final Asset asset;
|
||||
final int index;
|
||||
final Asset Function(int index) loadAsset;
|
||||
|
@ -37,7 +38,7 @@ class ThumbnailImage extends StatelessWidget {
|
|||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final assetContainerColor = context.isDarkTheme
|
||||
? Colors.blueGrey
|
||||
: context.themeData.primaryColorLight;
|
||||
|
@ -186,7 +187,7 @@ class ThumbnailImage extends StatelessWidget {
|
|||
},
|
||||
onLongPress: () {
|
||||
onSelect?.call();
|
||||
HapticFeedback.heavyImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).heavyImpact();
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
|
|
|
@ -2,7 +2,6 @@ import 'dart:io';
|
|||
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_udid/flutter_udid.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/album/providers/album.provider.dart';
|
||||
|
@ -92,7 +91,6 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
|
|||
serverUrl: serverUrl,
|
||||
);
|
||||
} catch (e) {
|
||||
HapticFeedback.vibrate();
|
||||
debugPrint("Error logging in $e");
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import 'package:auto_route/auto_route.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/home/ui/asset_grid/thumbnail_placeholder.dart';
|
||||
import 'package:immich_mobile/modules/memories/providers/memory.provider.dart';
|
||||
import 'package:immich_mobile/routing/router.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
class MemoryLane extends HookConsumerWidget {
|
||||
|
@ -33,7 +33,9 @@ class MemoryLane extends HookConsumerWidget {
|
|||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
HapticFeedback.heavyImpact();
|
||||
ref
|
||||
.read(hapticFeedbackProvider.notifier)
|
||||
.heavyImpact();
|
||||
context.pushRoute(
|
||||
MemoryRoute(
|
||||
memories: memories,
|
||||
|
|
|
@ -9,6 +9,7 @@ import 'package:immich_mobile/modules/memories/ui/memory_card.dart';
|
|||
import 'package:immich_mobile/modules/memories/ui/memory_epilogue.dart';
|
||||
import 'package:immich_mobile/modules/memories/ui/memory_progress_indicator.dart';
|
||||
import 'package:immich_mobile/shared/models/asset.dart';
|
||||
import 'package:immich_mobile/shared/providers/haptic_feedback.provider.dart';
|
||||
import 'package:immich_mobile/shared/ui/immich_image.dart';
|
||||
|
||||
@RoutePage()
|
||||
|
@ -127,7 +128,7 @@ class MemoryPage extends HookConsumerWidget {
|
|||
}
|
||||
|
||||
Future<void> onAssetChanged(int otherIndex) async {
|
||||
HapticFeedback.selectionClick();
|
||||
ref.read(hapticFeedbackProvider.notifier).selectionClick();
|
||||
currentAssetPage.value = otherIndex;
|
||||
updateProgressText();
|
||||
// Wait for page change animation to finish
|
||||
|
@ -169,7 +170,7 @@ class MemoryPage extends HookConsumerWidget {
|
|||
scrollDirection: Axis.vertical,
|
||||
controller: memoryPageController,
|
||||
onPageChanged: (pageNumber) {
|
||||
HapticFeedback.mediumImpact();
|
||||
ref.read(hapticFeedbackProvider.notifier).mediumImpact();
|
||||
if (pageNumber < memories.length) {
|
||||
currentMemoryIndex.value = pageNumber;
|
||||
currentMemory.value = memories[pageNumber];
|
||||
|
|
|
@ -58,6 +58,7 @@ enum AppSettingsEnum<T> {
|
|||
null,
|
||||
false,
|
||||
),
|
||||
enableHapticFeedback<bool>(StoreKey.enableHapticFeedback, null, true),
|
||||
;
|
||||
|
||||
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
|
||||
|
|
81
mobile/lib/modules/settings/ui/language_settings.dart
Normal file
|
@ -0,0 +1,81 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/constants/locales.dart';
|
||||
import 'package:immich_mobile/extensions/build_context_extensions.dart';
|
||||
import 'package:immich_mobile/modules/backup/background_service/localization.dart';
|
||||
|
||||
class LanguageSettings extends HookConsumerWidget {
|
||||
const LanguageSettings({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentLocale = context.locale;
|
||||
final textController = useTextEditingController(
|
||||
text: locales.keys.firstWhere(
|
||||
(countryName) => locales[countryName] == currentLocale,
|
||||
),
|
||||
);
|
||||
|
||||
final selectedLocale = useState<Locale>(currentLocale);
|
||||
|
||||
return ListView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: [
|
||||
LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
return DropdownMenu(
|
||||
width: constraints.maxWidth,
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
contentPadding: const EdgeInsets.only(left: 16),
|
||||
),
|
||||
menuStyle: MenuStyle(
|
||||
shape: MaterialStatePropertyAll<OutlinedBorder>(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
),
|
||||
),
|
||||
backgroundColor: MaterialStatePropertyAll<Color>(
|
||||
context.isDarkTheme
|
||||
? Colors.grey[900]!
|
||||
: context.scaffoldBackgroundColor,
|
||||
),
|
||||
),
|
||||
menuHeight: context.height * 0.5,
|
||||
hintText: "Languages",
|
||||
label: const Text('Languages'),
|
||||
dropdownMenuEntries: locales.keys
|
||||
.map(
|
||||
(countryName) => DropdownMenuEntry(
|
||||
value: locales[countryName],
|
||||
label: countryName,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
controller: textController,
|
||||
onSelected: (value) {
|
||||
if (value != null) {
|
||||
selectedLocale.value = value;
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
ElevatedButton(
|
||||
onPressed: selectedLocale.value == currentLocale
|
||||
? null
|
||||
: () {
|
||||
context.setLocale(selectedLocale.value);
|
||||
loadTranslations();
|
||||
},
|
||||
child: const Text('setting_languages_apply').tr(),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:immich_mobile/modules/settings/services/app_settings.service.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_title.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_switch_list_tile.dart';
|
||||
import 'package:immich_mobile/modules/settings/utils/app_settings_update_hook.dart';
|
||||
|
||||
class HapticSetting extends HookConsumerWidget {
|
||||
const HapticSetting({
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final hapticFeedbackSetting =
|
||||
useAppSettingsState(AppSettingsEnum.enableHapticFeedback);
|
||||
final isHapticFeedbackEnabled =
|
||||
useValueNotifier(hapticFeedbackSetting.value);
|
||||
|
||||
onHapticFeedbackChange(bool isEnabled) {
|
||||
hapticFeedbackSetting.value = isEnabled;
|
||||
}
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SettingsSubTitle(title: "haptic_feedback_title".tr()),
|
||||
SettingsSwitchListTile(
|
||||
valueNotifier: isHapticFeedbackEnabled,
|
||||
title: 'haptic_feedback_switch'.tr(),
|
||||
onChanged: onHapticFeedbackChange,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import 'package:flutter/material.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/haptic_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/preference_settings/theme_setting.dart';
|
||||
import 'package:immich_mobile/modules/settings/ui/settings_sub_page_scaffold.dart';
|
||||
|
||||
|
@ -11,6 +12,7 @@ class PreferenceSetting extends StatelessWidget {
|
|||
Widget build(BuildContext context) {
|
||||
const preferenceSettings = [
|
||||
ThemeSetting(),
|
||||
HapticSetting(),
|
||||
];
|
||||
|
||||
return const SettingsSubPageScaffold(settings: preferenceSettings);
|
||||
|
|