Compare commits

...

35 Commits

Author SHA1 Message Date
Alex Tran
1cb5c617c8 wip: update 2024-12-28 23:09:32 -06:00
Alex Tran
c9e03003c0 wip: update 2024-12-28 23:09:24 -06:00
Alex Tran
677fbca7b5 merge main 2024-12-27 14:40:13 -06:00
mehring
34ce61d03a feat(web): create tag on the fly (#14726) 2024-12-27 17:29:57 +00:00
Lukas
0250a7a23a fix(web): Fix for failing to load pictures (#14943)
* attempt at fix for failing to load pictures

* comments

* remove unused files

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-12-27 16:16:07 +00:00
renovate[bot]
b91f39d1af chore(deps): update base-image to v20241224 (major) (#14905)
chore(deps): update base-image to v20241224

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-27 09:51:42 -06:00
Daniel Dietzler
139090715e fix: trusted proxies (#14888) 2024-12-27 09:51:07 -06:00
Sam Debruyn
05cea0fc69 chore(mobile): remove duplicate settingsservice (#14946)
remove duplicate settingsservice
2024-12-27 09:45:23 -06:00
Yaros
2255f3e966 feat(mobile): Modified draggable area of detail modal (#14953)
Modified draggable area of detail modal
2024-12-27 09:28:54 -06:00
Yaros
2be1cb7de2 fix(mobile): Fixed resolution format in Details (#14954)
Fixed resolution format on mobile
2024-12-27 09:20:07 -06:00
indam
227eb4b0a6 docs: Update Chinese README (#14926)
* Update Chinese README

* retrigger checks
2024-12-25 08:09:47 -05:00
bo0tzz
23461e98fb fix: clarify PR label validation message (#14925) 2024-12-25 08:07:52 -05:00
renovate[bot]
ef0070c3fd fix(deps): update machine-learning (#14891) 2024-12-23 20:04:55 -05:00
renovate[bot]
6b08e82cf7 fix(deps): update dependency @nestjs/swagger to v8 (#13881)
* fix(deps): update dependency @nestjs/swagger to v8

* chore: generate open api

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-12-23 21:03:34 +00:00
Ben
b88f98bf66 feat(web): Add "set as featured" option for an asset (#14879) 2024-12-23 12:26:53 -06:00
Jonathan Jogenfors
c3be74c450 fix(server): support import paths with special chars (#14856) 2024-12-22 16:22:16 -06:00
Jonathan Jogenfors
4bc2aa5451 feat(server): Handle sidecars in external libraries (#14800)
* handle sidecars in external libraries

* don't add separate source
2024-12-21 20:50:07 -06:00
Alex
6080e6e827 fix(web): infinite loop browser navigation crash admin settings page (#14850)
* fix(web): infinite loop browser navigation crash admin settings page

* pr feedback
2024-12-21 19:26:01 +00:00
Lukas
d5906c2efe feat(web): Adds toggle to disable sorting of faces (#14830)
* Allows for toggling of sorting in the merge face selector

* Adds toggle to the side panel for faces

* Improve layout and fix toggle

* chore: ui cleanup

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-12-21 16:53:37 +00:00
Matthew Momjian
b3821c50d7 chore(docs): FAQ cleanup (#14835)
* FAQ cleanup

* link fix

* copy paste error
2024-12-20 23:40:33 -06:00
Matthew Momjian
1cec3af98c chore(docs): fix broken link in FAQ (#14834)
broken link
2024-12-20 16:36:42 -06:00
Ben McCann
da70a1e457 chore: migrate to SvelteKit app state (#14807)
* chore: migrate to SvelteKit app state

* update package-lock.json
2024-12-20 17:18:22 -05:00
Alex
a14735846c chore(mobile): add timeout when reading video files (#14831) 2024-12-20 13:52:42 -06:00
Michel Heusschen
79a780e8d9 fix(web): downloading partner assets (#14803) 2024-12-19 14:56:16 +00:00
Slavik
556c59f12f docs: misspelled var "urls" in the "machineLearning" (#14786)
[docs] misspelled var "urls" in the "machineLearning"

Co-authored-by: slavik.fursov <slavik.fursov@oracle.com>
2024-12-18 19:57:25 -05:00
renovate[bot]
8e724c3c27 chore(deps): update base-image to v20241217 (major) (#14776)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 18:27:08 +00:00
renovate[bot]
207bb527bb chore(deps): update dependency exiftool-vendored to v28.8.0 (#13924)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 18:14:30 +00:00
Alex
b034cb1861 fix(mobile): keep empty album as selected target (#14778) 2024-12-18 11:10:29 -06:00
Daniel Dietzler
6a855f6331 chore: update deps (#14755) 2024-12-18 14:19:48 +00:00
renovate[bot]
a03f4f5610 chore(deps): update docker/setup-buildx-action action to v3.8.0 (#14757)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-18 14:18:21 +00:00
renovate[bot]
e0fc8734bf chore(deps): update dependency ruff to v0.8.3 (#14756)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-17 15:35:22 -05:00
renovate[bot]
0d060ebb32 chore(deps): update node (#14736)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-12-17 15:21:01 -05:00
Alex
f58b2572e6 chore: post release tasks (#14749) 2024-12-17 14:02:07 -06:00
Alex
f1577bc120 Merge branch 'main' of github.com:immich-app/immich into workflows 2024-09-15 23:12:14 -05:00
Alex Tran
b86e1c389e feat(web): workflows 2024-09-15 00:06:50 -05:00
98 changed files with 5947 additions and 14841 deletions

View File

@@ -59,7 +59,7 @@ jobs:
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3

View File

@@ -125,7 +125,7 @@ jobs:
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
@@ -216,7 +216,7 @@ jobs:
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.7.1
uses: docker/setup-buildx-action@v3.8.0
- name: Login to Docker Hub
# Only push to Docker Hub when making a release

View File

@@ -19,3 +19,4 @@ jobs:
use_regex: true
labels: "changelog:.*"
add_comment: true
message: "Label error. Requires {{errorString}} {{count}} of: {{ provided }}. Found: {{ applied }}. A maintainer will add the required label."

View File

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

119
cli/package-lock.json generated
View File

@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -34,7 +34,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.9.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
@@ -59,7 +59,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"typescript": "^5.3.3"
}
},
@@ -1397,13 +1397,13 @@
}
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.8"
"undici-types": "~6.20.0"
}
},
"node_modules/@types/normalize-package-data": {
@@ -1876,9 +1876,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true,
"funding": [
{
@@ -1894,11 +1894,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
@@ -1949,9 +1950,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001597",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
"integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
"version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
"integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
"dev": true,
"funding": [
{
@@ -1966,7 +1967,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.2",
@@ -2095,12 +2097,13 @@
"license": "MIT"
},
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0"
"browserslist": "^4.24.2"
},
"funding": {
"type": "opencollective",
@@ -2163,10 +2166,11 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.705",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.705.tgz",
"integrity": "sha512-LKqhpwJCLhYId2VVwEzFXWrqQI5n5zBppz1W9ehhTlfYU8CUUW6kClbN8LHF/v7flMgRdETS772nqywJ+ckVAw==",
"dev": true
"version": "1.5.74",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
"integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -2230,10 +2234,11 @@
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -2355,19 +2360,19 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "55.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
"integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
"version": "56.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
"integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@babel/helper-validator-identifier": "^7.24.7",
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.37.0",
"esquery": "^1.5.0",
"globals": "^15.7.0",
"core-js-compat": "^3.38.1",
"esquery": "^1.6.0",
"globals": "^15.9.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
"jsesc": "^3.0.2",
@@ -2375,7 +2380,7 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.6.1",
"semver": "^7.6.3",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -2503,10 +2508,11 @@
}
},
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -3253,10 +3259,11 @@
"dev": true
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/normalize-package-data": {
"version": "2.5.0",
@@ -3792,10 +3799,11 @@
}
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -4156,16 +4164,16 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@@ -4181,9 +4189,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"

View File

@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -30,7 +30,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.9.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",

View File

@@ -69,7 +69,8 @@ However, Immich will delete original files that have been trashed when the trash
### Why do my file names appear as a random string in the file manager?
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names.
To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
### Can I add my existing photo library?
@@ -82,11 +83,20 @@ 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 in front of Immich.
Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to allow large requests.
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.
If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed.
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
If you are having issues, we recommend switching to a different network deployment.
### 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 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.
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?
@@ -116,7 +126,8 @@ 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).
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 #9116](https://github.com/immich-app/immich/discussions/9116).
### What happens if an asset exists in more than one account?
@@ -308,7 +319,7 @@ Do not exaggerate with the job concurrency because you're probably thoroughly ov
### My server shows Server Status Offline | Version Unknown. What can I do?
You need to enable WebSockets on your reverse proxy.
You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your reverse proxy.
---
@@ -339,7 +350,7 @@ The non-root user/group needs read/write access to the volume mounts, including
The Docker Compose top level volume element does not support non-root access, all of the above volumes must be local volume mounts.
:::
For a further hardened system, you can add the following block to every container except for `immich_postgres`.
For a further hardened system, you can add the following block to every container.
<details>
<summary>docker-compose.yml</summary>
@@ -388,22 +399,21 @@ If the error says the worker is exiting, then this is normal. This is a feature
There are a few reasons why this can happen.
If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory. Consider either increasing the server's RAM or moving the service to a server with more RAM.
If the error mentions SIGKILL or error code 137, it most likely means the service is running out of memory.
Consider either increasing the server's RAM or moving the service to a server with more RAM.
If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible. This is unlikely to occur on version 1.92.0 or later. Consider upgrading if your version of Immich is below that.
If your version of Immich is below 1.92.0 and the crash occurs after logs about tracing or exporting a model, consider either upgrading or disabling the Tag Objects job.
If it mentions SIGILL (note the lack of a K) or error code 132, it most likely means your server's CPU is incompatible with Immich.
## Database
### Why am I getting database ownership errors?
If you get database errors such as `FATAL: data directory "/var/lib/postgresql/data" has wrong ownership` upon database startup, this is likely due to an issue with your filesystem.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/environment-variables#supported-filesystems) for more details.
NTFS and ex/FAT/32 filesystems are not supported. See [here](/docs/install/requirements#special-requirements-for-windows-users) for more details.
### How can I verify the integrity of my database?
If you installed Immich using v1.104.0 or later, you likely have database checksums enabled by default. You can check this by running the following command.
Database checksums are enabled by default for new installations since v1.104.0. You can check if they are enabled by running the following command.
A result of `on` means that checksums are enabled.
<details>
@@ -419,7 +429,7 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details>
If checksums are enabled, you can check the status of the database with the following command. A normal result is all zeroes.
If checksums are enabled, you can check the status of the database with the following command. A normal result is all `0`s.
<details>
<summary>Check for database corruption</summary>

View File

@@ -83,7 +83,7 @@ The default configuration looks like this:
},
"machineLearning": {
"enabled": true,
"url": ["http://immich-machine-learning:3003"],
"urls": ["http://immich-machine-learning:3003"],
"clip": {
"enabled": true,
"modelName": "ViT-B-32__openai"

View File

@@ -36,7 +36,6 @@
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.5.2",
"@tsconfig/docusaurus": "^2.0.2",
"prettier": "^3.2.4",
"typescript": "^5.1.6"
},

145
e2e/package-lock.json generated
View File

@@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -26,7 +26,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"exiftool-vendored": "^28.3.1",
"globals": "^15.9.0",
"jose": "^5.6.3",
@@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -74,7 +74,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.9.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
@@ -99,7 +99,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"typescript": "^5.3.3"
}
},
@@ -1658,13 +1658,13 @@
"dev": true
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.8"
"undici-types": "~6.20.0"
}
},
"node_modules/@types/normalize-package-data": {
@@ -2381,9 +2381,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true,
"funding": [
{
@@ -2399,11 +2399,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
@@ -2524,9 +2525,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001591",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001591.tgz",
"integrity": "sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==",
"version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
"integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
"dev": true,
"funding": [
{
@@ -2541,7 +2542,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/chai": {
"version": "5.1.2",
@@ -2757,12 +2759,13 @@
}
},
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0"
"browserslist": "^4.24.2"
},
"funding": {
"type": "opencollective",
@@ -2943,10 +2946,11 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.4.687",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.687.tgz",
"integrity": "sha512-Ic85cOuXSP6h7KM0AIJ2hpJ98Bo4hyTUjc4yjMbkvD+8yTxEhfK9+8exT2KKYsSjnCn2tGsKVSZwE7ZgTORQCw==",
"dev": true
"version": "1.5.74",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
"integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
"dev": true,
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@@ -3063,10 +3067,11 @@
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
@@ -3194,19 +3199,19 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "55.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
"integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
"version": "56.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
"integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@babel/helper-validator-identifier": "^7.24.7",
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.37.0",
"esquery": "^1.5.0",
"globals": "^15.7.0",
"core-js-compat": "^3.38.1",
"esquery": "^1.6.0",
"globals": "^15.9.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
"jsesc": "^3.0.2",
@@ -3214,7 +3219,7 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.6.1",
"semver": "^7.6.3",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -3311,10 +3316,11 @@
}
},
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -3377,9 +3383,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "28.7.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.7.0.tgz",
"integrity": "sha512-0zoq6kBS1yPjzJs+p0qZDinWEA72PTKoRk5ETYKfmeRcZAkhv83Y3HCpbb/LdgJJywfm8BcIJGezrBHvL7dVnQ==",
"version": "28.8.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.8.0.tgz",
"integrity": "sha512-R7tirJLr9fWuH9JS/KFFLB+O7jNGKuPXGxREc6YybYangEudGb+X8ERsYXk9AifMiAWh/2agNfbgkbcQcF+MxA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3390,14 +3396,14 @@
"luxon": "^3.5.0"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.99.0",
"exiftool-vendored.pl": "12.99.0"
"exiftool-vendored.exe": "13.0.0",
"exiftool-vendored.pl": "13.0.1"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.99.0.tgz",
"integrity": "sha512-ffpJHCzC9OYJqw4JlPNtCwRy02jwhmnSJEF/QqEjpuIWDEnlRBQP/yWRh1Nw21K1R4FB4yG5PlCgEDu09VQz/w==",
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-13.0.0.tgz",
"integrity": "sha512-4zAMuFGgxZkOoyQIzZMHv1HlvgyJK3AkNqjAgm8A8V0UmOZO7yv3pH49cDV1OduzFJqgs6yQ6eG4OGydhKtxlg==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -3406,9 +3412,9 @@
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.99.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.99.0.tgz",
"integrity": "sha512-qRVEPQxtoerXF+izJ0O7jGAr5o0Uyvnyu7ao5DTKzF+V7Fv3SurE0l43oCeZPFKo/Ld4V7vEylhFCm4IHVZKWA==",
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-13.0.1.tgz",
"integrity": "sha512-+BRRzjselpWudKR0ltAW5SUt9T82D+gzQN8DdOQUgnSVWWp7oLCeTGBRptbQz+436Ihn/mPzmo/xnf0cv/Qw1A==",
"dev": true,
"license": "MIT",
"optional": true,
@@ -4765,10 +4771,11 @@
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
"dev": true
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
"node_modules/nopt": {
"version": "5.0.0",
@@ -5819,10 +5826,11 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz",
"integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==",
"version": "7.6.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz",
"integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
@@ -6409,9 +6417,9 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
},
@@ -6425,9 +6433,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@@ -6443,9 +6451,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"

View File

@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -36,7 +36,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"exiftool-vendored": "^28.3.1",
"globals": "^15.9.0",
"jose": "^5.6.3",

View File

@@ -1,5 +1,5 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -403,68 +403,157 @@ describe('/libraries', () => {
utils.removeImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
});
it('should reimport a modified file', async () => {
const annoyingChars = [
"'",
'"',
'`',
'*',
'{',
'}',
',',
'(',
')',
'[',
']',
'?',
'!',
'@',
'#',
'$',
'%',
'^',
'&',
'=',
'+',
'~',
'|',
'<',
'>',
';',
':',
'/', // We never got backslashes to work
];
it.each(annoyingChars)('should scan multiple import paths with %s', async (char) => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
importPaths: [`${testAssetDirInternal}/temp/folder${char}1`, `${testAssetDirInternal}/temp/folder${char}2`],
});
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);
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}1/asset1.png`) }),
expect.objectContaining({ originalPath: expect.stringContaining(`folder${char}2/asset2.png`) }),
]),
);
utils.removeImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
utils.removeImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
});
it('should reimport a modified file', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/reimport`],
});
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(1);
expect(assets.count).toEqual(1);
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(asset).toEqual(
expect.objectContaining({
originalFileName: 'asset.jpg',
exifInfo: expect.objectContaining({
model: 'NIKON D750',
}),
}),
);
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
});
it('should not reimport unmodified files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
importPaths: [`${testAssetDirInternal}/temp/reimport`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
await utimes(`${testAssetDir}/temp/directoryA/assetB.jpg`, 447_775_200_000);
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.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);
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ refreshModifiedFiles: true });
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.jpg`);
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
model: 'NIKON D750',
});
expect(assets.count).toBe(0);
expect(assets.count).toEqual(1);
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(asset).toEqual(
expect.objectContaining({
originalFileName: 'asset.jpg',
exifInfo: expect.not.objectContaining({
model: 'NIKON D750',
}),
}),
);
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
});
it('should set an asset offline if its file is missing', async () => {
@@ -601,6 +690,298 @@ describe('/libraries', () => {
expect(assets).toEqual(assetsBefore);
});
describe('xmp metadata', async () => {
it('should import metadata from file.xmp', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should import metadata from file.ext.xmp', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2000-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-09-27T12:35:33.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.ext.xmp to file metadata', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-07-20T17:27:12.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
it('should switch from using file.xmp to file metadata', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/xmp`],
});
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(newAssets.items).toEqual([
expect.objectContaining({
originalFileName: 'glarus.nef',
fileCreatedAt: '2010-07-20T17:27:12.000Z',
}),
]);
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
});
});
describe('POST /libraries/:id/validate', () => {

View File

@@ -1142,6 +1142,7 @@
"set": "Set",
"set_as_album_cover": "Set as album cover",
"set_as_profile_picture": "Set as profile picture",
"set_as_featured_photo": "Set as featured photo",
"set_date_of_birth": "Set date of birth",
"set_profile_picture": "Set profile picture",
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
@@ -1196,6 +1197,7 @@
"sort_items": "Number of items",
"sort_modified": "Date modified",
"sort_oldest": "Oldest photo",
"sort_people_by_similarity": "Sort people by similarity",
"sort_recent": "Most recent photo",
"sort_title": "Title",
"source": "Source",
@@ -1336,5 +1338,6 @@
"years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes",
"you_dont_have_any_shared_links": "You don't have any shared links",
"zoom_image": "Zoom Image"
"zoom_image": "Zoom Image",
"workflows": "Workflows"
}

View File

@@ -1331,13 +1331,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "huggingface-hub"
version = "0.26.5"
version = "0.27.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.26.5-py3-none-any.whl", hash = "sha256:fb7386090bbe892072e64b85f7c4479fd2d65eea5f2543327c970d5169e83924"},
{file = "huggingface_hub-0.26.5.tar.gz", hash = "sha256:1008bd18f60bfb65e8dbc0a97249beeeaa8c99d3c2fa649354df9fa5a13ed83b"},
{file = "huggingface_hub-0.27.0-py3-none-any.whl", hash = "sha256:8f2e834517f1f1ddf1ecc716f91b120d7333011b7485f665a9a412eacb1a2a81"},
{file = "huggingface_hub-0.27.0.tar.gz", hash = "sha256:902cce1a1be5739f5589e560198a65a8edcfd3b830b1666f36e4b961f0454fac"},
]
[package.dependencies]
@@ -2492,18 +2492,18 @@ files = [
[[package]]
name = "pydantic"
version = "2.10.3"
version = "2.10.4"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic-2.10.3-py3-none-any.whl", hash = "sha256:be04d85bbc7b65651c5f8e6b9976ed9c6f41782a55524cef079a34a0bb82144d"},
{file = "pydantic-2.10.3.tar.gz", hash = "sha256:cb5ac360ce894ceacd69c403187900a02c4b20b693a9dd1d643e1effab9eadf9"},
{file = "pydantic-2.10.4-py3-none-any.whl", hash = "sha256:597e135ea68be3a37552fb524bc7d0d66dcf93d395acd93a00682f1efcb8ee3d"},
{file = "pydantic-2.10.4.tar.gz", hash = "sha256:82f12e9723da6de4fe2ba888b5971157b3be7ad914267dea8f05f82b28254f06"},
]
[package.dependencies]
annotated-types = ">=0.6.0"
pydantic-core = "2.27.1"
pydantic-core = "2.27.2"
typing-extensions = ">=4.12.2"
[package.extras]
@@ -2512,111 +2512,111 @@ timezone = ["tzdata"]
[[package]]
name = "pydantic-core"
version = "2.27.1"
version = "2.27.2"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:71a5e35c75c021aaf400ac048dacc855f000bdfed91614b4a726f7432f1f3d6a"},
{file = "pydantic_core-2.27.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f82d068a2d6ecfc6e054726080af69a6764a10015467d7d7b9f66d6ed5afa23b"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:121ceb0e822f79163dd4699e4c54f5ad38b157084d97b34de8b232bcaad70278"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4603137322c18eaf2e06a4495f426aa8d8388940f3c457e7548145011bb68e05"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a33cd6ad9017bbeaa9ed78a2e0752c5e250eafb9534f308e7a5f7849b0b1bfb4"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:15cc53a3179ba0fcefe1e3ae50beb2784dede4003ad2dfd24f81bba4b23a454f"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45d9c5eb9273aa50999ad6adc6be5e0ecea7e09dbd0d31bd0c65a55a2592ca08"},
{file = "pydantic_core-2.27.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8bf7b66ce12a2ac52d16f776b31d16d91033150266eb796967a7e4621707e4f6"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:655d7dd86f26cb15ce8a431036f66ce0318648f8853d709b4167786ec2fa4807"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:5556470f1a2157031e676f776c2bc20acd34c1990ca5f7e56f1ebf938b9ab57c"},
{file = "pydantic_core-2.27.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f69ed81ab24d5a3bd93861c8c4436f54afdf8e8cc421562b0c7504cf3be58206"},
{file = "pydantic_core-2.27.1-cp310-none-win32.whl", hash = "sha256:f5a823165e6d04ccea61a9f0576f345f8ce40ed533013580e087bd4d7442b52c"},
{file = "pydantic_core-2.27.1-cp310-none-win_amd64.whl", hash = "sha256:57866a76e0b3823e0b56692d1a0bf722bffb324839bb5b7226a7dbd6c9a40b17"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac3b20653bdbe160febbea8aa6c079d3df19310d50ac314911ed8cc4eb7f8cb8"},
{file = "pydantic_core-2.27.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a5a8e19d7c707c4cadb8c18f5f60c843052ae83c20fa7d44f41594c644a1d330"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f7059ca8d64fea7f238994c97d91f75965216bcbe5f695bb44f354893f11d52"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bed0f8a0eeea9fb72937ba118f9db0cb7e90773462af7962d382445f3005e5a4"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a3cb37038123447cf0f3ea4c74751f6a9d7afef0eb71aa07bf5f652b5e6a132c"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:84286494f6c5d05243456e04223d5a9417d7f443c3b76065e75001beb26f88de"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acc07b2cfc5b835444b44a9956846b578d27beeacd4b52e45489e93276241025"},
{file = "pydantic_core-2.27.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4fefee876e07a6e9aad7a8c8c9f85b0cdbe7df52b8a9552307b09050f7512c7e"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:258c57abf1188926c774a4c94dd29237e77eda19462e5bb901d88adcab6af919"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:35c14ac45fcfdf7167ca76cc80b2001205a8d5d16d80524e13508371fb8cdd9c"},
{file = "pydantic_core-2.27.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d1b26e1dff225c31897696cab7d4f0a315d4c0d9e8666dbffdb28216f3b17fdc"},
{file = "pydantic_core-2.27.1-cp311-none-win32.whl", hash = "sha256:2cdf7d86886bc6982354862204ae3b2f7f96f21a3eb0ba5ca0ac42c7b38598b9"},
{file = "pydantic_core-2.27.1-cp311-none-win_amd64.whl", hash = "sha256:3af385b0cee8df3746c3f406f38bcbfdc9041b5c2d5ce3e5fc6637256e60bbc5"},
{file = "pydantic_core-2.27.1-cp311-none-win_arm64.whl", hash = "sha256:81f2ec23ddc1b476ff96563f2e8d723830b06dceae348ce02914a37cb4e74b89"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9cbd94fc661d2bab2bc702cddd2d3370bbdcc4cd0f8f57488a81bcce90c7a54f"},
{file = "pydantic_core-2.27.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f8c4718cd44ec1580e180cb739713ecda2bdee1341084c1467802a417fe0f02"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15aae984e46de8d376df515f00450d1522077254ef6b7ce189b38ecee7c9677c"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ba5e3963344ff25fc8c40da90f44b0afca8cfd89d12964feb79ac1411a260ac"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:992cea5f4f3b29d6b4f7f1726ed8ee46c8331c6b4eed6db5b40134c6fe1768bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0325336f348dbee6550d129b1627cb8f5351a9dc91aad141ffb96d4937bd9529"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7597c07fbd11515f654d6ece3d0e4e5093edc30a436c63142d9a4b8e22f19c35"},
{file = "pydantic_core-2.27.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3bbd5d8cc692616d5ef6fbbbd50dbec142c7e6ad9beb66b78a96e9c16729b089"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:dc61505e73298a84a2f317255fcc72b710b72980f3a1f670447a21efc88f8381"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:e1f735dc43da318cad19b4173dd1ffce1d84aafd6c9b782b3abc04a0d5a6f5bb"},
{file = "pydantic_core-2.27.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:f4e5658dbffe8843a0f12366a4c2d1c316dbe09bb4dfbdc9d2d9cd6031de8aae"},
{file = "pydantic_core-2.27.1-cp312-none-win32.whl", hash = "sha256:672ebbe820bb37988c4d136eca2652ee114992d5d41c7e4858cdd90ea94ffe5c"},
{file = "pydantic_core-2.27.1-cp312-none-win_amd64.whl", hash = "sha256:66ff044fd0bb1768688aecbe28b6190f6e799349221fb0de0e6f4048eca14c16"},
{file = "pydantic_core-2.27.1-cp312-none-win_arm64.whl", hash = "sha256:9a3b0793b1bbfd4146304e23d90045f2a9b5fd5823aa682665fbdaf2a6c28f3e"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:f216dbce0e60e4d03e0c4353c7023b202d95cbaeff12e5fd2e82ea0a66905073"},
{file = "pydantic_core-2.27.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2e02889071850bbfd36b56fd6bc98945e23670773bc7a76657e90e6b6603c08"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42b0e23f119b2b456d07ca91b307ae167cc3f6c846a7b169fca5326e32fdc6cf"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:764be71193f87d460a03f1f7385a82e226639732214b402f9aa61f0d025f0737"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c00666a3bd2f84920a4e94434f5974d7bbc57e461318d6bb34ce9cdbbc1f6b2"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3ccaa88b24eebc0f849ce0a4d09e8a408ec5a94afff395eb69baf868f5183107"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c65af9088ac534313e1963443d0ec360bb2b9cba6c2909478d22c2e363d98a51"},
{file = "pydantic_core-2.27.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206b5cf6f0c513baffaeae7bd817717140770c74528f3e4c3e1cec7871ddd61a"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:062f60e512fc7fff8b8a9d680ff0ddaaef0193dba9fa83e679c0c5f5fbd018bc"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:a0697803ed7d4af5e4c1adf1670af078f8fcab7a86350e969f454daf598c4960"},
{file = "pydantic_core-2.27.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:58ca98a950171f3151c603aeea9303ef6c235f692fe555e883591103da709b23"},
{file = "pydantic_core-2.27.1-cp313-none-win32.whl", hash = "sha256:8065914ff79f7eab1599bd80406681f0ad08f8e47c880f17b416c9f8f7a26d05"},
{file = "pydantic_core-2.27.1-cp313-none-win_amd64.whl", hash = "sha256:ba630d5e3db74c79300d9a5bdaaf6200172b107f263c98a0539eeecb857b2337"},
{file = "pydantic_core-2.27.1-cp313-none-win_arm64.whl", hash = "sha256:45cf8588c066860b623cd11c4ba687f8d7175d5f7ef65f7129df8a394c502de5"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:5897bec80a09b4084aee23f9b73a9477a46c3304ad1d2d07acca19723fb1de62"},
{file = "pydantic_core-2.27.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d0165ab2914379bd56908c02294ed8405c252250668ebcb438a55494c69f44ab"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b9af86e1d8e4cfc82c2022bfaa6f459381a50b94a29e95dcdda8442d6d83864"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f6c8a66741c5f5447e047ab0ba7a1c61d1e95580d64bce852e3df1f895c4067"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9a42d6a8156ff78981f8aa56eb6394114e0dedb217cf8b729f438f643608cbcd"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64c65f40b4cd8b0e049a8edde07e38b476da7e3aaebe63287c899d2cff253fa5"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdcf339322a3fae5cbd504edcefddd5a50d9ee00d968696846f089b4432cf78"},
{file = "pydantic_core-2.27.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bf99c8404f008750c846cb4ac4667b798a9f7de673ff719d705d9b2d6de49c5f"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8f1edcea27918d748c7e5e4d917297b2a0ab80cad10f86631e488b7cddf76a36"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:159cac0a3d096f79ab6a44d77a961917219707e2a130739c64d4dd46281f5c2a"},
{file = "pydantic_core-2.27.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:029d9757eb621cc6e1848fa0b0310310de7301057f623985698ed7ebb014391b"},
{file = "pydantic_core-2.27.1-cp38-none-win32.whl", hash = "sha256:a28af0695a45f7060e6f9b7092558a928a28553366519f64083c63a44f70e618"},
{file = "pydantic_core-2.27.1-cp38-none-win_amd64.whl", hash = "sha256:2d4567c850905d5eaaed2f7a404e61012a51caf288292e016360aa2b96ff38d4"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e9386266798d64eeb19dd3677051f5705bf873e98e15897ddb7d76f477131967"},
{file = "pydantic_core-2.27.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4228b5b646caa73f119b1ae756216b59cc6e2267201c27d3912b592c5e323b60"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0b3dfe500de26c52abe0477dde16192ac39c98f05bf2d80e76102d394bd13854"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aee66be87825cdf72ac64cb03ad4c15ffef4143dbf5c113f64a5ff4f81477bf9"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b748c44bb9f53031c8cbc99a8a061bc181c1000c60a30f55393b6e9c45cc5bd"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ca038c7f6a0afd0b2448941b6ef9d5e1949e999f9e5517692eb6da58e9d44be"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e0bd57539da59a3e4671b90a502da9a28c72322a4f17866ba3ac63a82c4498e"},
{file = "pydantic_core-2.27.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ac6c2c45c847bbf8f91930d88716a0fb924b51e0c6dad329b793d670ec5db792"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b94d4ba43739bbe8b0ce4262bcc3b7b9f31459ad120fb595627eaeb7f9b9ca01"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:00e6424f4b26fe82d44577b4c842d7df97c20be6439e8e685d0d715feceb9fb9"},
{file = "pydantic_core-2.27.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:38de0a70160dd97540335b7ad3a74571b24f1dc3ed33f815f0880682e6880131"},
{file = "pydantic_core-2.27.1-cp39-none-win32.whl", hash = "sha256:7ccebf51efc61634f6c2344da73e366c75e735960b5654b63d7e6f69a5885fa3"},
{file = "pydantic_core-2.27.1-cp39-none-win_amd64.whl", hash = "sha256:a57847b090d7892f123726202b7daa20df6694cbd583b67a592e856bff603d6c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3fa80ac2bd5856580e242dbc202db873c60a01b20309c8319b5c5986fbe53ce6"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d950caa237bb1954f1b8c9227b5065ba6875ac9771bb8ec790d956a699b78676"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e4216e64d203e39c62df627aa882f02a2438d18a5f21d7f721621f7a5d3611d"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02a3d637bd387c41d46b002f0e49c52642281edacd2740e5a42f7017feea3f2c"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:161c27ccce13b6b0c8689418da3885d3220ed2eae2ea5e9b2f7f3d48f1d52c27"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:19910754e4cc9c63bc1c7f6d73aa1cfee82f42007e407c0f413695c2f7ed777f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e173486019cc283dc9778315fa29a363579372fe67045e971e89b6365cc035ed"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:af52d26579b308921b73b956153066481f064875140ccd1dfd4e77db89dbb12f"},
{file = "pydantic_core-2.27.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:981fb88516bd1ae8b0cbbd2034678a39dedc98752f264ac9bc5839d3923fa04c"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5fde892e6c697ce3e30c61b239330fc5d569a71fefd4eb6512fc6caec9dd9e2f"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:816f5aa087094099fff7edabb5e01cc370eb21aa1a1d44fe2d2aefdfb5599b31"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c10c309e18e443ddb108f0ef64e8729363adbfd92d6d57beec680f6261556f3"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98476c98b02c8e9b2eec76ac4156fd006628b1b2d0ef27e548ffa978393fd154"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3027001c28434e7ca5a6e1e527487051136aa81803ac812be51802150d880dd"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:7699b1df36a48169cdebda7ab5a2bac265204003f153b4bd17276153d997670a"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1c39b07d90be6b48968ddc8c19e7585052088fd7ec8d568bb31ff64c70ae3c97"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:46ccfe3032b3915586e469d4972973f893c0a2bb65669194a5bdea9bacc088c2"},
{file = "pydantic_core-2.27.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:62ba45e21cf6571d7f716d903b5b7b6d2617e2d5d67c0923dc47b9d41369f840"},
{file = "pydantic_core-2.27.1.tar.gz", hash = "sha256:62a763352879b84aa31058fc931884055fd75089cccbd9d58bb6afd01141b235"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"},
{file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"},
{file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"},
{file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"},
{file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"},
{file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"},
{file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"},
{file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"},
{file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"},
{file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"},
{file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"},
{file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"},
{file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"},
{file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"},
{file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"},
{file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"},
{file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"},
{file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"},
{file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"},
{file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"},
{file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"},
{file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"},
{file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"},
{file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"},
{file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"},
{file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"},
{file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"},
{file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"},
{file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"},
{file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"},
{file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"},
{file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"},
{file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"},
{file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"},
]
[package.dependencies]
@@ -2624,13 +2624,13 @@ typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
[[package]]
name = "pydantic-settings"
version = "2.6.1"
version = "2.7.0"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.8"
files = [
{file = "pydantic_settings-2.6.1-py3-none-any.whl", hash = "sha256:7fb0637c786a558d3103436278a7c4f1cfd29ba8973238a50c5bb9a55387da87"},
{file = "pydantic_settings-2.6.1.tar.gz", hash = "sha256:e0f92546d8a9923cb8941689abf85d6601a8c19a23e97a34b2964a2e3f813ca0"},
{file = "pydantic_settings-2.7.0-py3-none-any.whl", hash = "sha256:e00c05d5fa6cbbb227c84bd7487c5c1065084119b750df7c8c1a554aed236eb5"},
{file = "pydantic_settings-2.7.0.tar.gz", hash = "sha256:ac4bfd4a36831a48dbf8b2d9325425b549a0a6f18cea118436d728eb4f1c4d66"},
]
[package.dependencies]
@@ -2706,20 +2706,20 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.24.0"
version = "0.25.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
{file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
]
[package.dependencies]
pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
@@ -2787,13 +2787,13 @@ cli = ["click (>=5.0)"]
[[package]]
name = "python-multipart"
version = "0.0.19"
version = "0.0.20"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "python_multipart-0.0.19-py3-none-any.whl", hash = "sha256:f8d5b0b9c618575bf9df01c684ded1d94a338839bdd8223838afacfb4bb2082d"},
{file = "python_multipart-0.0.19.tar.gz", hash = "sha256:905502ef39050557b7a6af411f454bc19526529ca46ae6831508438890ce12cc"},
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
]
[[package]]
@@ -3043,29 +3043,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.8.2"
version = "0.8.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.8.2-py3-none-linux_armv6l.whl", hash = "sha256:c49ab4da37e7c457105aadfd2725e24305ff9bc908487a9bf8d548c6dad8bb3d"},
{file = "ruff-0.8.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ec016beb69ac16be416c435828be702ee694c0d722505f9c1f35e1b9c0cc1bf5"},
{file = "ruff-0.8.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f05cdf8d050b30e2ba55c9b09330b51f9f97d36d4673213679b965d25a785f3c"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60f578c11feb1d3d257b2fb043ddb47501ab4816e7e221fbb0077f0d5d4e7b6f"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cbd5cf9b0ae8f30eebc7b360171bd50f59ab29d39f06a670b3e4501a36ba5897"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b402ddee3d777683de60ff76da801fa7e5e8a71038f57ee53e903afbcefdaa58"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:705832cd7d85605cb7858d8a13d75993c8f3ef1397b0831289109e953d833d29"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:32096b41aaf7a5cc095fa45b4167b890e4c8d3fd217603f3634c92a541de7248"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e769083da9439508833cfc7c23e351e1809e67f47c50248250ce1ac52c21fb93"},
{file = "ruff-0.8.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fe716592ae8a376c2673fdfc1f5c0c193a6d0411f90a496863c99cd9e2ae25d"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:81c148825277e737493242b44c5388a300584d73d5774defa9245aaef55448b0"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d261d7850c8367704874847d95febc698a950bf061c9475d4a8b7689adc4f7fa"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1ca4e3a87496dc07d2427b7dd7ffa88a1e597c28dad65ae6433ecb9f2e4f022f"},
{file = "ruff-0.8.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:729850feed82ef2440aa27946ab39c18cb4a8889c1128a6d589ffa028ddcfc22"},
{file = "ruff-0.8.2-py3-none-win32.whl", hash = "sha256:ac42caaa0411d6a7d9594363294416e0e48fc1279e1b0e948391695db2b3d5b1"},
{file = "ruff-0.8.2-py3-none-win_amd64.whl", hash = "sha256:2aae99ec70abf43372612a838d97bfe77d45146254568d94926e8ed5bbb409ea"},
{file = "ruff-0.8.2-py3-none-win_arm64.whl", hash = "sha256:fb88e2a506b70cfbc2de6fae6681c4f944f7dd5f2fe87233a7233d888bad73e8"},
{file = "ruff-0.8.2.tar.gz", hash = "sha256:b84f4f414dda8ac7f75075c1fa0b905ac0ff25361f42e6d5da681a465e0f78e5"},
{file = "ruff-0.8.3-py3-none-linux_armv6l.whl", hash = "sha256:8d5d273ffffff0acd3db5bf626d4b131aa5a5ada1276126231c4174543ce20d6"},
{file = "ruff-0.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e4d66a21de39f15c9757d00c50c8cdd20ac84f55684ca56def7891a025d7e939"},
{file = "ruff-0.8.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c356e770811858bd20832af696ff6c7e884701115094f427b64b25093d6d932d"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c0a60a825e3e177116c84009d5ebaa90cf40dfab56e1358d1df4e29a9a14b13"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:75fb782f4db39501210ac093c79c3de581d306624575eddd7e4e13747e61ba18"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f26bc76a133ecb09a38b7868737eded6941b70a6d34ef53a4027e83913b6502"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:01b14b2f72a37390c1b13477c1c02d53184f728be2f3ffc3ace5b44e9e87b90d"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:53babd6e63e31f4e96ec95ea0d962298f9f0d9cc5990a1bbb023a6baf2503a82"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ae441ce4cf925b7f363d33cd6570c51435972d697e3e58928973994e56e1452"},
{file = "ruff-0.8.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7c65bc0cadce32255e93c57d57ecc2cca23149edd52714c0c5d6fa11ec328cd"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:5be450bb18f23f0edc5a4e5585c17a56ba88920d598f04a06bd9fd76d324cb20"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8faeae3827eaa77f5721f09b9472a18c749139c891dbc17f45e72d8f2ca1f8fc"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:db503486e1cf074b9808403991663e4277f5c664d3fe237ee0d994d1305bb060"},
{file = "ruff-0.8.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:6567be9fb62fbd7a099209257fef4ad2c3153b60579818b31a23c886ed4147ea"},
{file = "ruff-0.8.3-py3-none-win32.whl", hash = "sha256:19048f2f878f3ee4583fc6cb23fb636e48c2635e30fb2022b3a1cd293402f964"},
{file = "ruff-0.8.3-py3-none-win_amd64.whl", hash = "sha256:f7df94f57d7418fa7c3ffb650757e0c2b96cf2501a0b192c18e4fb5571dfada9"},
{file = "ruff-0.8.3-py3-none-win_arm64.whl", hash = "sha256:fe2756edf68ea79707c8d68b78ca9a58ed9af22e430430491ee03e718b5e4936"},
{file = "ruff-0.8.3.tar.gz", hash = "sha256:5e7558304353b84279042fc584a4f4cb8a07ae79b2bf3da1a7551d960b5626d3"},
]
[[package]]
@@ -3393,13 +3393,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.32.1"
version = "0.34.0"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
python-versions = ">=3.9"
files = [
{file = "uvicorn-0.32.1-py3-none-any.whl", hash = "sha256:82ad92fd58da0d12af7482ecdb5f2470a04c9c9a53ced65b9bbb4a205377602e"},
{file = "uvicorn-0.32.1.tar.gz", hash = "sha256:ee9519c246a72b1c084cea8d3b44ed6026e78a4a309cbedae9c37e4cb9fbb175"},
{file = "uvicorn-0.34.0-py3-none-any.whl", hash = "sha256:023dc038422502fa28a09c7a30bf2b6991512da7dcdb8fd35fe57cfc154126f4"},
{file = "uvicorn-0.34.0.tar.gz", hash = "sha256:404051050cd7e905de2c9a7e61790943440b3416f49cb409f965d9dcd0fa73e9"},
]
[package.dependencies]

View File

@@ -403,7 +403,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -546,7 +546,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -575,7 +575,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 185;
CURRENT_PROJECT_VERSION = 186;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.122.3</string>
<string>1.123.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>185</string>
<string>186</string>
<key>FLTEnableImpeller</key>
<true/>
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -127,18 +127,29 @@ class GalleryViewerPage extends HookConsumerWidget {
context: context,
useSafeArea: true,
builder: (context) {
return FractionallySizedBox(
heightFactor: 0.75,
child: Padding(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: ref
.watch(appSettingsServiceProvider)
.getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)
? AdvancedBottomSheet(assetDetail: asset)
: DetailPanel(asset: asset),
),
return DraggableScrollableSheet(
minChildSize: 0.5,
maxChildSize: 1,
initialChildSize: 0.75,
expand: false,
builder: (context, scrollController) {
return Padding(
padding: EdgeInsets.only(
bottom: context.viewInsets.bottom,
),
child: ref.watch(appSettingsServiceProvider).getSetting<bool>(
AppSettingsEnum.advancedTroubleshooting,
)
? AdvancedBottomSheet(
assetDetail: asset,
scrollController: scrollController,
)
: DetailPanel(
asset: asset,
scrollController: scrollController,
),
);
},
);
},
);

View File

@@ -14,7 +14,6 @@ class AlbumMediaRepository implements IAlbumMediaRepository {
final List<AssetPathEntity> assetPathEntities =
await PhotoManager.getAssetPathList(
hasAll: true,
filterOption: FilterOptionGroup(containsPathModified: true),
);
return assetPathEntities.map(_toAlbum).toList();
}

View File

@@ -372,7 +372,6 @@ class BackgroundService {
HttpOverrides.global = HttpSSLCertOverride();
ApiService apiService = ApiService();
apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
AppSettingsService settingsService = AppSettingsService();
AlbumRepository albumRepository = AlbumRepository(db);
AssetRepository assetRepository = AssetRepository(db);
@@ -422,7 +421,7 @@ class BackgroundService {
);
BackupService backupService = BackupService(
apiService,
settingService,
settingsService,
albumService,
albumMediaRepository,
fileMediaRepository,

View File

@@ -313,15 +313,12 @@ class BackupService {
);
}
} else {
if (asset.type == AssetType.video) {
file = await asset.local!.originFile;
} else {
file = await asset.local!.originFile
file =
await asset.local!.originFile.timeout(const Duration(seconds: 5));
if (asset.local!.isLivePhoto) {
livePhotoFile = await asset.local!.originFileWithSubtype
.timeout(const Duration(seconds: 5));
if (asset.local!.isLivePhoto) {
livePhotoFile = await asset.local!.originFileWithSubtype
.timeout(const Duration(seconds: 5));
}
}
}

View File

@@ -6,12 +6,18 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class AdvancedBottomSheet extends HookConsumerWidget {
final Asset assetDetail;
final ScrollController? scrollController;
const AdvancedBottomSheet({super.key, required this.assetDetail});
const AdvancedBottomSheet({
super.key,
required this.assetDetail,
this.scrollController,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return SingleChildScrollView(
controller: scrollController,
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8.0),
child: LayoutBuilder(

View File

@@ -9,12 +9,14 @@ import 'package:immich_mobile/entities/asset.entity.dart';
class DetailPanel extends HookConsumerWidget {
final Asset asset;
final ScrollController? scrollController;
const DetailPanel({super.key, required this.asset});
const DetailPanel({super.key, required this.asset, this.scrollController});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ListView(
controller: scrollController,
shrinkWrap: true,
children: [
Padding(

View File

@@ -18,7 +18,7 @@ class FileInfo extends StatelessWidget {
final height = asset.orientatedHeight ?? asset.height;
final width = asset.orientatedWidth ?? asset.width;
String resolution =
height != null && width != null ? "$height x $width " : "";
height != null && width != null ? "$width x $height " : "";
String fileSize = asset.exifInfo?.fileSize != null
? formatBytes(asset.exifInfo!.fileSize!)
: "";

View File

@@ -93,17 +93,17 @@ Class | Method | HTTP request | Description
*AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} |
*AlbumsApi* | [**updateAlbumInfo**](doc//AlbumsApi.md#updatealbuminfo) | **PATCH** /albums/{id} |
*AlbumsApi* | [**updateAlbumUser**](doc//AlbumsApi.md#updatealbumuser) | **PUT** /albums/{id}/user/{userId} |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check |
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist |
*AssetsApi* | [**checkBulkUpload**](doc//AssetsApi.md#checkbulkupload) | **POST** /assets/bulk-upload-check | Checks if assets exist by checksums
*AssetsApi* | [**checkExistingAssets**](doc//AssetsApi.md#checkexistingassets) | **POST** /assets/exist | Checks if multiple assets exist on the server and returns all existing - used by background backup
*AssetsApi* | [**deleteAssets**](doc//AssetsApi.md#deleteassets) | **DELETE** /assets |
*AssetsApi* | [**downloadAsset**](doc//AssetsApi.md#downloadasset) | **GET** /assets/{id}/original |
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} |
*AssetsApi* | [**getAllUserAssetsByDeviceId**](doc//AssetsApi.md#getalluserassetsbydeviceid) | **GET** /assets/device/{deviceId} | Get all asset of a device that are in the database, ID only.
*AssetsApi* | [**getAssetInfo**](doc//AssetsApi.md#getassetinfo) | **GET** /assets/{id} |
*AssetsApi* | [**getAssetStatistics**](doc//AssetsApi.md#getassetstatistics) | **GET** /assets/statistics |
*AssetsApi* | [**getMemoryLane**](doc//AssetsApi.md#getmemorylane) | **GET** /assets/memory-lane |
*AssetsApi* | [**getRandom**](doc//AssetsApi.md#getrandom) | **GET** /assets/random |
*AssetsApi* | [**playAssetVideo**](doc//AssetsApi.md#playassetvideo) | **GET** /assets/{id}/video/playback |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original |
*AssetsApi* | [**replaceAsset**](doc//AssetsApi.md#replaceasset) | **PUT** /assets/{id}/original | Replace the asset with new file, without changing its id
*AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs |
*AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} |
*AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets |

View File

@@ -13,17 +13,11 @@ part of openapi.api;
class AlbumUserAddDto {
/// Returns a new [AlbumUserAddDto] instance.
AlbumUserAddDto({
this.role,
this.role = AlbumUserRole.editor,
required this.userId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
AlbumUserRole? role;
AlbumUserRole role;
String userId;
@@ -35,7 +29,7 @@ class AlbumUserAddDto {
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(role == null ? 0 : role!.hashCode) +
(role.hashCode) +
(userId.hashCode);
@override
@@ -43,11 +37,7 @@ class AlbumUserAddDto {
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.role != null) {
json[r'role'] = this.role;
} else {
// json[r'role'] = null;
}
json[r'userId'] = this.userId;
return json;
}
@@ -61,7 +51,7 @@ class AlbumUserAddDto {
final json = value.cast<String, dynamic>();
return AlbumUserAddDto(
role: AlbumUserRole.fromJson(json[r'role']),
role: AlbumUserRole.fromJson(json[r'role']) ?? AlbumUserRole.editor,
userId: mapValueOfType<String>(json, r'userId')!,
);
}

View File

@@ -13,15 +13,15 @@ part of openapi.api;
class CreateLibraryDto {
/// Returns a new [CreateLibraryDto] instance.
CreateLibraryDto({
this.exclusionPatterns = const [],
this.importPaths = const [],
this.exclusionPatterns = const {},
this.importPaths = const {},
this.name,
required this.ownerId,
});
List<String> exclusionPatterns;
Set<String> exclusionPatterns;
List<String> importPaths;
Set<String> importPaths;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -53,8 +53,8 @@ class CreateLibraryDto {
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns;
json[r'importPaths'] = this.importPaths;
json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths.toList(growable: false);
if (this.name != null) {
json[r'name'] = this.name;
} else {
@@ -74,11 +74,11 @@ class CreateLibraryDto {
return CreateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const {},
importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const {},
name: mapValueOfType<String>(json, r'name'),
ownerId: mapValueOfType<String>(json, r'ownerId')!,
);

View File

@@ -13,14 +13,14 @@ part of openapi.api;
class UpdateLibraryDto {
/// Returns a new [UpdateLibraryDto] instance.
UpdateLibraryDto({
this.exclusionPatterns = const [],
this.importPaths = const [],
this.exclusionPatterns = const {},
this.importPaths = const {},
this.name,
});
List<String> exclusionPatterns;
Set<String> exclusionPatterns;
List<String> importPaths;
Set<String> importPaths;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -48,8 +48,8 @@ class UpdateLibraryDto {
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns;
json[r'importPaths'] = this.importPaths;
json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths.toList(growable: false);
if (this.name != null) {
json[r'name'] = this.name;
} else {
@@ -68,11 +68,11 @@ class UpdateLibraryDto {
return UpdateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const {},
importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const {},
name: mapValueOfType<String>(json, r'name'),
);
}

View File

@@ -13,13 +13,13 @@ part of openapi.api;
class ValidateLibraryDto {
/// Returns a new [ValidateLibraryDto] instance.
ValidateLibraryDto({
this.exclusionPatterns = const [],
this.importPaths = const [],
this.exclusionPatterns = const {},
this.importPaths = const {},
});
List<String> exclusionPatterns;
Set<String> exclusionPatterns;
List<String> importPaths;
Set<String> importPaths;
@override
bool operator ==(Object other) => identical(this, other) || other is ValidateLibraryDto &&
@@ -37,8 +37,8 @@ class ValidateLibraryDto {
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
json[r'exclusionPatterns'] = this.exclusionPatterns;
json[r'importPaths'] = this.importPaths;
json[r'exclusionPatterns'] = this.exclusionPatterns.toList(growable: false);
json[r'importPaths'] = this.importPaths.toList(growable: false);
return json;
}
@@ -52,11 +52,11 @@ class ValidateLibraryDto {
return ValidateLibraryDto(
exclusionPatterns: json[r'exclusionPatterns'] is Iterable
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'exclusionPatterns'] as Iterable).cast<String>().toSet()
: const {},
importPaths: json[r'importPaths'] is Iterable
? (json[r'importPaths'] as Iterable).cast<String>().toList(growable: false)
: const [],
? (json[r'importPaths'] as Iterable).cast<String>().toSet()
: const {},
);
}
return null;

View File

@@ -1424,7 +1424,6 @@
},
"/assets/bulk-upload-check": {
"post": {
"description": "Checks if assets exist by checksums",
"operationId": "checkBulkUpload",
"parameters": [],
"requestBody": {
@@ -1460,6 +1459,7 @@
"api_key": []
}
],
"summary": "Checks if assets exist by checksums",
"tags": [
"Assets"
]
@@ -1467,7 +1467,6 @@
},
"/assets/device/{deviceId}": {
"get": {
"description": "Get all asset of a device that are in the database, ID only.",
"operationId": "getAllUserAssetsByDeviceId",
"parameters": [
{
@@ -1505,6 +1504,7 @@
"api_key": []
}
],
"summary": "Get all asset of a device that are in the database, ID only.",
"tags": [
"Assets"
]
@@ -1512,7 +1512,6 @@
},
"/assets/exist": {
"post": {
"description": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"operationId": "checkExistingAssets",
"parameters": [],
"requestBody": {
@@ -1548,6 +1547,7 @@
"api_key": []
}
],
"summary": "Checks if multiple assets exist on the server and returns all existing - used by background backup",
"tags": [
"Assets"
]
@@ -1903,7 +1903,6 @@
]
},
"put": {
"description": "Replace the asset with new file, without changing its id",
"operationId": "replaceAsset",
"parameters": [
{
@@ -1957,6 +1956,7 @@
"api_key": []
}
],
"summary": "Replace the asset with new file, without changing its id",
"tags": [
"Assets"
],
@@ -7492,6 +7492,7 @@
"items": {
"$ref": "#/components/schemas/Permission"
},
"minItems": 1,
"type": "array"
}
},
@@ -7572,7 +7573,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/ReactionType"
"allOf": [
{
"$ref": "#/components/schemas/ReactionType"
}
]
}
},
"required": [
@@ -7599,7 +7604,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/ReactionType"
"allOf": [
{
"$ref": "#/components/schemas/ReactionType"
}
]
},
"user": {
"$ref": "#/components/schemas/UserResponseDto"
@@ -7631,6 +7640,7 @@
"items": {
"$ref": "#/components/schemas/AlbumUserAddDto"
},
"minItems": 1,
"type": "array"
}
},
@@ -7699,7 +7709,11 @@
"type": "string"
},
"order": {
"$ref": "#/components/schemas/AssetOrder"
"allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
},
"owner": {
"$ref": "#/components/schemas/UserResponseDto"
@@ -7759,7 +7773,12 @@
"AlbumUserAddDto": {
"properties": {
"role": {
"$ref": "#/components/schemas/AlbumUserRole"
"allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
],
"default": "editor"
},
"userId": {
"format": "uuid",
@@ -7774,7 +7793,11 @@
"AlbumUserCreateDto": {
"properties": {
"role": {
"$ref": "#/components/schemas/AlbumUserRole"
"allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
},
"userId": {
"format": "uuid",
@@ -7790,7 +7813,11 @@
"AlbumUserResponseDto": {
"properties": {
"role": {
"$ref": "#/components/schemas/AlbumUserRole"
"allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
},
"user": {
"$ref": "#/components/schemas/UserResponseDto"
@@ -8087,7 +8114,11 @@
"nullable": true
},
"sourceType": {
"$ref": "#/components/schemas/SourceType"
"allOf": [
{
"$ref": "#/components/schemas/SourceType"
}
]
}
},
"required": [
@@ -8158,7 +8189,11 @@
"type": "integer"
},
"sourceType": {
"$ref": "#/components/schemas/SourceType"
"allOf": [
{
"$ref": "#/components/schemas/SourceType"
}
]
}
},
"required": [
@@ -8254,7 +8289,11 @@
"type": "array"
},
"name": {
"$ref": "#/components/schemas/AssetJobName"
"allOf": [
{
"$ref": "#/components/schemas/AssetJobName"
}
]
}
},
"required": [
@@ -8352,7 +8391,11 @@
"type": "string"
},
"status": {
"$ref": "#/components/schemas/AssetMediaStatus"
"allOf": [
{
"$ref": "#/components/schemas/AssetMediaStatus"
}
]
}
},
"required": [
@@ -8490,7 +8533,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"unassignedFaces": {
"items": {
@@ -8603,7 +8650,11 @@
"AvatarResponse": {
"properties": {
"color": {
"$ref": "#/components/schemas/UserAvatarColor"
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}
},
"required": [
@@ -8614,7 +8665,11 @@
"AvatarUpdate": {
"properties": {
"color": {
"$ref": "#/components/schemas/UserAvatarColor"
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
}
},
"type": "object"
@@ -8705,6 +8760,7 @@
"items": {
"type": "string"
},
"minItems": 1,
"type": "array"
},
"deviceId": {
@@ -8771,13 +8827,17 @@
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
},
"importPaths": {
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
},
"name": {
"type": "string"
@@ -9246,10 +9306,18 @@
"type": "string"
},
"entityType": {
"$ref": "#/components/schemas/PathEntityType"
"allOf": [
{
"$ref": "#/components/schemas/PathEntityType"
}
]
},
"pathType": {
"$ref": "#/components/schemas/PathType"
"allOf": [
{
"$ref": "#/components/schemas/PathType"
}
]
},
"pathValue": {
"type": "string"
@@ -9311,7 +9379,11 @@
"JobCommandDto": {
"properties": {
"command": {
"$ref": "#/components/schemas/JobCommand"
"allOf": [
{
"$ref": "#/components/schemas/JobCommand"
}
]
},
"force": {
"type": "boolean"
@@ -9356,7 +9428,11 @@
"JobCreateDto": {
"properties": {
"name": {
"$ref": "#/components/schemas/ManualJobName"
"allOf": [
{
"$ref": "#/components/schemas/ManualJobName"
}
]
}
},
"required": [
@@ -9544,6 +9620,7 @@
"properties": {
"email": {
"example": "testuser@email.com",
"format": "email",
"type": "string"
},
"password": {
@@ -9717,7 +9794,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/MemoryType"
"allOf": [
{
"$ref": "#/components/schemas/MemoryType"
}
]
}
},
"required": [
@@ -9782,7 +9863,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/MemoryType"
"allOf": [
{
"$ref": "#/components/schemas/MemoryType"
}
]
},
"updatedAt": {
"format": "date-time",
@@ -9911,7 +9996,11 @@
"type": "string"
},
"order": {
"$ref": "#/components/schemas/AssetOrder"
"allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
},
"originalFileName": {
"type": "string"
@@ -9962,7 +10051,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"updatedAfter": {
"format": "date-time",
@@ -10046,7 +10139,11 @@
"PartnerResponseDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
},
"email": {
"type": "string"
@@ -10564,7 +10661,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"updatedAfter": {
"format": "date-time",
@@ -11232,7 +11333,11 @@
"type": "boolean"
},
"type": {
"$ref": "#/components/schemas/SharedLinkType"
"allOf": [
{
"$ref": "#/components/schemas/SharedLinkType"
}
]
}
},
"required": [
@@ -11317,7 +11422,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/SharedLinkType"
"allOf": [
{
"$ref": "#/components/schemas/SharedLinkType"
}
]
},
"userId": {
"type": "string"
@@ -11350,6 +11459,7 @@
"properties": {
"email": {
"example": "testuser@email.com",
"format": "email",
"type": "string"
},
"name": {
@@ -11466,7 +11576,11 @@
"type": "string"
},
"type": {
"$ref": "#/components/schemas/AssetTypeEnum"
"allOf": [
{
"$ref": "#/components/schemas/AssetTypeEnum"
}
]
},
"updatedAfter": {
"format": "date-time",
@@ -11507,6 +11621,7 @@
"format": "uuid",
"type": "string"
},
"minItems": 2,
"type": "array"
}
},
@@ -11647,7 +11762,11 @@
"SystemConfigFFmpegDto": {
"properties": {
"accel": {
"$ref": "#/components/schemas/TranscodeHWAccel"
"allOf": [
{
"$ref": "#/components/schemas/TranscodeHWAccel"
}
]
},
"accelDecode": {
"type": "boolean"
@@ -11676,7 +11795,11 @@
"type": "integer"
},
"cqMode": {
"$ref": "#/components/schemas/CQMode"
"allOf": [
{
"$ref": "#/components/schemas/CQMode"
}
]
},
"crf": {
"maximum": 51,
@@ -11702,13 +11825,21 @@
"type": "integer"
},
"targetAudioCodec": {
"$ref": "#/components/schemas/AudioCodec"
"allOf": [
{
"$ref": "#/components/schemas/AudioCodec"
}
]
},
"targetResolution": {
"type": "string"
},
"targetVideoCodec": {
"$ref": "#/components/schemas/VideoCodec"
"allOf": [
{
"$ref": "#/components/schemas/VideoCodec"
}
]
},
"temporalAQ": {
"type": "boolean"
@@ -11718,10 +11849,18 @@
"type": "integer"
},
"tonemap": {
"$ref": "#/components/schemas/ToneMapping"
"allOf": [
{
"$ref": "#/components/schemas/ToneMapping"
}
]
},
"transcode": {
"$ref": "#/components/schemas/TranscodePolicy"
"allOf": [
{
"$ref": "#/components/schemas/TranscodePolicy"
}
]
},
"twoPass": {
"type": "boolean"
@@ -11766,7 +11905,11 @@
"SystemConfigGeneratedImageDto": {
"properties": {
"format": {
"$ref": "#/components/schemas/ImageFormat"
"allOf": [
{
"$ref": "#/components/schemas/ImageFormat"
}
]
},
"quality": {
"maximum": 100,
@@ -11788,7 +11931,11 @@
"SystemConfigImageDto": {
"properties": {
"colorspace": {
"$ref": "#/components/schemas/Colorspace"
"allOf": [
{
"$ref": "#/components/schemas/Colorspace"
}
]
},
"extractEmbedded": {
"type": "boolean"
@@ -11906,7 +12053,11 @@
"type": "boolean"
},
"level": {
"$ref": "#/components/schemas/LogLevel"
"allOf": [
{
"$ref": "#/components/schemas/LogLevel"
}
]
}
},
"required": [
@@ -11935,6 +12086,7 @@
"type": "string"
},
"urls": {
"format": "uri",
"items": {
"format": "uri",
"type": "string"
@@ -11955,12 +12107,14 @@
"SystemConfigMapDto": {
"properties": {
"darkStyle": {
"format": "uri",
"type": "string"
},
"enabled": {
"type": "boolean"
},
"lightStyle": {
"format": "uri",
"type": "string"
}
},
@@ -12035,6 +12189,7 @@
"type": "boolean"
},
"mobileRedirectUri": {
"format": "uri",
"type": "string"
},
"profileSigningAlgorithm": {
@@ -12097,6 +12252,7 @@
"SystemConfigServerDto": {
"properties": {
"externalDomain": {
"format": "uri",
"type": "string"
},
"loginPageMessage": {
@@ -12353,6 +12509,7 @@
"TagCreateDto": {
"properties": {
"color": {
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string"
},
"name": {
@@ -12408,6 +12565,7 @@
"properties": {
"color": {
"nullable": true,
"pattern": "^#?([0-9A-F]{3}|[0-9A-F]{4}|[0-9A-F]{6}|[0-9A-F]{8})$",
"type": "string"
}
},
@@ -12570,7 +12728,11 @@
"type": "boolean"
},
"order": {
"$ref": "#/components/schemas/AssetOrder"
"allOf": [
{
"$ref": "#/components/schemas/AssetOrder"
}
]
}
},
"type": "object"
@@ -12578,7 +12740,11 @@
"UpdateAlbumUserDto": {
"properties": {
"role": {
"$ref": "#/components/schemas/AlbumUserRole"
"allOf": [
{
"$ref": "#/components/schemas/AlbumUserRole"
}
]
}
},
"required": [
@@ -12625,13 +12791,17 @@
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
},
"importPaths": {
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
},
"name": {
"type": "string"
@@ -12697,6 +12867,7 @@
"UserAdminCreateDto": {
"properties": {
"email": {
"format": "email",
"type": "string"
},
"name": {
@@ -12740,7 +12911,11 @@
"UserAdminResponseDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
},
"createdAt": {
"format": "date-time",
@@ -12795,7 +12970,11 @@
"type": "boolean"
},
"status": {
"$ref": "#/components/schemas/UserStatus"
"allOf": [
{
"$ref": "#/components/schemas/UserStatus"
}
]
},
"storageLabel": {
"nullable": true,
@@ -12830,6 +13009,7 @@
"UserAdminUpdateDto": {
"properties": {
"email": {
"format": "email",
"type": "string"
},
"name": {
@@ -12967,7 +13147,11 @@
"UserResponseDto": {
"properties": {
"avatarColor": {
"$ref": "#/components/schemas/UserAvatarColor"
"allOf": [
{
"$ref": "#/components/schemas/UserAvatarColor"
}
]
},
"email": {
"type": "string"
@@ -13007,6 +13191,7 @@
"UserUpdateMeDto": {
"properties": {
"email": {
"format": "email",
"type": "string"
},
"name": {
@@ -13035,13 +13220,17 @@
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
},
"importPaths": {
"items": {
"type": "string"
},
"type": "array"
"maxItems": 128,
"type": "array",
"uniqueItems": true
}
},
"type": "object"

View File

@@ -12,7 +12,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"typescript": "^5.3.3"
}
},
@@ -22,13 +22,13 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
},
"node_modules/@types/node": {
"version": "22.9.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz",
"integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==",
"version": "22.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz",
"integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.8"
"undici-types": "~6.20.0"
}
},
"node_modules/typescript": {
@@ -46,9 +46,9 @@
}
},
"node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"version": "6.20.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz",
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==",
"dev": true,
"license": "MIT"
}

View File

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

View File

@@ -36,6 +36,7 @@
<a href="README_pt_BR.md">Português Brasileiro</a>
<a href="README_sv_SE.md">Svenska</a>
<a href="README_ar_JO.md">العربية</a>
<a href="README_vi_VN.md">Tiếng Việt</a>
<a href="README_th_TH.md">ภาษาไทย</a>
</p>
@@ -105,6 +106,8 @@
| 离线支持 | 是 | 否 |
| 只读相册 | 是 | 是 |
| 照片堆叠 | 是 | 是 |
| 标签 | 否 | 是 |
| 文件夹浏览 | 否 | 是 |
## 多语言

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20241210@sha256:35c28404b508fc0741fffb39a9de17e3a6acdff0b623bb7b6cdf4a427462a0e2 AS dev
FROM ghcr.io/immich-app/base-server-dev:20241224@sha256:6832c632c2a8cba5e20053ab694c9a8080e621841c784ed5d4675ef9dd203588 AS dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build
FROM node:22.11.0-alpine3.20@sha256:b64ced2e7cd0a4816699fe308ce6e8a08ccba463c757c00c14cd372e3d2c763e AS web
FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS web
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
@@ -42,7 +42,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20241210@sha256:076d002070385bc6dc7454ef9419f44341c074bcfc49be5deddbdb4108ae0060
FROM ghcr.io/immich-app/base-server-prod:20241224@sha256:69da007c241a961d6927d3d03f1c83ef0ec5c70bf656bff3ced32546a777e6f6
WORKDIR /usr/src/app
ENV NODE_ENV=production \

18448
server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -41,14 +41,14 @@
"@nestjs/platform-express": "^10.2.2",
"@nestjs/platform-socket.io": "^10.2.2",
"@nestjs/schedule": "^4.0.0",
"@nestjs/swagger": "^7.1.8",
"@nestjs/swagger": "^8.0.0",
"@nestjs/typeorm": "^10.0.0",
"@nestjs/websockets": "^10.2.2",
"@opentelemetry/auto-instrumentations-node": "^0.52.0",
"@opentelemetry/auto-instrumentations-node": "^0.54.0",
"@opentelemetry/context-async-hooks": "^1.24.0",
"@opentelemetry/exporter-prometheus": "^0.54.0",
"@opentelemetry/sdk-node": "^0.54.0",
"@react-email/components": "^0.0.28",
"@opentelemetry/exporter-prometheus": "^0.56.0",
"@opentelemetry/sdk-node": "^0.56.0",
"@react-email/components": "^0.0.31",
"@socket.io/redis-adapter": "^8.3.0",
"archiver": "^7.0.0",
"async-lock": "^1.4.0",
@@ -76,8 +76,8 @@
"openid-client": "^5.4.3",
"pg": "^8.11.3",
"picomatch": "^4.0.2",
"react": "^18.3.1",
"react-email": "^3.0.0",
"react": "^19.0.0",
"react-email": "^3.0.4",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"sanitize-filename": "^1.6.3",
@@ -108,11 +108,11 @@
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
"@types/multer": "^1.4.7",
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"@types/nodemailer": "^6.4.14",
"@types/picomatch": "^3.0.0",
"@types/pngjs": "^6.0.5",
"@types/react": "^18.3.4",
"@types/react": "^19.0.0",
"@types/semver": "^7.5.8",
"@types/supertest": "^6.0.0",
"@types/ua-parser-js": "^0.7.36",
@@ -122,7 +122,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.9.0",
"mock-fs": "^5.2.0",
"pngjs": "^7.0.0",

View File

@@ -49,7 +49,7 @@ export const GetLoginDetails = createParamDecorator((data, context: ExecutionCon
const userAgent = UAParser(request.headers['user-agent']);
return {
clientIp: request.ip,
clientIp: request.ip ?? '',
isSecure: request.secure,
deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '',
deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '',

View File

@@ -204,7 +204,7 @@ describe('getEnv', () => {
it('should return default network options', () => {
const { network } = getEnv();
expect(network).toEqual({
trustedProxies: [],
trustedProxies: ['linklocal', 'uniquelocal'],
});
});

View File

@@ -177,7 +177,7 @@ const getEnv = (): EnvData => {
licensePublicKey: isProd ? productionKeys : stagingKeys,
network: {
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? [],
trustedProxies: dto.IMMICH_TRUSTED_PROXIES ?? ['linklocal', 'uniquelocal'],
},
otel: {

View File

@@ -214,7 +214,7 @@ export class StorageRepository implements IStorageRepository {
}
private asGlob(pathToCrawl: string): string {
const escapedPath = escapePath(pathToCrawl);
const escapedPath = escapePath(pathToCrawl).replaceAll('"', '["]').replaceAll("'", "[']").replaceAll('`', '[`]');
const extensions = `*{${mimeTypes.getSupportedFileExtensions().join(',')}}`;
return `${escapedPath}/**/${extensions}`;
}

View File

@@ -414,7 +414,6 @@ describe(LibraryService.name, () => {
localDateTime: expect.any(Date),
type: AssetType.IMAGE,
originalFileName: 'photo.jpg',
sidecarPath: null,
isExternal: true,
},
],
@@ -423,57 +422,9 @@ describe(LibraryService.name, () => {
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.METADATA_EXTRACTION,
name: JobName.SIDECAR_DISCOVERY,
data: {
id: assetStub.image.id,
source: 'upload',
},
},
],
]);
});
it('should import a new asset with sidecar', async () => {
const mockLibraryJob: ILibraryFileJob = {
id: libraryStub.externalLibrary1.id,
ownerId: mockUser.id,
assetPath: '/data/user1/photo.jpg',
};
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(null);
assetMock.create.mockResolvedValue(assetStub.image);
storageMock.checkFileExists.mockResolvedValue(true);
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
await expect(sut.handleSyncFile(mockLibraryJob)).resolves.toBe(JobStatus.SUCCESS);
expect(assetMock.create.mock.calls).toEqual([
[
{
ownerId: mockUser.id,
libraryId: libraryStub.externalLibrary1.id,
checksum: expect.any(Buffer),
originalPath: '/data/user1/photo.jpg',
deviceAssetId: expect.any(String),
deviceId: 'Library Import',
fileCreatedAt: expect.any(Date),
fileModifiedAt: expect.any(Date),
localDateTime: expect.any(Date),
type: AssetType.IMAGE,
originalFileName: 'photo.jpg',
sidecarPath: '/data/user1/photo.jpg.xmp',
isExternal: true,
},
],
]);
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.METADATA_EXTRACTION,
data: {
id: assetStub.image.id,
source: 'upload',
},
},
],
@@ -507,7 +458,6 @@ describe(LibraryService.name, () => {
localDateTime: expect.any(Date),
type: AssetType.VIDEO,
originalFileName: 'video.mp4',
sidecarPath: null,
isExternal: true,
},
],
@@ -516,10 +466,9 @@ describe(LibraryService.name, () => {
expect(jobMock.queue.mock.calls).toEqual([
[
{
name: JobName.METADATA_EXTRACTION,
name: JobName.SIDECAR_DISCOVERY,
data: {
id: assetStub.image.id,
source: 'upload',
},
},
],

View File

@@ -396,12 +396,6 @@ export class LibraryService extends BaseService {
const pathHash = this.cryptoRepository.hashSha1(`path:${assetPath}`);
// TODO: doesn't xmp replace the file extension? Will need investigation
let sidecarPath: string | null = null;
if (await this.storageRepository.checkFileExists(`${assetPath}.xmp`, R_OK)) {
sidecarPath = `${assetPath}.xmp`;
}
const assetType = mimeTypes.isVideo(assetPath) ? AssetType.VIDEO : AssetType.IMAGE;
const mtime = stat.mtime;
@@ -418,8 +412,6 @@ export class LibraryService extends BaseService {
localDateTime: mtime,
type: assetType,
originalFileName: parse(assetPath).base,
sidecarPath,
isExternal: true,
});
@@ -431,7 +423,11 @@ export class LibraryService extends BaseService {
async queuePostSyncJobs(asset: AssetEntity) {
this.logger.debug(`Queueing metadata extraction for: ${asset.originalPath}`);
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
// We queue a sidecar discovery which, in turn, queues metadata extraction
await this.jobRepository.queue({
name: JobName.SIDECAR_DISCOVERY,
data: { id: asset.id },
});
}
async queueScan(id: string) {

View File

@@ -698,7 +698,7 @@ export class MetadataService extends BaseService {
return JobStatus.FAILED;
}
if (!isSync && (!asset.isVisible || asset.sidecarPath)) {
if (!isSync && (!asset.isVisible || asset.sidecarPath) && !asset.isExternal) {
return JobStatus.FAILED;
}
@@ -720,6 +720,13 @@ export class MetadataService extends BaseService {
sidecarPath = sidecarPathWithoutExt;
}
if (asset.isExternal) {
if (sidecarPath !== asset.sidecarPath) {
await this.assetRepository.update({ id: asset.id, sidecarPath });
}
return JobStatus.SUCCESS;
}
if (sidecarPath) {
await this.assetRepository.update({ id: asset.id, sidecarPath });
return JobStatus.SUCCESS;

View File

@@ -32,7 +32,7 @@ async function bootstrap() {
logger.setContext('Bootstrap');
app.useLogger(logger);
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal', ...network.trustedProxies]);
app.set('trust proxy', ['loopback', ...network.trustedProxies]);
app.set('etag', 'strong');
app.use(cookieParser());
app.use(json({ limit: '10mb' }));

View File

@@ -1,4 +1,4 @@
FROM node:22.11.0-alpine3.20@sha256:b64ced2e7cd0a4816699fe308ce6e8a08ccba463c757c00c14cd372e3d2c763e
FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f
RUN apk add --no-cache tini
USER node

313
web/package-lock.json generated
View File

@@ -37,7 +37,7 @@
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.4.0",
"@sveltejs/kit": "^2.7.2",
"@sveltejs/kit": "^2.12.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4",
@@ -54,7 +54,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"factory.ts": "^1.4.1",
"globals": "^15.9.0",
"postcss": "^8.4.35",
@@ -80,7 +80,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^22.9.0",
"@types/node": "^22.10.2",
"typescript": "^5.3.3"
}
},
@@ -1709,9 +1709,9 @@
"dev": true
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz",
"integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.28.1.tgz",
"integrity": "sha512-2aZp8AES04KI2dy3Ss6/MDjXbwBzj+i0GqKtWXgw2/Ma6E4jJvujryO6gJAghIRVz7Vwr9Gtl/8na3nDUKpraQ==",
"cpu": [
"arm"
],
@@ -1723,9 +1723,9 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz",
"integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.28.1.tgz",
"integrity": "sha512-EbkK285O+1YMrg57xVA+Dp0tDBRB93/BZKph9XhMjezf6F4TpYjaUSuPt5J0fZXlSag0LmZAsTmdGGqPp4pQFA==",
"cpu": [
"arm64"
],
@@ -1737,9 +1737,9 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz",
"integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.28.1.tgz",
"integrity": "sha512-prduvrMKU6NzMq6nxzQw445zXgaDBbMQvmKSJaxpaZ5R1QDM8w+eGxo6Y/jhT/cLoCvnZI42oEqf9KQNYz1fqQ==",
"cpu": [
"arm64"
],
@@ -1751,9 +1751,9 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz",
"integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.28.1.tgz",
"integrity": "sha512-WsvbOunsUk0wccO/TV4o7IKgloJ942hVFK1CLatwv6TJspcCZb9umQkPdvB7FihmdxgaKR5JyxDjWpCOp4uZlQ==",
"cpu": [
"x64"
],
@@ -1764,10 +1764,38 @@
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.28.1.tgz",
"integrity": "sha512-HTDPdY1caUcU4qK23FeeGxCdJF64cKkqajU0iBnTVxS8F7H/7BewvYoG+va1KPSL63kQ1PGNyiwKOfReavzvNA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.28.1.tgz",
"integrity": "sha512-m/uYasxkUevcFTeRSM9TeLyPe2QDuqtjkeoTpP9SW0XxUWfcYrGDMkO/m2tTw+4NMAF9P2fU3Mw4ahNvo7QmsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz",
"integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.28.1.tgz",
"integrity": "sha512-QAg11ZIt6mcmzpNE6JZBpKfJaKkqTm1A9+y9O+frdZJEuhQxiugM05gnCWiANHj4RmbgeVJpTdmKRmH/a+0QbA==",
"cpu": [
"arm"
],
@@ -1779,9 +1807,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz",
"integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.28.1.tgz",
"integrity": "sha512-dRP9PEBfolq1dmMcFqbEPSd9VlRuVWEGSmbxVEfiq2cs2jlZAl0YNxFzAQS2OrQmsLBLAATDMb3Z6MFv5vOcXg==",
"cpu": [
"arm"
],
@@ -1793,9 +1821,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz",
"integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.28.1.tgz",
"integrity": "sha512-uGr8khxO+CKT4XU8ZUH1TTEUtlktK6Kgtv0+6bIFSeiSlnGJHG1tSFSjm41uQ9sAO/5ULx9mWOz70jYLyv1QkA==",
"cpu": [
"arm64"
],
@@ -1807,9 +1835,9 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz",
"integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.28.1.tgz",
"integrity": "sha512-QF54q8MYGAqMLrX2t7tNpi01nvq5RI59UBNx+3+37zoKX5KViPo/gk2QLhsuqok05sSCRluj0D00LzCwBikb0A==",
"cpu": [
"arm64"
],
@@ -1820,10 +1848,24 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.28.1.tgz",
"integrity": "sha512-vPul4uodvWvLhRco2w0GcyZcdyBfpfDRgNKU+p35AWEbJ/HPs1tOUrkSueVbBS0RQHAf/A+nNtDpvw95PeVKOA==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz",
"integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.28.1.tgz",
"integrity": "sha512-pTnTdBuC2+pt1Rmm2SV7JWRqzhYpEILML4PKODqLz+C7Ou2apEV52h19CR7es+u04KlqplggmN9sqZlekg3R1A==",
"cpu": [
"ppc64"
],
@@ -1835,9 +1877,9 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz",
"integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.28.1.tgz",
"integrity": "sha512-vWXy1Nfg7TPBSuAncfInmAI/WZDd5vOklyLJDdIRKABcZWojNDY0NJwruY2AcnCLnRJKSaBgf/GiJfauu8cQZA==",
"cpu": [
"riscv64"
],
@@ -1849,9 +1891,9 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz",
"integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.28.1.tgz",
"integrity": "sha512-/yqC2Y53oZjb0yz8PVuGOQQNOTwxcizudunl/tFs1aLvObTclTwZ0JhXF2XcPT/zuaymemCDSuuUPXJJyqeDOg==",
"cpu": [
"s390x"
],
@@ -1863,9 +1905,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz",
"integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.28.1.tgz",
"integrity": "sha512-fzgeABz7rrAlKYB0y2kSEiURrI0691CSL0+KXwKwhxvj92VULEDQLpBYLHpF49MSiPG4sq5CK3qHMnb9tlCjBw==",
"cpu": [
"x64"
],
@@ -1877,9 +1919,9 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz",
"integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.28.1.tgz",
"integrity": "sha512-xQTDVzSGiMlSshpJCtudbWyRfLaNiVPXt1WgdWTwWz9n0U12cI2ZVtWe/Jgwyv/6wjL7b66uu61Vg0POWVfz4g==",
"cpu": [
"x64"
],
@@ -1891,9 +1933,9 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz",
"integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.28.1.tgz",
"integrity": "sha512-wSXmDRVupJstFP7elGMgv+2HqXelQhuNf+IS4V+nUpNVi/GUiBgDmfwD0UGN3pcAnWsgKG3I52wMOBnk1VHr/A==",
"cpu": [
"arm64"
],
@@ -1905,9 +1947,9 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz",
"integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.28.1.tgz",
"integrity": "sha512-ZkyTJ/9vkgrE/Rk9vhMXhf8l9D+eAhbAVbsGsXKy2ohmJaWg0LPQLnIxRdRp/bKyr8tXuPlXhIoGlEB5XpJnGA==",
"cpu": [
"ia32"
],
@@ -1919,9 +1961,9 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz",
"integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.28.1.tgz",
"integrity": "sha512-ZvK2jBafvttJjoIdKm/Q/Bh7IJ1Ose9IBOwpOXcOvW3ikGTQGmKDgxTC6oCAzW6PynbkKP8+um1du81XJHZ0JA==",
"cpu": [
"x64"
],
@@ -1965,9 +2007,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.8.3.tgz",
"integrity": "sha512-DVBVwugfzzn0SxKA+eAmKqcZ7aHZROCHxH7/pyrOi+HLtQ721eEsctGb9MkhEuqj6q/9S/OFYdn37vdxzFPdvw==",
"version": "2.13.0",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.13.0.tgz",
"integrity": "sha512-6t6ne00vZx/TjD6s0Jvwt8wRLKBwbSAN1nhlOzcLUSTYX1hTp4eCBaTPB5Yz/lu+tYcvz4YPEEuPv3yfsNp2gw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -1975,7 +2017,7 @@
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
"devalue": "^5.1.0",
"esm-env": "^1.0.0",
"esm-env": "^1.2.1",
"import-meta-resolve": "^4.1.0",
"kleur": "^4.1.5",
"magic-string": "^0.30.5",
@@ -1992,9 +2034,9 @@
"node": ">=18.13"
},
"peerDependencies": {
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1",
"@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0",
"svelte": "^4.0.0 || ^5.0.0-next.0",
"vite": "^5.0.3"
"vite": "^5.0.3 || ^6.0.0"
}
},
"node_modules/@sveltejs/vite-plugin-svelte": {
@@ -2285,9 +2327,10 @@
"dev": true
},
"node_modules/@types/estree": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
"integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw=="
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/@types/geojson": {
"version": "7946.0.14",
@@ -3039,9 +3082,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"version": "4.24.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz",
"integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==",
"dev": true,
"funding": [
{
@@ -3059,10 +3102,10 @@
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
"node-releases": "^2.0.19",
"update-browserslist-db": "^1.1.1"
},
"bin": {
"browserslist": "cli.js"
@@ -3135,9 +3178,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"version": "1.0.30001689",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001689.tgz",
"integrity": "sha512-CmeR2VBycfa+5/jOfnp/NpWPGd06nf1XYiefUvhXFfZE4GkRc9jv+eGPS4nT558WS/8lYCzV8SlANCIPvbWP1g==",
"dev": true,
"funding": [
{
@@ -3398,12 +3441,13 @@
}
},
"node_modules/core-js-compat": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz",
"integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==",
"version": "3.39.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.39.0.tgz",
"integrity": "sha512-VgEUx3VwlExr5no0tXlBt+silBvhTryPwCXRI2Id1PN8WTKu7MreethvddqOubrYxkFdv/RnYrqlv1sFNAUelw==",
"dev": true,
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0"
"browserslist": "^4.24.2"
},
"funding": {
"type": "opencollective",
@@ -3653,9 +3697,9 @@
"dev": true
},
"node_modules/electron-to-chromium": {
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
"integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
"version": "1.5.74",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.74.tgz",
"integrity": "sha512-ck3//9RC+6oss/1Bh9tiAVFy5vfSKbRHAFh7Z3/eTRkEqJeWgymloShB17Vg3Z4nmDNp35vAd1BZ6CMW4Wt6Iw==",
"dev": true,
"license": "ISC"
},
@@ -3805,9 +3849,9 @@
}
},
"node_modules/escalade": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
"integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
@@ -3948,19 +3992,19 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "55.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz",
"integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==",
"version": "56.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-56.0.1.tgz",
"integrity": "sha512-FwVV0Uwf8XPfVnKSGpMg7NtlZh0G0gBarCaFcMUOoqPxXryxdYxTRRv4kH6B9TFCVIrjRXG+emcxIk2ayZilog==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@babel/helper-validator-identifier": "^7.24.7",
"@eslint-community/eslint-utils": "^4.4.0",
"ci-info": "^4.0.0",
"clean-regexp": "^1.0.0",
"core-js-compat": "^3.37.0",
"esquery": "^1.5.0",
"globals": "^15.7.0",
"core-js-compat": "^3.38.1",
"esquery": "^1.6.0",
"globals": "^15.9.0",
"indent-string": "^4.0.0",
"is-builtin-module": "^3.2.1",
"jsesc": "^3.0.2",
@@ -3968,7 +4012,7 @@
"read-pkg-up": "^7.0.1",
"regexp-tree": "^0.1.27",
"regjsparser": "^0.10.0",
"semver": "^7.6.1",
"semver": "^7.6.3",
"strip-indent": "^3.0.0"
},
"engines": {
@@ -4046,13 +4090,6 @@
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/eslint/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"dev": true,
"license": "MIT"
},
"node_modules/eslint/node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -4191,9 +4228,10 @@
}
},
"node_modules/esm-env": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.0.0.tgz",
"integrity": "sha512-Cf6VksWPsTuW01vU9Mk/3vRue91Zevka5SjyNf3nEpokFRuqt/KjUQoGAwq9qMmhpLTHmXzSIrFRw8zxWzmFBA=="
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/esm-env/-/esm-env-1.2.1.tgz",
"integrity": "sha512-U9JedYYjCnadUlXk7e1Kr+aENQhtUaoaV9+gZm1T8LC/YBAPJx3NSPIAurFOC0U5vrdSevnUJS2/wUVxGwPhng==",
"license": "MIT"
},
"node_modules/esniff": {
"version": "2.0.1",
@@ -4227,10 +4265,11 @@
}
},
"node_modules/esquery": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
"integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
@@ -5056,12 +5095,6 @@
"@types/estree": "^1.0.6"
}
},
"node_modules/is-reference/node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
"integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
"license": "MIT"
},
"node_modules/is-wsl": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
@@ -5717,9 +5750,9 @@
}
},
"node_modules/nanoid": {
"version": "3.3.7",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
"integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
"integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
"dev": true,
"funding": [
{
@@ -5727,6 +5760,7 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
@@ -5751,9 +5785,9 @@
"integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ=="
},
"node_modules/node-releases": {
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"version": "2.0.19",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz",
"integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==",
"dev": true,
"license": "MIT"
},
@@ -6641,13 +6675,13 @@
}
},
"node_modules/rollup": {
"version": "4.21.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz",
"integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==",
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.28.1.tgz",
"integrity": "sha512-61fXYl/qNVinKmGSTHAZ6Yy8I3YIJC/r2m9feHo6SwVAVcLT5MPwOUFe7EuURA/4m0NR8lXG4BBXuo/IZEsjMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
"@types/estree": "1.0.6"
},
"bin": {
"rollup": "dist/bin/rollup"
@@ -6657,22 +6691,25 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.21.1",
"@rollup/rollup-android-arm64": "4.21.1",
"@rollup/rollup-darwin-arm64": "4.21.1",
"@rollup/rollup-darwin-x64": "4.21.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.21.1",
"@rollup/rollup-linux-arm-musleabihf": "4.21.1",
"@rollup/rollup-linux-arm64-gnu": "4.21.1",
"@rollup/rollup-linux-arm64-musl": "4.21.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.21.1",
"@rollup/rollup-linux-riscv64-gnu": "4.21.1",
"@rollup/rollup-linux-s390x-gnu": "4.21.1",
"@rollup/rollup-linux-x64-gnu": "4.21.1",
"@rollup/rollup-linux-x64-musl": "4.21.1",
"@rollup/rollup-win32-arm64-msvc": "4.21.1",
"@rollup/rollup-win32-ia32-msvc": "4.21.1",
"@rollup/rollup-win32-x64-msvc": "4.21.1",
"@rollup/rollup-android-arm-eabi": "4.28.1",
"@rollup/rollup-android-arm64": "4.28.1",
"@rollup/rollup-darwin-arm64": "4.28.1",
"@rollup/rollup-darwin-x64": "4.28.1",
"@rollup/rollup-freebsd-arm64": "4.28.1",
"@rollup/rollup-freebsd-x64": "4.28.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.28.1",
"@rollup/rollup-linux-arm-musleabihf": "4.28.1",
"@rollup/rollup-linux-arm64-gnu": "4.28.1",
"@rollup/rollup-linux-arm64-musl": "4.28.1",
"@rollup/rollup-linux-loongarch64-gnu": "4.28.1",
"@rollup/rollup-linux-powerpc64le-gnu": "4.28.1",
"@rollup/rollup-linux-riscv64-gnu": "4.28.1",
"@rollup/rollup-linux-s390x-gnu": "4.28.1",
"@rollup/rollup-linux-x64-gnu": "4.28.1",
"@rollup/rollup-linux-x64-musl": "4.28.1",
"@rollup/rollup-win32-arm64-msvc": "4.28.1",
"@rollup/rollup-win32-ia32-msvc": "4.28.1",
"@rollup/rollup-win32-x64-msvc": "4.28.1",
"fsevents": "~2.3.2"
}
},
@@ -8285,9 +8322,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz",
"integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==",
"dev": true,
"funding": [
{
@@ -8305,8 +8342,8 @@
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
"escalade": "^3.2.0",
"picocolors": "^1.1.0"
},
"bin": {
"update-browserslist-db": "cli.js"

View File

@@ -29,7 +29,7 @@
"@socket.io/component-emitter": "^3.1.0",
"@sveltejs/adapter-static": "^3.0.5",
"@sveltejs/enhanced-img": "^0.4.0",
"@sveltejs/kit": "^2.7.2",
"@sveltejs/kit": "^2.12.0",
"@sveltejs/vite-plugin-svelte": "^4.0.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/svelte": "^5.2.4",
@@ -46,7 +46,7 @@
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.45.1",
"eslint-plugin-unicorn": "^55.0.0",
"eslint-plugin-unicorn": "^56.0.1",
"factory.ts": "^1.4.1",
"globals": "^15.9.0",
"postcss": "^8.4.35",

View File

@@ -29,7 +29,7 @@
// click runs before bind
const previouslyEnabled = config.oauth.mobileOverrideEnabled;
if (!previouslyEnabled && !config.oauth.mobileRedirectUri) {
config.oauth.mobileRedirectUri = window.location.origin + '/api/oauth/mobile-redirect';
config.oauth.mobileRedirectUri = globalThis.location.origin + '/api/oauth/mobile-redirect';
}
};

View File

@@ -0,0 +1,29 @@
<script lang="ts">
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import {
notificationController,
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import { updatePerson, type AssetResponseDto, type PersonResponseDto } from '@immich/sdk';
import { mdiFaceManProfile } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
person: PersonResponseDto;
}
let { asset, person }: Props = $props();
const handleSelectFeaturePhoto = async () => {
try {
await updatePerson({ id: person.id, personUpdateDto: { featureFaceAssetId: asset.id } });
notificationController.show({ message: $t('feature_photo_updated'), type: NotificationType.Info });
} catch (error) {
handleError(error, $t('errors.unable_to_set_feature_photo'));
}
};
</script>
<MenuOption text={$t('set_as_featured_photo')} icon={mdiFaceManProfile} onClick={handleSelectFeaturePhoto} />

View File

@@ -9,6 +9,7 @@
import FavoriteAction from '$lib/components/asset-viewer/actions/favorite-action.svelte';
import RestoreAction from '$lib/components/asset-viewer/actions/restore-action.svelte';
import SetAlbumCoverAction from '$lib/components/asset-viewer/actions/set-album-cover-action.svelte';
import SetFeaturedPhotoAction from '$lib/components/asset-viewer/actions/set-person-featured-action.svelte';
import SetProfilePictureAction from '$lib/components/asset-viewer/actions/set-profile-picture-action.svelte';
import ShareAction from '$lib/components/asset-viewer/actions/share-action.svelte';
import ShowDetailAction from '$lib/components/asset-viewer/actions/show-detail-action.svelte';
@@ -27,6 +28,7 @@
AssetTypeEnum,
type AlbumResponseDto,
type AssetResponseDto,
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import {
@@ -50,6 +52,7 @@
interface Props {
asset: AssetResponseDto;
album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
stack?: StackResponseDto | null;
showDetailButton: boolean;
showSlideshow?: boolean;
@@ -67,6 +70,7 @@
let {
asset,
album = null,
person = null,
stack = null,
showDetailButton,
showSlideshow = false,
@@ -169,6 +173,9 @@
{#if album}
<SetAlbumCoverAction {asset} {album} />
{/if}
{#if person}
<SetFeaturedPhotoAction {asset} {person} />
{/if}
{#if asset.type === AssetTypeEnum.Image}
<SetProfilePictureAction {asset} />
{/if}

View File

@@ -30,6 +30,7 @@
type ActivityResponseDto,
type AlbumResponseDto,
type AssetResponseDto,
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
import { onDestroy, onMount, untrack } from 'svelte';
@@ -56,6 +57,7 @@
withStacked?: boolean;
isShared?: boolean;
album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
onAction?: OnAction | undefined;
reactions?: ActivityResponseDto[];
onClose: (dto: { asset: AssetResponseDto }) => void;
@@ -72,6 +74,7 @@
withStacked = false,
isShared = false,
album = null,
person = null,
onAction = undefined,
reactions = $bindable([]),
onClose,
@@ -429,6 +432,7 @@
<AssetViewerNavBar
{asset}
{album}
{person}
{stack}
showDetailButton={enableDetailPanel}
showSlideshow={!!assetStore}

View File

@@ -57,11 +57,11 @@
handleError(error, $t('error_loading_image'));
});
window.addEventListener('mousemove', handleMouseMove);
globalThis.addEventListener('mousemove', handleMouseMove);
});
onDestroy(() => {
window.removeEventListener('mousemove', handleMouseMove);
globalThis.removeEventListener('mousemove', handleMouseMove);
resetCropStore();
resetGlobalCropStore();
});

View File

@@ -58,7 +58,7 @@ export function handleMouseDown(e: MouseEvent) {
}
document.body.style.userSelect = 'none';
window.addEventListener('mouseup', handleMouseUp);
globalThis.addEventListener('mouseup', handleMouseUp);
}
export function handleMouseMove(e: MouseEvent) {
@@ -80,7 +80,7 @@ export function handleMouseMove(e: MouseEvent) {
}
export function handleMouseUp() {
window.removeEventListener('mouseup', handleMouseUp);
globalThis.removeEventListener('mouseup', handleMouseUp);
document.body.style.userSelect = '';
stopInteraction();
}

View File

@@ -107,7 +107,7 @@
};
const onCopyShortcut = (event: KeyboardEvent) => {
if (window.getSelection()?.type === 'Range') {
if (globalThis.getSelection()?.type === 'Range') {
return;
}
event.preventDefault();

View File

@@ -1,5 +1,5 @@
<script lang="ts" module>
export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert';
export type Color = 'transparent' | 'light' | 'dark' | 'red' | 'gray' | 'primary' | 'opaque' | 'alert' | 'neutral';
export type Padding = '1' | '2' | '3';
</script>
@@ -68,6 +68,8 @@
dark: 'bg-[#202123] hover:bg-[#d3d3d3]',
alert: 'text-[#ff0000] hover:text-white',
gray: 'bg-[#d3d3d3] hover:bg-[#e2e7e9] text-immich-dark-gray hover:text-black',
neutral:
'dark:bg-immich-dark-gray dark:text-gray-300 hover:dark:bg-immich-dark-gray/50 hover:dark:text-gray-300 bg-gray-200 text-gray-700 hover:bg-gray-300',
primary:
'bg-immich-primary dark:bg-immich-dark-primary hover:bg-immich-primary/75 hover:dark:bg-immich-dark-primary/80 text-white dark:text-immich-dark-gray',
};

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import Icon from '$lib/components/elements/icon.svelte';
import { ActionQueryParameterValue, AppRoute, QueryParameter } from '$lib/constants';
import { handleError } from '$lib/utils/handle-error';
@@ -34,15 +34,17 @@
let hasSelection = $derived(selectedPeople.length > 0);
let peopleToNotShow = $derived([...selectedPeople, person]);
onMount(async () => {
const data = await getAllPeople({ withHidden: false, closestPersonId: person.id });
const handleSearch = async (sortFaces: boolean = false) => {
const data = await getAllPeople({ withHidden: false, closestPersonId: sortFaces ? person.id : undefined });
people = data.people;
});
};
onMount(handleSearch);
const handleSwapPeople = async () => {
[person, selectedPeople[0]] = [selectedPeople[0], person];
$page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
await goto(`${AppRoute.PEOPLE}/${person.id}?${$page.url.searchParams.toString()}`);
page.url.searchParams.set(QueryParameter.ACTION, ActionQueryParameterValue.MERGE);
await goto(`${AppRoute.PEOPLE}/${person.id}?${page.url.searchParams.toString()}`);
};
const onSelect = async (selected: PersonResponseDto) => {
@@ -149,8 +151,7 @@
<FaceThumbnail {person} border circle selectable={false} thumbnailSize={180} />
</div>
</div>
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} />
<PeopleList {people} {peopleToNotShow} {screenHeight} {onSelect} {handleSearch} />
</section>
</section>
</section>

View File

@@ -3,18 +3,20 @@
import FaceThumbnail from './face-thumbnail.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
import { t } from 'svelte-i18n';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { mdiSwapVertical } from '@mdi/js';
interface Props {
screenHeight: number;
people: PersonResponseDto[];
peopleToNotShow: PersonResponseDto[];
onSelect: (person: PersonResponseDto) => void;
handleSearch?: (sortFaces: boolean) => void;
}
let { screenHeight, people, peopleToNotShow, onSelect }: Props = $props();
let { screenHeight, people, peopleToNotShow, onSelect, handleSearch }: Props = $props();
let searchedPeopleLocal: PersonResponseDto[] = $state([]);
let sortBySimilarirty = $state(false);
let name = $state('');
const showPeople = $derived(
@@ -24,12 +26,26 @@
);
</script>
<div class=" w-40 sm:w-48 md:w-96 h-14 mb-8">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
<div class="w-40 sm:w-48 md:w-full h-14 flex gap-4 place-items-center">
<div class="md:w-96">
<SearchPeople type="searchBar" placeholder={$t('search_people')} bind:searchName={name} bind:searchedPeopleLocal />
</div>
{#if handleSearch}
<CircleIconButton
icon={mdiSwapVertical}
onclick={() => {
sortBySimilarirty = !sortBySimilarirty;
handleSearch(sortBySimilarirty);
}}
color="neutral"
title={$t('sort_people_by_similarity')}
></CircleIconButton>
{/if}
</div>
<div
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray"
class="immich-scrollbar overflow-y-auto rounded-3xl bg-gray-200 p-10 dark:bg-immich-dark-gray mt-6"
style:max-height={screenHeight - 400 + 'px'}
>
<div class="grid-col-2 grid gap-8 md:grid-cols-3 lg:grid-cols-6 xl:grid-cols-8 2xl:grid-cols-10">

View File

@@ -11,7 +11,6 @@
let { onSuccess }: Props = $props();
let errorMessage: string = $state('');
let success: string;
let password = $state('');
let passwordConfirm = $state('');
@@ -59,9 +58,6 @@
<p class="text-sm text-red-400">{errorMessage}</p>
{/if}
{#if success}
<p class="text-sm text-immich-primary">{success}</p>
{/if}
<div class="my-5 flex w-full">
<Button type="submit" size="lg" fullwidth>{$t('to_change_password')}</Button>
</div>

View File

@@ -28,8 +28,6 @@
onEditSuccess,
}: Props = $props();
let error: string;
let success: string;
let quotaSize = $state(user.quotaSizeInBytes ? convertFromBytes(user.quotaSizeInBytes, ByteUnit.GiB) : null);
const previousQutoa = user.quotaSizeInBytes;
@@ -149,14 +147,6 @@
</a>
</p>
</div>
{#if error}
<p class="ml-4 text-sm text-red-400">{error}</p>
{/if}
{#if success}
<p class="ml-4 text-sm text-immich-primary">{success}</p>
{/if}
</form>
{#snippet stickyBottom()}

View File

@@ -33,9 +33,9 @@
return;
}
if (oauth.isCallback(window.location)) {
if (oauth.isCallback(globalThis.location)) {
try {
await oauth.login(window.location);
await oauth.login(globalThis.location);
await onSuccess();
return;
} catch (error) {
@@ -46,9 +46,9 @@
}
try {
if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(window.location)) {
if ($featureFlags.oauthAutoLaunch && !oauth.isAutoLaunchDisabled(globalThis.location)) {
await goto(`${AppRoute.AUTH_LOGIN}?autoLaunch=0`, { replaceState: true });
await oauth.authorize(window.location);
await oauth.authorize(globalThis.location);
return;
}
} catch (error) {
@@ -85,7 +85,7 @@
const handleOAuthLogin = async () => {
oauthLoading = true;
oauthError = '';
const success = await oauth.authorize(window.location);
const success = await oauth.authorize(globalThis.location);
if (!success) {
oauthLoading = false;
oauthError = $t('errors.unable_to_login_with_oauth');

View File

@@ -5,10 +5,8 @@
import Combobox, { type ComboBoxOption } from '../shared-components/combobox.svelte';
import FullScreenModal from '../shared-components/full-screen-modal.svelte';
import { onMount } from 'svelte';
import { getAllTags, type TagResponseDto } from '@immich/sdk';
import { getAllTags, upsertTags, type TagResponseDto } from '@immich/sdk';
import Icon from '$lib/components/elements/icon.svelte';
import { AppRoute } from '$lib/constants';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import { SvelteSet } from 'svelte/reactivity';
interface Props {
@@ -22,6 +20,7 @@
let tagMap = $derived(Object.fromEntries(allTags.map((tag) => [tag.id, tag])));
let selectedIds = $state(new SvelteSet<string>());
let disabled = $derived(selectedIds.size === 0);
let allowCreate: boolean = $state(true);
onMount(async () => {
allTags = await getAllTags();
@@ -29,12 +28,18 @@
const handleSubmit = () => onTag([...selectedIds]);
const handleSelect = (option?: ComboBoxOption) => {
const handleSelect = async (option?: ComboBoxOption) => {
if (!option) {
return;
}
selectedIds.add(option.value);
if (option.id) {
selectedIds.add(option.value);
} else {
const [newTag] = await upsertTags({ tagUpsertDto: { tags: [option.label] } });
allTags.push(newTag);
selectedIds.add(newTag.id);
}
};
const handleRemove = (tag: string) => {
@@ -48,22 +53,13 @@
</script>
<FullScreenModal title={$t('tag_assets')} icon={mdiTag} onClose={onCancel}>
<div class="text-sm">
<p>
<FormatMessage key="tag_not_found_question">
{#snippet children({ message })}
<a href={AppRoute.TAGS} class="text-immich-primary dark:text-immich-dark-primary underline">
{message}
</a>
{/snippet}
</FormatMessage>
</p>
</div>
<form {onsubmit} autocomplete="off" id="create-tag-form">
<div class="my-4 flex flex-col gap-2">
<Combobox
onSelect={handleSelect}
label={$t('tag')}
{allowCreate}
defaultFirstOption
options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))}
placeholder={$t('search_tags')}
/>

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { InterpolationValues } from '$lib/components/i18n/format-message';
import FormatMessage from '$lib/components/i18n/format-message.svelte';
import type { InterpolationValues } from '$lib/components/i18n/format-message.svelte';
import type { Translations } from 'svelte-i18n';
interface Props {

View File

@@ -1,10 +1,5 @@
<script lang="ts" module>
import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat';
export type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;
</script>
<script lang="ts">
import { IntlMessageFormat } from 'intl-messageformat';
import { IntlMessageFormat, type FormatXMLElementFn } from 'intl-messageformat';
import {
TYPE,
type MessageFormatElement,
@@ -12,6 +7,7 @@
type SelectElement,
} from '@formatjs/icu-messageformat-parser';
import { locale as i18nLocale, json, type Translations } from 'svelte-i18n';
import type { InterpolationValues } from '$lib/components/i18n/format-message';
type MessagePart = {
message: string;

View File

@@ -0,0 +1,2 @@
import type { FormatXMLElementFn, PrimitiveType } from 'intl-messageformat';
export type InterpolationValues = Record<string, PrimitiveType | FormatXMLElementFn<unknown>>;

View File

@@ -19,7 +19,7 @@
type ScrollTargetListener,
} from '$lib/utils/timeline-util';
import { TUNABLES } from '$lib/utils/tunables';
import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk';
import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
import { throttle } from 'lodash-es';
import { onDestroy, onMount, type Snippet } from 'svelte';
import Portal from '../shared-components/portal/portal.svelte';
@@ -52,6 +52,7 @@
showArchiveIcon?: boolean;
isShared?: boolean;
album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
isShowDeleteConfirmation?: boolean;
onSelect?: (asset: AssetResponseDto) => void;
onEscape?: () => void;
@@ -70,6 +71,7 @@
showArchiveIcon = false,
isShared = false,
album = null,
person = null,
isShowDeleteConfirmation = $bindable(false),
onSelect = () => {},
onEscape = () => {},
@@ -914,6 +916,7 @@
preloadAssets={$preloadAssets}
{isShared}
{album}
{person}
onAction={handleAction}
onPrevious={handlePrevious}
onNext={handleNext}

View File

@@ -69,6 +69,7 @@
}
showLoadingSpinner = true;
// eslint-disable-next-line unicorn/prefer-global-this
const searchTimeout = window.setTimeout(() => {
if (searchWord === '') {
places = [];

View File

@@ -36,6 +36,14 @@
options?: ComboBoxOption[];
selectedOption?: ComboBoxOption | undefined;
placeholder?: string;
/**
* whether creating new items is allowed.
*/
allowCreate?: boolean;
/**
* select first matching option on enter key.
*/
defaultFirstOption?: boolean;
onSelect?: (option: ComboBoxOption | undefined) => void;
}
@@ -45,6 +53,8 @@
options = [],
selectedOption = $bindable(),
placeholder = '',
allowCreate = false,
defaultFirstOption = false,
onSelect = () => {},
}: Props = $props();
@@ -141,7 +151,7 @@
const onInput: FormEventHandler<HTMLInputElement> = (event) => {
openDropdown();
searchQuery = event.currentTarget.value;
selectedIndex = undefined;
selectedIndex = defaultFirstOption ? 0 : undefined;
optionRefs[0]?.scrollIntoView({ block: 'nearest' });
};
@@ -221,9 +231,15 @@
searchQuery = selectedOption ? selectedOption.label : '';
});
let filteredOptions = $derived(
options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase())),
);
let filteredOptions = $derived.by(() => {
const _options = options.filter((option) => option.label.toLowerCase().includes(searchQuery.toLowerCase()));
if (allowCreate && searchQuery !== '' && _options.filter((option) => option.label === searchQuery).length === 0) {
_options.unshift({ label: searchQuery, value: searchQuery });
}
return _options;
});
let position = $derived(calculatePosition(bounds));
let dropdownDirection: 'bottom' | 'top' = $derived(getComboboxDirection(bounds, visualViewport));
</script>
@@ -352,7 +368,7 @@
id={`${listboxId}-${0}`}
onclick={() => closeDropdown()}
>
{$t('no_results')}
{allowCreate ? searchQuery : $t('no_results')}
</li>
{/if}
{#each filteredOptions as option, index (option.id || option.label)}

View File

@@ -29,6 +29,7 @@
const contextMenuEvent = new MouseEvent('contextmenu', {
bubbles: true,
cancelable: true,
// eslint-disable-next-line unicorn/prefer-global-this
view: window,
clientX: event.x,
clientY: event.y,

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { shouldIgnoreEvent } from '$lib/actions/shortcut';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler } from '$lib/utils/file-uploader';
@@ -8,8 +8,8 @@
import { fade } from 'svelte/transition';
import ImmichLogo from './immich-logo.svelte';
let albumId = $derived(isAlbumsRoute($page.route?.id) ? $page.params.albumId : undefined);
let isShare = $derived(isSharedLinkRoute($page.route?.id));
let albumId = $derived(isAlbumsRoute(page.route?.id) ? page.params.albumId : undefined);
let isShare = $derived(isSharedLinkRoute(page.route?.id));
let dragStartTarget: EventTarget | null = $state(null);

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { focusTrap } from '$lib/actions/focus-trap';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
@@ -90,7 +90,7 @@
size="sm"
shadow={false}
border
aria-current={$page.url.pathname.includes('/admin') ? 'page' : undefined}
aria-current={page.url.pathname.includes('/admin') ? 'page' : undefined}
>
<div class="flex place-content-center place-items-center text-center gap-2 px-2">
<Icon path={mdiWrench} size="18" ariaHidden />

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
import { clickOutside } from '$lib/actions/click-outside';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import LinkButton from '$lib/components/elements/buttons/link-button.svelte';
@@ -96,7 +96,7 @@
/>
</div>
{#if !$page.url.pathname.includes('/admin') && showUploadButton}
{#if !page.url.pathname.includes('/admin') && showUploadButton}
<LinkButton onclick={onUploadClick} class="hidden lg:block">
<div class="flex gap-2">
<Icon path={mdiTrayArrowUp} size="1.5em" />

View File

@@ -5,10 +5,10 @@ import { NotificationType } from '../notification';
import NotificationCard from '../notification-card.svelte';
describe('NotificationCard component', () => {
let sut: RenderResult<NotificationCard>;
let sut: RenderResult<typeof NotificationCard>;
it('disposes timeout if already removed from the DOM', () => {
vi.spyOn(window, 'clearTimeout');
vi.spyOn(globalThis, 'clearTimeout');
sut = render(NotificationCard, {
notification: {
@@ -21,7 +21,7 @@ describe('NotificationCard component', () => {
});
cleanup();
expect(window.clearTimeout).toHaveBeenCalledTimes(1);
expect(globalThis.clearTimeout).toHaveBeenCalledTimes(1);
});
it('shows message and title', () => {

View File

@@ -8,13 +8,13 @@
<script lang="ts">
import { writable, type Writable } from 'svelte/store';
import { createContext } from '$lib/utils/context';
import { page } from '$app/stores';
import { handlePromiseError } from '$lib/utils';
import { page } from '$app/state';
import { goto } from '$app/navigation';
import type { Snippet } from 'svelte';
import { handlePromiseError } from '$lib/utils';
const getParamValues = (param: string) => {
return new Set(($page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
return new Set((page.url.searchParams.get(param) || '').split(' ').filter((x) => x !== ''));
};
interface Props {
@@ -26,17 +26,16 @@
let { queryParam, state = writable(getParamValues(queryParam)), children }: Props = $props();
setAccordionState(state);
$effect(() => {
if (queryParam && $state) {
const searchParams = new URLSearchParams($page.url.searchParams);
if ($state.size > 0) {
searchParams.set(queryParam, [...$state].join(' '));
} else {
searchParams.delete(queryParam);
}
const searchParams = new URLSearchParams(page.url.searchParams);
handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }));
$effect(() => {
if ($state.size > 0) {
searchParams.set(queryParam, [...$state].join(' '));
} else {
searchParams.delete(queryParam);
}
handlePromiseError(goto(`?${searchParams.toString()}`, { replaceState: true, noScroll: true, keepFocus: true }));
});
</script>

View File

@@ -2,7 +2,7 @@
import Icon from '$lib/components/elements/icon.svelte';
import { mdiChevronDown, mdiChevronLeft } from '@mdi/js';
import { resolveRoute } from '$app/paths';
import { page } from '$app/stores';
import { page } from '$app/state';
import type { Snippet } from 'svelte';
import { t } from 'svelte-i18n';
@@ -32,7 +32,7 @@
let routePath = $derived(resolveRoute(routeId, {}));
$effect(() => {
isSelected = ($page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId;
isSelected = (page.route.id?.match(/^\/(admin|\(user\))\/[^/]*/) || [])[0] === routeId;
});
</script>

View File

@@ -20,11 +20,11 @@
let loading = $state(true);
onMount(async () => {
if (oauth.isCallback(window.location)) {
if (oauth.isCallback(globalThis.location)) {
try {
loading = true;
user = await oauth.link(window.location);
user = await oauth.link(globalThis.location);
notificationController.show({
message: $t('linked_oauth_account'),
@@ -64,7 +64,7 @@
{#if user.oauthId}
<Button size="sm" onclick={() => handleUnlink()}>{$t('unlink_oauth')}</Button>
{:else}
<Button size="sm" onclick={() => oauth.authorize(window.location)}>{$t('link_to_oauth')}</Button>
<Button size="sm" onclick={() => oauth.authorize(globalThis.location)}>{$t('link_to_oauth')}</Button>
{/if}
{/if}
</div>

View File

@@ -64,10 +64,7 @@
for (const candidate of sharedWith) {
const existIndex = partners.findIndex((p) => candidate.id === p.user.id);
if (existIndex >= 0) {
partners[existIndex].sharedWithMe = true;
partners[existIndex].inTimeline = candidate.inTimeline ?? false;
} else {
if (existIndex === -1) {
partners = [
...partners,
{
@@ -77,6 +74,9 @@
inTimeline: candidate.inTimeline ?? false,
},
];
} else {
partners[existIndex].sharedWithMe = true;
partners[existIndex].inTimeline = candidate.inTimeline ?? false;
}
}
};

View File

@@ -43,7 +43,7 @@
let { keys = $bindable([]), sessions = $bindable([]) }: Props = $props();
let oauthOpen =
oauth.isCallback(window.location) ||
oauth.isCallback(globalThis.location) ||
$page.url.searchParams.get(QueryParameter.OPEN_SETTING) === OpenSettingQueryParameterValue.OAUTH;
</script>

View File

@@ -17,4 +17,14 @@
</span>
{$t('review_duplicates')}
</a>
<a
href={AppRoute.WORKFLOWS}
class="w-full hover:bg-gray-100 dark:hover:bg-immich-dark-gray flex items-center gap-4 p-4"
>
<span
><Icon path={mdiContentDuplicate} class="text-immich-primary dark:text-immich-dark-primary" size="24" />
</span>
{$t('workflows')}
</a>
</div>

View File

@@ -46,6 +46,7 @@ export enum AppRoute {
UTILITIES = '/utilities',
DUPLICATES = '/utilities/duplicates',
WORKFLOWS = '/utilities/workflows',
FOLDERS = '/folders',
TAGS = '/tags',

View File

@@ -398,7 +398,9 @@ export class AssetStore {
}
async updateOptions(options: AssetStoreOptions) {
if (!this.initialized) {
// Make sure to re-initialize if the personId changes
const needsReinitializing = this.options.personId !== options.personId;
if (!this.initialized && !needsReinitializing) {
this.setOptions(options);
return;
}

View File

@@ -16,7 +16,7 @@ export const handleToggleTheme = () => {
};
const initTheme = (): ThemeSetting => {
if (browser && window.matchMedia && !window.matchMedia('(prefers-color-scheme: dark)').matches) {
if (browser && globalThis.matchMedia && !globalThis.matchMedia('(prefers-color-scheme: dark)').matches) {
return { value: Theme.LIGHT, system: false };
}
return { value: Theme.DARK, system: false };

View File

@@ -261,7 +261,7 @@ export const copyToClipboard = async (secret: string) => {
};
export const makeSharedLinkUrl = (externalDomain: string, key: string) => {
return new URL(`share/${key}`, externalDomain || window.location.origin).href;
return new URL(`share/${key}`, externalDomain || globalThis.location.origin).href;
};
export const oauth = {
@@ -283,7 +283,7 @@ export const oauth = {
try {
const redirectUri = location.href.split('?')[0];
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri } });
window.location.href = url;
globalThis.location.href = url;
return true;
} catch (error) {
handleError(error, $t('errors.unable_to_login_with_oauth'));

View File

@@ -1,5 +1,6 @@
import { goto } from '$app/navigation';
import FormatBoldMessage from '$lib/components/i18n/format-bold-message.svelte';
import type { InterpolationValues } from '$lib/components/i18n/format-message';
import { NotificationType, notificationController } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
@@ -33,7 +34,7 @@ import {
type UserResponseDto,
} from '@immich/sdk';
import { DateTime } from 'luxon';
import { t } from 'svelte-i18n';
import { t, type Translations } from 'svelte-i18n';
import { get } from 'svelte/store';
import { handleError } from './handle-error';
@@ -120,7 +121,8 @@ export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[])
return;
}
const $t = get(t);
notificationController.show({
// for reasons beyond me <ComponentProps<typeof FormatBoldMessage>> doesn't work, even though it's (afaik) exactly this object
notificationController.show<{ key: Translations; values: InterpolationValues }>({
type: NotificationType.Info,
timeout: 5000,
component: {
@@ -549,7 +551,7 @@ export const delay = async (ms: number) => {
};
export const canCopyImageToClipboard = (): boolean => {
return !!(navigator.clipboard && window.ClipboardItem);
return !!(navigator.clipboard && globalThis.ClipboardItem);
};
const imgToBlob = async (imageElement: HTMLImageElement) => {

View File

@@ -95,7 +95,7 @@ export const handleLogout = async (redirectUri: string) => {
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {
window.location.href = redirectUri;
globalThis.location.href = redirectUri;
}
} finally {
resetSavedUser();

View File

@@ -16,5 +16,5 @@ function fake_cancelIdleCallback(id: number) {
return clearTimeout(id);
}
export const idleCB = window.requestIdleCallback || fake_requestIdleCallback;
export const cancelIdleCB = window.cancelIdleCallback || fake_cancelIdleCallback;
export const idleCB = globalThis.requestIdleCallback || fake_requestIdleCallback;
export const cancelIdleCB = globalThis.cancelIdleCallback || fake_cancelIdleCallback;

View File

@@ -11,7 +11,7 @@ export class KeyedPriorityQueue<K, T> {
const removed = this.set.delete(key);
if (removed) {
const idx = this.items.findIndex((i) => i.key === key);
if (idx >= 0) {
if (idx !== -1) {
this.items.splice(idx, 1);
}
}

View File

@@ -24,6 +24,6 @@ export const getActivationKey = async (licenseKey: string): Promise<string> => {
export const getLicenseLink = (license: ImmichProduct) => {
const url = new URL('/', PUBLIC_IMMICH_BUY_HOST);
url.searchParams.append('productId', license);
url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin);
url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || globalThis.origin);
return url.href;
};

View File

@@ -9,7 +9,7 @@ export type AssetGridRouteSearchParams = {
at: string | null | undefined;
};
export const isExternalUrl = (url: string): boolean => {
return new URL(url, window.location.href).origin !== window.location.origin;
return new URL(url, globalThis.location.href).origin !== globalThis.location.origin;
};
export const isPhotosRoute = (route?: string | null) => !!route?.startsWith('/(user)/photos/[[assetId=id]]');

View File

@@ -42,7 +42,7 @@
};
const getLink = (path: string) => {
const url = new URL(AppRoute.FOLDERS, window.location.href);
const url = new URL(AppRoute.FOLDERS, globalThis.location.href);
if (path) {
url.searchParams.set(QueryParameter.PATH, path);
}

View File

@@ -31,7 +31,10 @@
<main class="grid h-screen bg-immich-bg pt-18 dark:bg-immich-dark-bg">
{#if assetInteraction.selectionActive}
<AssetSelectControlBar assets={assetInteraction.selectedAssets} clearSelect={assetInteraction.clearMultiselect}>
<AssetSelectControlBar
assets={assetInteraction.selectedAssets}
clearSelect={() => assetInteraction.clearMultiselect()}
>
<CreateSharedLink />
<ButtonContextMenu icon={mdiPlus} title={$t('add')}>
<AddToAlbum />

View File

@@ -74,8 +74,13 @@
const assetStore = new AssetStore(assetStoreOptions);
$effect(() => {
// Check to trigger rebuild the timeline when navigating between people from the info panel
const change = assetStoreOptions.personId !== data.person.id;
assetStoreOptions.personId = data.person.id;
handlePromiseError(assetStore.updateOptions(assetStoreOptions));
if (change) {
assetStore.triggerUpdate();
}
});
const assetInteraction = new AssetInteraction();
@@ -454,6 +459,7 @@
{#key person.id}
<AssetGrid
enableRouting={true}
{person}
{assetStore}
{assetInteraction}
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { page } from '$app/stores';
import { page } from '$app/state';
</script>
<svelte:head>
@@ -8,7 +8,7 @@
<section class="flex flex-col px-4 h-screen w-screen place-content-center place-items-center">
<h1 class="py-10 text-4xl text-immich-primary dark:text-immich-dark-primary">Page not found :/</h1>
{#if $page.error?.message}
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{$page.error.message}</h2>
{#if page.error?.message}
<h2 class="text-xl text-immich-fg dark:text-immich-dark-fg">{page.error.message}</h2>
{/if}
</section>

View File

@@ -63,7 +63,7 @@
};
const getLink = (path: string) => {
const url = new URL(AppRoute.TAGS, window.location.href);
const url = new URL(AppRoute.TAGS, globalThis.location.href);
if (path) {
url.searchParams.set(QueryParameter.PATH, path);
}

View File

@@ -0,0 +1,14 @@
<script lang="ts">
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
</script>
<UserPageLayout title={data.meta.title}>
<div class="w-full h-full bg-gray-50 dark:bg-immich-dark-gray rounded-xl p-6">hello</div>
</UserPageLayout>

View File

@@ -0,0 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import type { PageLoad } from './$types';
export const load = (async () => {
await authenticate();
const $t = await getFormatter();
return {
meta: {
title: $t('workflows'),
},
};
}) satisfies PageLoad;

View File

@@ -1,6 +1,6 @@
<script>
import { page } from '$app/stores';
import { page } from '$app/state';
import Error from '$lib/components/error.svelte';
</script>
<Error error={$page.error}></Error>
<Error error={page.error}></Error>

View File

@@ -2,7 +2,7 @@
import { run } from 'svelte/legacy';
import { afterNavigate, beforeNavigate } from '$app/navigation';
import { page } from '$app/stores';
import { page } from '$app/state';
import DownloadPanel from '$lib/components/asset-viewer/download-panel.svelte';
import AppleHeader from '$lib/components/shared-components/apple-header.svelte';
import FullscreenContainer from '$lib/components/shared-components/fullscreen-container.svelte';
@@ -35,7 +35,7 @@
const changeTheme = (theme: ThemeSetting) => {
if (theme.system) {
theme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
theme.value = globalThis.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT;
}
if (theme.value === Theme.LIGHT) {
@@ -52,22 +52,22 @@
};
const getMyImmichLink = () => {
return new URL($page.url.pathname + $page.url.search, 'https://my.immich.app');
return new URL(page.url.pathname + page.url.search, 'https://my.immich.app');
};
onMount(() => {
const element = document.querySelector('#stencil');
element?.remove();
// if the browser theme changes, changes the Immich theme too
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
globalThis.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme);
});
onDestroy(() => {
document.removeEventListener('change', handleChangeTheme);
});
if (isSharedLinkRoute($page.route?.id)) {
setKey($page.params.key);
if (isSharedLinkRoute(page.route?.id)) {
setKey(page.params.key);
}
beforeNavigate(({ from, to }) => {
@@ -95,33 +95,33 @@
</script>
<svelte:head>
<title>{$page.data.meta?.title || 'Web'} - Immich</title>
<title>{page.data.meta?.title || 'Web'} - Immich</title>
<link rel="manifest" href="/manifest.json" crossorigin="use-credentials" />
<meta name="theme-color" content="currentColor" />
<AppleHeader />
{#if $page.data.meta}
<meta name="description" content={$page.data.meta.description} />
{#if page.data.meta}
<meta name="description" content={page.data.meta.description} />
<!-- Facebook Meta Tags -->
<meta property="og:type" content="website" />
<meta property="og:title" content={$page.data.meta.title} />
<meta property="og:description" content={$page.data.meta.description} />
{#if $page.data.meta.imageUrl}
<meta property="og:title" content={page.data.meta.title} />
<meta property="og:description" content={page.data.meta.description} />
{#if page.data.meta.imageUrl}
<meta
property="og:image"
content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || window.location.origin).href}
content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
/>
{/if}
<!-- Twitter Meta Tags -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content={$page.data.meta.title} />
<meta name="twitter:description" content={$page.data.meta.description} />
{#if $page.data.meta.imageUrl}
<meta name="twitter:title" content={page.data.meta.title} />
<meta name="twitter:description" content={page.data.meta.description} />
{#if page.data.meta.imageUrl}
<meta
name="twitter:image"
content={new URL($page.data.meta.imageUrl, $serverConfig.externalDomain || window.location.origin).href}
content={new URL(page.data.meta.imageUrl, $serverConfig.externalDomain || globalThis.location.origin).href}
/>
{/if}
{/if}
@@ -142,8 +142,8 @@
}}
/>
{#if $page.data.error}
<Error error={$page.data.error}></Error>
{#if page.data.error}
<Error error={page.data.error}></Error>
{:else}
{@render children?.()}
{/if}

View File

@@ -32,7 +32,7 @@
run(() => {
const stepState = $page.url.searchParams.get('step');
const temporaryIndex = onboardingSteps.findIndex((step) => step.name === stepState);
index = temporaryIndex >= 0 ? temporaryIndex : 0;
index = temporaryIndex === -1 ? 0 : temporaryIndex;
});
const handleDoneClicked = async () => {