Compare commits

...

30 Commits

Author SHA1 Message Date
Alex The Bot
b9fc59ca9f Version v1.109.2 2024-07-18 19:33:29 +00:00
Alex
e005a123ba fix(web): user can remove server license (#11199) 2024-07-18 14:26:54 -05:00
renovate[bot]
cd63212118 chore(deps): update base-image to v20240718 (major) (#11194)
chore(deps): update base-image to v20240718

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 13:58:17 -05:00
Michel Heusschen
a9dd013daf fix(web): hide license popup after mouse leave (#11193) 2024-07-18 13:13:45 -05:00
Alex The Bot
01ba859567 Version v1.109.1 2024-07-18 17:55:58 +00:00
Mert
173c9070c8 fix(ml): re-add worker env (#11192)
re-add worker env
2024-07-18 17:50:52 +00:00
Saschl
d37e8ede3b feat: optionally generate thumbnails for invalid images (#11126) 2024-07-18 12:07:22 -04:00
Alex The Bot
c77702279c Version v1.109.0 2024-07-18 16:03:42 +00:00
Alex
ef0e1a81b9 feat(web): license UI (#11182) 2024-07-18 10:56:27 -05:00
Mert
88f62087fd chore(ml): set higher worker timeout for openvino (#11174) 2024-07-18 10:50:57 -04:00
pokjay
4f89195702 feat(server): country geocoding for remote locations (#10950)
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-07-18 13:27:07 +02:00
renovate[bot]
ee22bbc85c chore(deps): update base-image to v20240717 (major) (#11172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 20:41:18 +00:00
Jason Rasmussen
66fae76af2 fix(server): delete large album (#11042)
fix: large album asset operations
2024-07-17 07:43:35 -04:00
renovate[bot]
f0d1dbccf4 chore(deps): update base-image to v20240716 (major) (#11144)
chore(deps): update base-image to v20240716

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 07:37:48 -04:00
waclaw66
a78365faab fix(web): more translations (#11167)
* item(s)

* search_by_filename

* filename example

* memory lane
2024-07-17 11:37:39 +00:00
Michel Heusschen
e3fd766e9b fix(web): byte units enum (#11161) 2024-07-17 07:25:06 -04:00
Weblate (bot)
c9c56ac600 chore(web): update translations (#11038)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Andreas Gammelgaard Damsbo <andreas@gdamsbo.dk>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Carlo Zanocco <zanocco.carlo@gmail.com>
Co-authored-by: Christer Solstrand Johannessen <weblate@csj.no>
Co-authored-by: Damian Krysta <damian@krysta.dev>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Håkon Velsvik <temanor@proton.me>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: João Gonçalves <jpcg89@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Leo Bottaro <weblate@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Matteo D <alex3025game@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: Vincenzo Nunziata <vinciosdev@gmail.com>
Co-authored-by: Vykintas Vyšniauskas <vykintasv@gmail.com>
Co-authored-by: Wojtek Sobczak <mister.adalbert@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: nazo6 <git@nazo6.dev>
Co-authored-by: polar <polar8143@users.noreply.hosted.weblate.org>
Co-authored-by: tomtom <beub3u@gmail.com>
Co-authored-by: vytautas <immichtranslation.a03gn@simplelogin.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Àlex Bravo <alexbravobosch@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-07-16 18:11:00 -05:00
bo0tzz
f6da01cb96 chore: Update feature-request.yaml (#11150) 2024-07-16 17:26:39 -05:00
Zack Pollard
fb8d9d8c40 fix: downgrade exiftool-vendored to fix motion photo extraction (#11145)
* Revert "chore(server): update exiftool and migrate off deprecated method signatures (#10367)"

This reverts commit 1b67ea2d

* fix: downgrade exiftool-vendored to 26.0.0

* chore: change motionphoto filenames to be kebab-case

* test: add pixel 6 pro motionphoto e2e test case

* test: add pixel 8a motion photo

* chore: update test-assets submodule pointer
2024-07-16 19:55:51 +00:00
Zack Pollard
87e8c16a90 fix: #11131 storage migration not moving archived files after template change (#11139) 2024-07-16 10:58:04 +00:00
renovate[bot]
99fe7b809a chore(deps): update terraform cloudflare to v4.37.0 (#11132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-16 10:49:54 +01:00
renovate[bot]
04e6e879a2 chore(deps): update typescript-projects (#11129)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-15 23:53:41 -04:00
Matthew Momjian
dda9c0057b docs: install script note (#11122)
install script note
2024-07-15 18:54:16 -04:00
Mert
cc1235d4aa docs: facial recognition and general clean-up (#11106)
* add facial recognition docs, clean up existing info

* Update smart-search.md

Co-authored-by: Alex <alex.tran1502@gmail.com>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-14 21:08:16 -05:00
Mert
8193416230 feat(server): conditionally run facial recognition nightly (#11080)
* only run nightly if new person

* add tests

* use string instead of date

* update sql

* update tests

* simplify condition
2024-07-14 22:53:42 +00:00
Matthew Momjian
8863bd4e7d docs: cleanup external libraries (#11099)
* cleanup external libraries

* Update external-library.md

* Update external-library.md

* Update libraries.md

* Update docs/docs/features/libraries.md

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>

* Update external-library.md

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2024-07-14 17:29:47 -04:00
Raj Dave
d23aa5e8e2 fix(docs): minor grammar fix in external-library.md (#11073) 2024-07-14 02:09:28 +00:00
renovate[bot]
18b466ee52 chore(deps): update base-image to v20240713 (major) (#11066) 2024-07-13 20:58:21 -05:00
Alex
e852971a13 fix(mobile): fix database out of size (#11065) 2024-07-13 20:55:35 -05:00
renovate[bot]
fbe29bf4cd chore(deps): update dependency rimraf to v6 (#11079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 21:45:45 -04:00
134 changed files with 4715 additions and 2553 deletions

View File

@@ -29,3 +29,4 @@ web/node_modules/
web/coverage/
web/.svelte-kit
web/build/
web/.env

View File

@@ -1,11 +1,13 @@
title: "[Feature] <feature-name-goes-here>"
title: "[Feature] feature-name-goes-here"
labels: ["feature"]
body:
- type: markdown
attributes:
value: |
Please use this form to request new feature for Immich
Please use this form to request new feature for Immich.
Stick to only a single feature per request. If you list multiple different features at once,
your request will be closed.
- type: checkboxes
attributes:

97
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.8",
"version": "2.2.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.8",
"version": "2.2.11",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -49,7 +49,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -1124,10 +1124,11 @@
"dev": true
},
"node_modules/@types/cli-progress": {
"version": "3.11.5",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz",
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==",
"version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
"integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*"
}
@@ -1179,17 +1180,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1213,16 +1214,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1242,14 +1243,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1260,14 +1261,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1288,9 +1289,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1302,14 +1303,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1331,16 +1332,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1354,13 +1355,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.8",
"version": "2.2.11",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.36.0"
constraints = "4.36.0"
version = "4.37.0"
constraints = "4.37.0"
hashes = [
"h1:00/Y+l17VV4RquGSfwDnYsGYzyf2ZmdQwUgeIzXC7eg=",
"h1:489GpKItA/VRIUA5S4+F8MsnurGVciRvUFyIV81MJTU=",
"h1:7cnczyKGj3+gvaJ0r5JIVWLXPbQfkHYejac76MJx+I8=",
"h1:8rmr1PjJc14Xmor2eEvo5/WBojylt1eYdx6VbSU3Ulo=",
"h1:HjgphNjtgny5tkcUAQoGgBdcuQ+0IyhL8yLsiBqWAP0=",
"h1:LH3umxdBnJcAyeVoBLVn+PC0F0CzN6v9UN6lb6CqQPE=",
"h1:Xx6WUD/zB8fM9SjkFx06Fgx2K7aGJIVvsJS2pwqALEM=",
"h1:YizL5YN9zQ8YkSR6V/G201YrCVdnkF9EUIK4lpROWiA=",
"h1:aPcXVGjYcCJdqvWSzc/dEjwj05LnbWZje8IanygVjcI=",
"h1:eKCvfashdCqfDcFGXE2gq+XxAURD5SzuaQ9Brs3zLos=",
"h1:gpKcBYkBcfn/uF1A8W7MD/OysMZW7EU4QVYvPEEnxGc=",
"h1:kCkcxZZnkKAnMz9scUQHb19d9/l9FPOHovAyrvtA618=",
"h1:t8mXXnICTeKqoD29uvyLFHVWMfMzTUrJuHje8lpI0zU=",
"h1:zjzavjIdLDGRYsWd3v0HJz6ul12Cewj9RW/cqAQ4DxI=",
"zh:02665712b3893307596b3caab99cf1f2502d5caca18e22d4b37bb535e628e102",
"zh:1514b0d3ef62934484ac471113ee68cddec0c21e56b4f710922741fe9b6e6fdf",
"zh:1fab4dfcecbcea13267b42e5ff05ba0692aa2dcb247b8e633fea0daf49feb156",
"zh:24d8367295fe1f1b2be37802aecb96edf32f743364663ffe781d1bb92438395d",
"zh:34e84e7940c99dcf65663cfd25afac22bf5c8a5ff2cd21900c67180d3a072be9",
"zh:3d71d63204a329acf1d1de8638f2c725243cb94cf444d2d7acde54b3d1ac1696",
"zh:57831ba88e779a762bcfa224ba9eac8bc22ef9cd70cd541d848b351e0ba6a75c",
"zh:6407560f2e548afcb4852c91efc664627a9ee565c31a9c81fc9ea1806fca0567",
"zh:738ddbc664d75f4859aa09444a27809bc398795a8ea8f5be8531040690287712",
"zh:841ca2b2d78b6f8d33ec3435bc090c5e04a3a7d85c80df11227a7ea00d36f6b1",
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.36.0"
version = "4.37.0"
}
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.36.0"
constraints = "4.36.0"
version = "4.37.0"
constraints = "4.37.0"
hashes = [
"h1:00/Y+l17VV4RquGSfwDnYsGYzyf2ZmdQwUgeIzXC7eg=",
"h1:489GpKItA/VRIUA5S4+F8MsnurGVciRvUFyIV81MJTU=",
"h1:7cnczyKGj3+gvaJ0r5JIVWLXPbQfkHYejac76MJx+I8=",
"h1:8rmr1PjJc14Xmor2eEvo5/WBojylt1eYdx6VbSU3Ulo=",
"h1:HjgphNjtgny5tkcUAQoGgBdcuQ+0IyhL8yLsiBqWAP0=",
"h1:LH3umxdBnJcAyeVoBLVn+PC0F0CzN6v9UN6lb6CqQPE=",
"h1:Xx6WUD/zB8fM9SjkFx06Fgx2K7aGJIVvsJS2pwqALEM=",
"h1:YizL5YN9zQ8YkSR6V/G201YrCVdnkF9EUIK4lpROWiA=",
"h1:aPcXVGjYcCJdqvWSzc/dEjwj05LnbWZje8IanygVjcI=",
"h1:eKCvfashdCqfDcFGXE2gq+XxAURD5SzuaQ9Brs3zLos=",
"h1:gpKcBYkBcfn/uF1A8W7MD/OysMZW7EU4QVYvPEEnxGc=",
"h1:kCkcxZZnkKAnMz9scUQHb19d9/l9FPOHovAyrvtA618=",
"h1:t8mXXnICTeKqoD29uvyLFHVWMfMzTUrJuHje8lpI0zU=",
"h1:zjzavjIdLDGRYsWd3v0HJz6ul12Cewj9RW/cqAQ4DxI=",
"zh:02665712b3893307596b3caab99cf1f2502d5caca18e22d4b37bb535e628e102",
"zh:1514b0d3ef62934484ac471113ee68cddec0c21e56b4f710922741fe9b6e6fdf",
"zh:1fab4dfcecbcea13267b42e5ff05ba0692aa2dcb247b8e633fea0daf49feb156",
"zh:24d8367295fe1f1b2be37802aecb96edf32f743364663ffe781d1bb92438395d",
"zh:34e84e7940c99dcf65663cfd25afac22bf5c8a5ff2cd21900c67180d3a072be9",
"zh:3d71d63204a329acf1d1de8638f2c725243cb94cf444d2d7acde54b3d1ac1696",
"zh:57831ba88e779a762bcfa224ba9eac8bc22ef9cd70cd541d848b351e0ba6a75c",
"zh:6407560f2e548afcb4852c91efc664627a9ee565c31a9c81fc9ea1806fca0567",
"zh:738ddbc664d75f4859aa09444a27809bc398795a8ea8f5be8531040690287712",
"zh:841ca2b2d78b6f8d33ec3435bc090c5e04a3a7d85c80df11227a7ea00d36f6b1",
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
]
}

View File

@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.36.0"
version = "4.37.0"
}
}
}

View File

@@ -1,7 +1,7 @@
---
title: The Immich core team goes full-time
authors: [alextran]
tags: [update, announcement, futo]
tags: [update, announcement, FUTO]
date: 2024-05-01T00:00
---

View File

@@ -0,0 +1,91 @@
---
title: Licensing announcement - Purchase a license to support Immich
authors: [alextran]
tags: [update, announcement, FUTO]
date: 2024-07-18T00:00
---
Hello everybody,
Firstly, on behalf of the Immich team, I'd like to thank everybody for your continuous support of Immich since the very first day! Your contributions, encouragement, and community engagement have helped bring Immich to its current state. The team and I are forever grateful for that.
Since our [last announcement of the core team joining FUTO to work on Immich full-time](https://immich.app/blog/2024/immich-core-team-goes-fulltime), one of the goals of our new position is to foster a healthy relationship between the developers and the users. We believe that this enables us to create great software, establish transparent policies and build trust.
We want to build a great software application that brings value to you and your loved ones' lives. We are not using you as a product, i.e., selling or tracking your data. We are not putting annoying ads into our software. We respect your privacy. We want to be compensated for the hard work we put in to build Immich for you.
With those notes, we have enabled a way for you to financially support the continued development of Immich, ensuring the software can move forward and will be maintained, by offering a lifetime license of the software. We think if you like and use software, you should pay for it, but _we're never going to force anyone to pay or try to limit Immich for those who don't._
There are two types of license that you can choose to purchase: **Server License** and **Individual License**.
### Server License
This is a lifetime license costing **$99.99**. The license is applied to the whole server. You and all users that use your server are licensed.
### Individual License
This is a lifetime license costing **$24.99**. The license is applied to a single user, and can be used on any server they choose to connect to.
<img
width="837"
alt="license-social-gh"
src="https://github.com/user-attachments/assets/241932ed-ef3b-44ec-a9e2-ee80754e0cca"
/>
You can purchase the license on [our page - https://buy.immich.app](https://buy.immich.app).
Starting with release `v1.109.0` you can purchase and enter your purchased license key directly in the app.
<img
width="1414"
alt="license-page-gh"
src="https://github.com/user-attachments/assets/364fc32a-f6ef-4594-9fea-28d5a26ad77c"
/>
## Thank you
Thank you again for your support, this will help create a strong foundation and stability for the Immich team to continue developing and maintaining the project that you love to use.
<p align="center">
<img
src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjY2eWc5Y2F0ZW56MmR4aWE0dDhzZXlidXRmYWZyajl1bWZidXZpcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/87CKDqErVfMqY/giphy.gif"
width="550"
title="SUPPORT THE PROJECT!"
/>
</p>
<br />
<br />
Cheers! 🎉
Immich team
# FAQ
### 1. Where can I purchase a license?
There are several places where you can purchase the license from
- [https://buy.immich.app](https://buy.immich.app)
- [https://pay.futo.org](https://pay.futo.org/)
- or directly from the app.
### 2. Do I need both _Individual License_ and _Server License_?
No,
If you are the admin and the sole user, or your instance has less than a total of 4 users, you can buy the **Individual License** for each user.
If your instance has more than 4 users, it is more cost-effective to buy the **Server License**, which will license all the users on your instance.
### 3. What do I do if I don't pay?
You can continue using Immich for an unlimited trial period.
### 4. Will there be any paywalled features?
No, there will never be any paywalled features.
### 5. Where can I get support regarding payment issues?
You can email us with your `orderId` and your email address `billing@futo.org` or on our Discord server.

View File

@@ -167,7 +167,7 @@ Immich uses CLIP models. For more information about CLIP and its capabilities, r
### How does facial recognition work?
For face detection and recognition, Immich uses [InsightFace models](https://github.com/deepinsight/insightface/tree/master/model_zoo).
See [How Facial Recognition Works](/docs/features/facial-recognition#How-Facial-Recognition-Works) for details.
### How can I disable machine learning?
@@ -181,19 +181,15 @@ However, disabling all jobs will not disable the machine learning service itself
### I'm getting errors about models being corrupt or failing to download. What do I do?
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface][huggingface] and place them in the cache folder.
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Hugging Face][huggingface] and place them in the cache folder.
### Can I use a custom CLIP model?
No, this is not supported. Only models listed in the [Huggingface][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
No, this is not supported. Only models listed in the [Hugging Face][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
### I want to be able to search in other languages besides English. How can I do that?
You can change to a multilingual model listed [here](https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7) by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model. Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages.
:::note
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list][huggingface].
:::
You can change to a multilingual CLIP model. See [here](/docs/features/smart-search#CLIP-model) for instructions.
### Does Immich support Facial Recognition for videos?
@@ -234,7 +230,7 @@ ls clip/ facial-recognition/
### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
### Can I lower CPU and RAM usage?
@@ -243,10 +239,12 @@ The initial backup is the most intensive due to the number of jobs running. The
- Lower the job concurrency for these jobs to 1.
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit the amount of CPU and RAM usage?
### Can I limit CPU and RAM usage?
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources.
@@ -266,6 +264,8 @@ deploy:
</details>
For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit).
Note that memory constraints work by terminating the container, so this can introduce instability if set too low.
### How can I boost machine learning speed?
:::note
@@ -275,21 +275,16 @@ This advice improves throughput, not latency. This is to say that it will make S
You can increase throughput by increasing the job concurrency for machine learning jobs (Smart Search, Face Detection). With higher concurrency, the host will work on more assets in parallel. You can do this by navigating to Administration > Settings > Job Settings and increasing concurrency as needed.
:::danger
On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond this, note that storage speed and latency may quickly become the limiting factor; particularly when using HDDs.
On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Storage speed and latency can quickly become the limiting factor beyond this, particularly when using HDDs.
Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server.
The concurrency can be increased more comfortably with a GPU, but should still not be above 16 in most cases.
More details can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708)
Do not exaggerate with the job concurrency because you're probably thoroughly overloading the server.
:::
### Why is Immich using so much of my CPU?
### My server shows Server Status Offline | Version Unknown. What can I do?
When a large number of assets are uploaded to Immich, it makes sense that the CPU and RAM will be heavily used for machine learning work and creating image thumbnails.
Once this process is completed, the percentage of CPU usage will drop to around 3-5% usage
### My server shows Server Status Offline | Version Unknown what can I do?
You need to enable Websocket on your reverse proxy.
You need to enable WebSockets on your reverse proxy.
---

View File

@@ -2,7 +2,7 @@
## Overview
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them.
Immich recognizes faces in your photos and videos and groups them together into people. You can then assign names to these people and search for them.
The list of people is shown in the Explore page.
@@ -18,13 +18,75 @@ The asset detail view will also show the faces that are recognized in the asset.
## Actions
Additional actions you can do with a detected person are:
Additional actions you can do include:
- Change the feature face photo of the person
- Set date of birth
- Merge two or more detected faces into one person
- Hide face
- Changing the feature photo of the person
- Setting a person's date of birth
- Merging two or more detected faces into one person
- Hiding the faces of a person from the Explore page and detail view
- Assigning an unrecognized face to a person
It can be found from the app bar when you access the detail view of a person.
<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>
## How Face Detection Works
Face detection sends the generated preview image to the machine learning service for processing. The service checks if it has the relevant model downloaded and downloads it if not. The image is decoded, pre-processed and passed to the face detection model (with hardware acceleration if configured). The bounding boxes and scores outputted from this model are used to crop and preprocess the image once again to be passed to a facial recognition model (also accelerated if configured). The embeddings from the recognition model, together with the bounding boxes and scores from the face detection model, are then sent back to the server to be added to the database. The embeddings in particular are indexed so they can be searched quickly during facial recognition clustering.
## How Facial Recognition Works
The facial recognition algorithm we use is derived from DBSCAN, a popular clustering algorithm. It essentially treats each detected face as a point in a graph and aims to group points that are close to each other.
:::note
An important concept is whether something is a _core point_. A core point has a minimum number of points around it within a certain distance. A non-core point can only be assigned to a cluster if it can reach a core point; a non-core point can't be used to extend a cluster even if it's part of one. In Immich, the _Minimum Recognized Faces_ setting controls the threshold to be considered a core point.
:::
For each face, it looks around it to find other faces within a certain distance. Faces within this distance are considered similar, so it then checks if any of these faces are associated with a person.
If there is an existing person, it assigns the person of the most similar face to the face being processed.
If there is none, then it has to determine something from the DBSCAN algorithm: whether the face is a _core point_. If there are a certain number of similar faces (by default 3, including the face being considered), then this face is a core point. A new person is created for this face and the face is assigned to it. When other faces are processed, if they're similar to this face, they'll see that it has an associated person and can be assigned to that person.
However, if there aren't enough similar faces, no new person will be created. Instead, the face will wait for all the other faces to be processed to see if any matches that previously didn't have an associated person now do. If they do, then the face will be assigned to that person. If not, this face will be considered an outlier, such as a stranger in the background of an image.
The algorithm has some subtle differences compared to DBSCAN:
- DBSCAN doesn't have a concept of incremental clustering: it clusters all points at once. In contrast, facial recognition has to evolve as more assets are added without re-clustering everything each time.
- The algorithm described above works within a set of queued assets. Once these faces are processed and a new round of faces are detected, the behavior will not be the same as traditional DBSCAN since it preserves the clusters (people) generated from the previous round.
- Facial recognition tries to wait for face detection and thumbnail generation to complete before starting for this reason: the larger the set of faces in the queue, the better the results will be.
- Re-running facial recognition on all assets afterwards does behave like DBSCAN, however.
- DBSCAN is designed for range-based searches (i.e. points within a distance), but high-dimensional vector indices are generally optimized for getting the closest K results. The recognition algorithm doesn't try to get _all_ similar faces within a distance for performance reasons. Instead, it searches for a small number of matches for each face. The end result should be very similar if not identical, but with possibly different performance characteristics.
- Because of this, part of the recognition process is handled during a nightly job to ensure that unassigned faces with potential matches can be recognized.
:::tip
If you didn't import your assets at once or if the server was able to process jobs faster than you could upload them, it's possible that the clustering was suboptimal. If you haven't put effort into the current results, it may be worth re-running facial recognition on all assets for the best starting point. If it's too late for that, you can also manually assign a selection of unassigned faces and queue _Missing_ for Facial Recognition to help it learn and assign more faces automatically.
:::
## Configuration
Navigating to Administration > Settings > Machine Learning Settings > Facial Recognition will show the options available.
:::tip
It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa.
:::
### Facial recognition model
There are a few different models available; the default is typically considered the best. On more constrained systems where the default is too intensive, you can choose a smaller model instead.
### Minimum detection score
This setting affects whether a result from the face detecton model is filtered out as a false positive. It may seem tempting to set this low to detect more faces, but it can lead to false positives that are difficult to deal with and can harm facial recognition. It is strongly recommended not to go below 0.5 for this setting. Setting it to a very high number like 0.9 is also not recommended: the default is already biased toward precision, so a threshold that high leads to many undetected faces.
After changing this setting, it will only apply to new face detection jobs. To apply the new setting to all assets, you need to re-run face detection for all assets.
### Maximum recognition distance
The distance threshold described in How Facial Recognition Works. The default works well for most people, but it may be worth lowering it if the library has twins or otherwise very similar looking people. A threshold that's too low just means needing to merge duplicate people after facial recognition, whereas a threshold too high can produce unsalvageable results. It is strongly recommended not to go below 0.3 or above 0.7.
### Minimum recognized faces
The core point threshold described in How Facial Recognition Works. This setting has a few implications. First, it takes effect immediately in that people with fewer faces than this are hidden from view. Secondly, it makes clustering more robust as it prevents loosely-related faces from being linked to each other by requiring a certain level of density.
Increasing this setting is a good idea if you increase the recognition distance or reduce the minimum detection score. Setting it to 1 effectively disables the concept of core points, but can be an option if you prefer a more hands-on approach.

View File

@@ -123,6 +123,7 @@ Once this is done, you can continue to step 3 of "Basic Setup".
- You may want to choose a slower preset than for software transcoding to maintain quality and efficiency
- While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices
- You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for NVIDIA, `intel_gpu_top` for Intel, etc.) when transcoding. A lack of error logs when transcoding also indicates that it's being used.
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

View File

@@ -104,18 +104,19 @@ The `immich-server` container will need access to the gallery. Modify your docke
immich-server:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/nas/christmas-trip:/mnt/nas/christmas-trip:ro
+ - /home/user/old-pics:/home/user/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - /mnt/media/videos2:/mnt/media/videos2 # the files in this folder can be deleted, as it does not end with :ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
```
:::tip
The `ro` flag at the end only gives read-only access to the volumes. While Immich does not modify files, it's a good practice to mount read-only.
The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI.
:::
:::info
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._
_Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._
:::
### Create External Libraries

View File

@@ -7,29 +7,30 @@ Immich uses Postgres as its search database for both metadata and smart search.
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
Archived photos are not included in search results by default. To include them, mark the checkbox in [advanced search filters](/docs/features/smart-search#advanced-search-filters).
:::tip Alternative CLIP Models
More powerful models can be used for more accurate search results. For more information, see the related [FAQ](/docs/FAQ#can-i-use-a-custom-clip-model).
:::
:::info
Smart Search is currently limited to 5,000 results for a single search on the web.
:::
## Advanced Search Filters
In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app).
Smart search features include:
The filters smart search allows you to search by include:
- Search for one or more faces (with or without context search).
- Search by Country or State or City or by all three.
- Search by camera make and model.
- Search by date range.
- Search by file name.
- Search by media types: image, video or all (**Note:** Image includes live images).
- Search by condition: not in any album or archive or Favorite or all conditions.
- People
- Location
- Country
- State
- City
- Camera
- Make
- Model
- Date range
- File name or extension
- Media type
- Image (including live/motion photos)
- Video
- All
- Condition
- Not in any album
- Archived
- Favorited
<Tabs>
<TabItem value="Computer" label="Computer" default>
@@ -47,3 +48,27 @@ Some search examples:
</TabItem>
</Tabs>
## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
### CLIP model
More powerful models can be used for more accurate search results, but are slower and can require more server resources. Check out the models [here][huggingface-clip] for more options!
[Multilingual models][huggingface-multilingual-clip] are also available so users can search in their native language. These models support over 100 languages; the `nllb` models in particular support 200.
:::note
Multilingual models are much slower and larger and perform slightly worse for English than English-only models. For this reason, only use them if you actually intend to search in a language besides English.
As a special case, the `ViT-H-14-quickgelu__dfn5b` and `ViT-H-14-378-quickgelu__dfn5b` models are excellent at many European languages despite not specifically being multilingual. They're very intensive regardless, however - especially the latter.
:::
Once you've chosen a model, change this setting to the name of the model you chose. Be sure to re-run Smart Search on all assets after this change.
:::note
Feel free to make a feature request if there's a model you want to use that we don't currently support.
:::
[huggingface-clip]: https://huggingface.co/collections/immich-app/clip-654eaefb077425890874cd07
[huggingface-multilingual-clip]: https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7

View File

@@ -6,36 +6,18 @@ in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points in the section `immich-server:` under `volumes:`
Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point.
```diff
immich-server:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /home/user/photos1:/home/user/photos1:ro
+ - /mnt/photos2:/mnt/photos2:ro # you can delete this line if you only have one mount point, or you can add more lines if you have more than two
```
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
```
EXTERNAL_PATH=<your-path-here>
```
On my computer, for example, I use this path:
```
EXTERNAL_PATH=/home/tenino/photos
```
:::info EXTERNAL_PATH design
The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously.
:::
Restart Immich.
```
docker compose down
docker compose up -d
```
Restart Immich by running `docker compose up -d`.
# Create the library

View File

@@ -4,14 +4,11 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system.
- If using [hardware acceleration](/docs/features/ml-hardware-acceleration), the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added
- Start the container by running `docker compose up -d`.
:::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning container, but facial recognition is done in the `microservices` worker.
:::
:::note
The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be in the same folder if trying to use [hardware acceleration](/docs/features/ml-hardware-acceleration).
Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
:::
```yaml
@@ -37,3 +34,7 @@ volumes:
```
Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together.
:::caution
As an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
:::

View File

@@ -38,17 +38,18 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :-------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
@@ -155,18 +156,18 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Containers |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
| Variable | Description | Default | Containers |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.

View File

@@ -8,6 +8,10 @@ sidebar_position: 20
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx).
:::
:::note
The install script only supports Linux operating systems and requires Docker to be already installed on the system.
:::
In the shell, from a directory of your choice, run the following command:
```bash

View File

@@ -1,4 +1,16 @@
[
{
"label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app"
},
{
"label": "v1.109.1",
"url": "https://v1.109.1.archive.immich.app"
},
{
"label": "v1.109.0",
"url": "https://v1.109.0.archive.immich.app"
},
{
"label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app"

92
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.108.0",
"version": "1.109.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
@@ -42,7 +42,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.8",
"version": "2.2.11",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -86,7 +86,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -1570,17 +1570,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1604,16 +1604,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1633,14 +1633,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1651,14 +1651,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1679,9 +1679,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1693,14 +1693,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1748,16 +1748,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1771,13 +1771,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.108.0",
"version": "1.109.2",
"description": "",
"main": "index.js",
"type": "module",

View File

@@ -507,6 +507,22 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it.skip('should geocode country from gps data in the middle of nowhere', async () => {
const { status } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ latitude: 42, longitude: 69 });
expect(status).toEqual(200);
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const asset = await getAssetInfo({ id: user1Assets[0].id }, { headers: asBearerAuth(user1.accessToken) });
expect(asset).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({ city: null, country: 'Kazakhstan' }),
});
});
it('should set the description', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
@@ -1170,17 +1186,25 @@ describe('/asset', () => {
// into the test here.
it.each([
{
filepath: 'formats/motionphoto/Samsung One UI 5.jpg',
filepath: 'formats/motionphoto/samsung-one-ui-5.jpg',
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
},
{
filepath: 'formats/motionphoto/Samsung One UI 6.jpg',
filepath: 'formats/motionphoto/samsung-one-ui-6.jpg',
checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
},
{
filepath: 'formats/motionphoto/Samsung One UI 6.heic',
filepath: 'formats/motionphoto/samsung-one-ui-6.heic',
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
},
{
filepath: 'formats/motionphoto/pixel-6-pro.jpg',
checksum: 'bFhLGbdK058PSk4FTfrSnoKWykc=',
},
{
filepath: 'formats/motionphoto/pixel-8a.jpg',
checksum: '7YdY+WF0h+CXHbiXpi0HiCMTTjs=',
},
])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
const response = await utils.createAsset(admin.accessToken, {
assetData: {

View File

@@ -49,9 +49,9 @@ describe('/search', () => {
{ filename: '/albums/nature/silver_fir.jpg' },
{ filename: '/formats/heic/IMG_2682.heic' },
{ filename: '/formats/jpg/el_torcal_rocks.jpg' },
{ filename: '/formats/motionphoto/Samsung One UI 6.jpg' },
{ filename: '/formats/motionphoto/Samsung One UI 6.heic' },
{ filename: '/formats/motionphoto/Samsung One UI 5.jpg' },
{ filename: '/formats/motionphoto/samsung-one-ui-6.jpg' },
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
@@ -315,7 +315,7 @@ describe('/search', () => {
{
should: 'should search by originalFilename with spaces',
deferred: () => ({
dto: { originalFileName: 'Samsung One', type: 'IMAGE' },
dto: { originalFileName: 'samsung-one', type: 'IMAGE' },
assets: [assetOneJpg5, assetOneJpg6, assetOneHeic6],
}),
},

View File

@@ -77,6 +77,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app
ARG DEVICE
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.108.0"
version = "1.109.2"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"

View File

@@ -5,12 +5,14 @@ lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
if ! [ "$DEVICE" = "openvino" ]; then
export LD_PRELOAD="$lib_path"
export LD_BIND_NOW=1
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
else
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=300}"
fi
: "${IMMICH_HOST:=[::]}"
: "${IMMICH_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
gunicorn app.main:app \
-k app.config.CustomUvicornWorker \

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 148,
"android.injected.version.name" => "1.108.0",
"android.injected.version.code" => 150,
"android.injected.version.name" => "1.109.2",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
version_number: "1.108.0"
version_number: "1.109.2"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -104,7 +104,7 @@ Future<Isar> loadDb() async {
if (Platform.isIOS) IOSDeviceAssetSchema,
],
directory: dir.path,
maxSizeMiB: 256,
maxSizeMiB: 1024,
);
Store.init(db);
return db;

View File

@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.108.0
- API version: 1.109.2
- Generator version: 7.5.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.108.0+148
version: 1.109.2+150
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -50,7 +50,7 @@ final class TestUtils {
AndroidDeviceAssetSchema,
IOSDeviceAssetSchema,
],
maxSizeMiB: 256,
maxSizeMiB: 1024,
directory: "test/",
);

View File

@@ -7007,7 +7007,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.108.0",
"version": "1.109.2",
"contact": {}
},
"tags": [],

View File

@@ -1,12 +1,12 @@
{
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module",
"main": "./build/index.js",

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.108.0
* 1.109.2
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240710@sha256:5944dac5d73dc54d733461db555786c823460e730f6793ea5ad4ee3843a75c7d as dev
FROM ghcr.io/immich-app/base-server-dev:20240718@sha256:55e009a70528cbea353c42e899ca5fbc3194cf75dd6fea39a4c01f3f40ccc6bd as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240710@sha256:bc3cda314634467d5a92b78c09ba566246e00ee8495aac398f5c1000c5e26fa9
FROM ghcr.io/immich-app/base-server-prod:20240718@sha256:f386227411c908ebfa983f04d1981e7df07d1314fcdf271ab987e7236379b606
WORKDIR /usr/src/app
ENV NODE_ENV=production \

591
server/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.108.0",
"version": "1.109.2",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",
@@ -34,7 +34,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "~27.0.0",
"exiftool-vendored": "26.0.0",
"fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0",
@@ -96,7 +96,7 @@
"mock-fs": "^5.2.0",
"prettier": "^3.0.2",
"prettier-plugin-organize-imports": "^4.0.0",
"rimraf": "^5.0.1",
"rimraf": "^6.0.0",
"source-map-support": "^0.5.21",
"sql-formatter": "^15.0.0",
"tsconfig-paths": "^4.2.0",
@@ -5685,9 +5685,9 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"node_modules/@swc/core": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.7.tgz",
"integrity": "sha512-BBzORL9qWz5hZqAZ83yn+WNaD54RH5eludjqIOboolFOK/Pw+2l00/H77H4CEBJnzCIBQszsyqtITmrn4evp0g==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz",
"integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==",
"devOptional": true,
"hasInstallScript": true,
"dependencies": {
@@ -5702,16 +5702,16 @@
"url": "https://opencollective.com/swc"
},
"optionalDependencies": {
"@swc/core-darwin-arm64": "1.6.7",
"@swc/core-darwin-x64": "1.6.7",
"@swc/core-linux-arm-gnueabihf": "1.6.7",
"@swc/core-linux-arm64-gnu": "1.6.7",
"@swc/core-linux-arm64-musl": "1.6.7",
"@swc/core-linux-x64-gnu": "1.6.7",
"@swc/core-linux-x64-musl": "1.6.7",
"@swc/core-win32-arm64-msvc": "1.6.7",
"@swc/core-win32-ia32-msvc": "1.6.7",
"@swc/core-win32-x64-msvc": "1.6.7"
"@swc/core-darwin-arm64": "1.6.13",
"@swc/core-darwin-x64": "1.6.13",
"@swc/core-linux-arm-gnueabihf": "1.6.13",
"@swc/core-linux-arm64-gnu": "1.6.13",
"@swc/core-linux-arm64-musl": "1.6.13",
"@swc/core-linux-x64-gnu": "1.6.13",
"@swc/core-linux-x64-musl": "1.6.13",
"@swc/core-win32-arm64-msvc": "1.6.13",
"@swc/core-win32-ia32-msvc": "1.6.13",
"@swc/core-win32-x64-msvc": "1.6.13"
},
"peerDependencies": {
"@swc/helpers": "*"
@@ -5723,9 +5723,9 @@
}
},
"node_modules/@swc/core-darwin-arm64": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.7.tgz",
"integrity": "sha512-sNb+ghP2OhZyUjS7E5Mf3PqSvoXJ5gY6GBaH2qp8WQxx9VL7ozC4HVo6vkeFJBN5cmYqUCLnhrM3HU4W+7yMSA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz",
"integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==",
"cpu": [
"arm64"
],
@@ -5739,9 +5739,9 @@
}
},
"node_modules/@swc/core-darwin-x64": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.7.tgz",
"integrity": "sha512-LQwYm/ATYN5fYSYVPMfComPiFo5i8jh75h1ASvNWhXtS+/+k1dq1zXTJWZRuojd5NXgW3bb6mJtJ2evwYIgYbA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz",
"integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==",
"cpu": [
"x64"
],
@@ -5755,9 +5755,9 @@
}
},
"node_modules/@swc/core-linux-arm-gnueabihf": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.7.tgz",
"integrity": "sha512-kEDzVhNci38LX3kdY99t68P2CDf+2QFDk5LawVamXH0iN5DRAO/+wjOhxL8KOHa6wQVqKEt5WrhD+Rrvk/34Yw==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz",
"integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==",
"cpu": [
"arm"
],
@@ -5771,9 +5771,9 @@
}
},
"node_modules/@swc/core-linux-arm64-gnu": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.7.tgz",
"integrity": "sha512-SyOBUGfl31xLGpIJ/Jd6GKHtkfZyHBXSwFlK7FmPN//MBQLtTBm4ZaWTnWnGo4aRsJwQdXWDKPyqlMBtnIl1nQ==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz",
"integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==",
"cpu": [
"arm64"
],
@@ -5787,9 +5787,9 @@
}
},
"node_modules/@swc/core-linux-arm64-musl": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.7.tgz",
"integrity": "sha512-1fOAXkDFbRfItEdMZPxT3du1QWYhgToa4YsnqTujjE8EqJW8K27hIcHRIkVuzp7PNhq8nLBg0JpJM4g27EWD7g==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz",
"integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==",
"cpu": [
"arm64"
],
@@ -5803,9 +5803,9 @@
}
},
"node_modules/@swc/core-linux-x64-gnu": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.7.tgz",
"integrity": "sha512-Gp7uCwPsNO5ATxbyvfTyeNCHUGD9oA+xKMm43G1tWCy+l07gLqWMKp7DIr3L3qPD05TfAVo3OuiOn2abpzOFbw==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz",
"integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==",
"cpu": [
"x64"
],
@@ -5819,9 +5819,9 @@
}
},
"node_modules/@swc/core-linux-x64-musl": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.7.tgz",
"integrity": "sha512-QeruGBZJ15tadqEMQ77ixT/CYGk20MtlS8wmvJiV+Wsb8gPW5LgCjtupzcLLnoQzDG54JGNCeeZ0l/T8NYsOvA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz",
"integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==",
"cpu": [
"x64"
],
@@ -5835,9 +5835,9 @@
}
},
"node_modules/@swc/core-win32-arm64-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.7.tgz",
"integrity": "sha512-ouRqgSnT95lTCiU/6kJRNS5b1o+p8I/V9jxtL21WUj/JOVhsFmBErqQ0MZyCu514noWiR5BIqOrZXR8C1Knx6Q==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz",
"integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==",
"cpu": [
"arm64"
],
@@ -5851,9 +5851,9 @@
}
},
"node_modules/@swc/core-win32-ia32-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.7.tgz",
"integrity": "sha512-eZAP/EmJ0IcfgAx6B4/SpSjq3aT8gr0ooktfMqw/w0/5lnNrbMl2v+2kvxcneNcF7bp8VNcYZnoHlsP+LvmVbA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz",
"integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==",
"cpu": [
"ia32"
],
@@ -5867,9 +5867,9 @@
}
},
"node_modules/@swc/core-win32-x64-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.7.tgz",
"integrity": "sha512-QOdE+7GQg1UQPS6p0KxzJOh/8GLbJ5zI1vqKArCCB0unFqUfKIjYb2TaH0geEBy3w9qtXxe3ZW6hzxtZSS9lDg==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz",
"integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==",
"cpu": [
"x64"
],
@@ -5904,12 +5904,12 @@
}
},
"node_modules/@testcontainers/postgresql": {
"version": "10.10.1",
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.1.tgz",
"integrity": "sha512-Wsc/OGT9vcwLJ34PPJ9lMngQY4SkyJaRePSXaFzXVzZS39TZdUUXiPAhdrZw36MQJsslZluqiwl95EsXJIIsuA==",
"version": "10.10.3",
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz",
"integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==",
"dev": true,
"dependencies": {
"testcontainers": "^10.10.1"
"testcontainers": "^10.10.3"
}
},
"node_modules/@tsconfig/node10": {
@@ -6478,16 +6478,16 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -6511,15 +6511,15 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
"dependencies": {
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4"
},
"engines": {
@@ -6539,13 +6539,13 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -6556,13 +6556,13 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
"dependencies": {
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -6583,9 +6583,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true,
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -6596,13 +6596,13 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -6648,15 +6648,15 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -6670,12 +6670,12 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true,
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -9549,10 +9549,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-27.0.0.tgz",
"integrity": "sha512-/jHX8Jjadj0YJzpqnuBo1Yy2ln2hnRbBIc+3jcVOLQ6qhHEKsLRlfJ145Ghn7k/EcnfpDzVX3V8AUCTC8juTow==",
"license": "MIT",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz",
"integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==",
"dependencies": {
"@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2",
@@ -9561,23 +9560,23 @@
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.85.0",
"exiftool-vendored.pl": "12.85.0"
"exiftool-vendored.exe": "12.84.0",
"exiftool-vendored.pl": "12.84.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz",
"integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz",
"integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz",
"integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz",
"integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==",
"optional": true,
"os": [
"!win32"
@@ -14447,18 +14446,109 @@
}
},
"node_modules/rimraf": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
"integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
"dev": true,
"dependencies": {
"glob": "^10.3.7"
"glob": "^11.0.0",
"package-json-from-dist": "^1.0.0"
},
"bin": {
"rimraf": "dist/esm/bin.mjs"
},
"engines": {
"node": ">=14.18"
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/rimraf/node_modules/glob": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
"integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
"dev": true,
"dependencies": {
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
},
"bin": {
"glob": "dist/esm/bin.mjs"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/jackspeak": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
"integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
"dev": true,
"dependencies": {
"@isaacs/cliui": "^8.0.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
},
"optionalDependencies": {
"@pkgjs/parseargs": "^0.11.0"
}
},
"node_modules/rimraf/node_modules/lru-cache": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
"integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
"dev": true,
"engines": {
"node": "20 || >=22"
}
},
"node_modules/rimraf/node_modules/minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rimraf/node_modules/path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"dependencies": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
},
"engines": {
"node": "20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -15657,9 +15747,9 @@
}
},
"node_modules/testcontainers": {
"version": "10.10.2",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.2.tgz",
"integrity": "sha512-Wbzi7enfpKFtmb7kffs6EBksPX7J6jHFWIsLYe2VOSrnscX2hRs84TNvl9Hfro8WTCtAfuY/935kunCcdlzi8A==",
"version": "10.10.3",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz",
"integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==",
"dev": true,
"dependencies": {
"@balena/dockerignore": "^1.0.2",
@@ -20471,92 +20561,92 @@
"integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw=="
},
"@swc/core": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.7.tgz",
"integrity": "sha512-BBzORL9qWz5hZqAZ83yn+WNaD54RH5eludjqIOboolFOK/Pw+2l00/H77H4CEBJnzCIBQszsyqtITmrn4evp0g==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz",
"integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==",
"devOptional": true,
"requires": {
"@swc/core-darwin-arm64": "1.6.7",
"@swc/core-darwin-x64": "1.6.7",
"@swc/core-linux-arm-gnueabihf": "1.6.7",
"@swc/core-linux-arm64-gnu": "1.6.7",
"@swc/core-linux-arm64-musl": "1.6.7",
"@swc/core-linux-x64-gnu": "1.6.7",
"@swc/core-linux-x64-musl": "1.6.7",
"@swc/core-win32-arm64-msvc": "1.6.7",
"@swc/core-win32-ia32-msvc": "1.6.7",
"@swc/core-win32-x64-msvc": "1.6.7",
"@swc/core-darwin-arm64": "1.6.13",
"@swc/core-darwin-x64": "1.6.13",
"@swc/core-linux-arm-gnueabihf": "1.6.13",
"@swc/core-linux-arm64-gnu": "1.6.13",
"@swc/core-linux-arm64-musl": "1.6.13",
"@swc/core-linux-x64-gnu": "1.6.13",
"@swc/core-linux-x64-musl": "1.6.13",
"@swc/core-win32-arm64-msvc": "1.6.13",
"@swc/core-win32-ia32-msvc": "1.6.13",
"@swc/core-win32-x64-msvc": "1.6.13",
"@swc/counter": "^0.1.3",
"@swc/types": "^0.1.9"
}
},
"@swc/core-darwin-arm64": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.7.tgz",
"integrity": "sha512-sNb+ghP2OhZyUjS7E5Mf3PqSvoXJ5gY6GBaH2qp8WQxx9VL7ozC4HVo6vkeFJBN5cmYqUCLnhrM3HU4W+7yMSA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz",
"integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==",
"dev": true,
"optional": true
},
"@swc/core-darwin-x64": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.7.tgz",
"integrity": "sha512-LQwYm/ATYN5fYSYVPMfComPiFo5i8jh75h1ASvNWhXtS+/+k1dq1zXTJWZRuojd5NXgW3bb6mJtJ2evwYIgYbA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz",
"integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm-gnueabihf": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.7.tgz",
"integrity": "sha512-kEDzVhNci38LX3kdY99t68P2CDf+2QFDk5LawVamXH0iN5DRAO/+wjOhxL8KOHa6wQVqKEt5WrhD+Rrvk/34Yw==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz",
"integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm64-gnu": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.7.tgz",
"integrity": "sha512-SyOBUGfl31xLGpIJ/Jd6GKHtkfZyHBXSwFlK7FmPN//MBQLtTBm4ZaWTnWnGo4aRsJwQdXWDKPyqlMBtnIl1nQ==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz",
"integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==",
"dev": true,
"optional": true
},
"@swc/core-linux-arm64-musl": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.7.tgz",
"integrity": "sha512-1fOAXkDFbRfItEdMZPxT3du1QWYhgToa4YsnqTujjE8EqJW8K27hIcHRIkVuzp7PNhq8nLBg0JpJM4g27EWD7g==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz",
"integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==",
"dev": true,
"optional": true
},
"@swc/core-linux-x64-gnu": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.7.tgz",
"integrity": "sha512-Gp7uCwPsNO5ATxbyvfTyeNCHUGD9oA+xKMm43G1tWCy+l07gLqWMKp7DIr3L3qPD05TfAVo3OuiOn2abpzOFbw==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz",
"integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==",
"dev": true,
"optional": true
},
"@swc/core-linux-x64-musl": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.7.tgz",
"integrity": "sha512-QeruGBZJ15tadqEMQ77ixT/CYGk20MtlS8wmvJiV+Wsb8gPW5LgCjtupzcLLnoQzDG54JGNCeeZ0l/T8NYsOvA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz",
"integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==",
"dev": true,
"optional": true
},
"@swc/core-win32-arm64-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.7.tgz",
"integrity": "sha512-ouRqgSnT95lTCiU/6kJRNS5b1o+p8I/V9jxtL21WUj/JOVhsFmBErqQ0MZyCu514noWiR5BIqOrZXR8C1Knx6Q==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz",
"integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==",
"dev": true,
"optional": true
},
"@swc/core-win32-ia32-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.7.tgz",
"integrity": "sha512-eZAP/EmJ0IcfgAx6B4/SpSjq3aT8gr0ooktfMqw/w0/5lnNrbMl2v+2kvxcneNcF7bp8VNcYZnoHlsP+LvmVbA==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz",
"integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==",
"dev": true,
"optional": true
},
"@swc/core-win32-x64-msvc": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.7.tgz",
"integrity": "sha512-QOdE+7GQg1UQPS6p0KxzJOh/8GLbJ5zI1vqKArCCB0unFqUfKIjYb2TaH0geEBy3w9qtXxe3ZW6hzxtZSS9lDg==",
"version": "1.6.13",
"resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz",
"integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==",
"dev": true,
"optional": true
},
@@ -20582,12 +20672,12 @@
}
},
"@testcontainers/postgresql": {
"version": "10.10.1",
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.1.tgz",
"integrity": "sha512-Wsc/OGT9vcwLJ34PPJ9lMngQY4SkyJaRePSXaFzXVzZS39TZdUUXiPAhdrZw36MQJsslZluqiwl95EsXJIIsuA==",
"version": "10.10.3",
"resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz",
"integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==",
"dev": true,
"requires": {
"testcontainers": "^10.10.1"
"testcontainers": "^10.10.3"
}
},
"@tsconfig/node10": {
@@ -21134,16 +21224,16 @@
}
},
"@typescript-eslint/eslint-plugin": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
"requires": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -21151,54 +21241,54 @@
}
},
"@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
"requires": {
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4"
}
},
"@typescript-eslint/scope-manager": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
}
},
"@typescript-eslint/type-utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
"requires": {
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
}
},
"@typescript-eslint/types": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true
},
"@typescript-eslint/typescript-estree": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -21228,24 +21318,24 @@
}
},
"@typescript-eslint/utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
"requires": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
}
},
"@typescript-eslint/visitor-keys": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true,
"requires": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3"
}
},
@@ -23343,29 +23433,29 @@
}
},
"exiftool-vendored": {
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-27.0.0.tgz",
"integrity": "sha512-/jHX8Jjadj0YJzpqnuBo1Yy2ln2hnRbBIc+3jcVOLQ6qhHEKsLRlfJ145Ghn7k/EcnfpDzVX3V8AUCTC8juTow==",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz",
"integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==",
"requires": {
"@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2",
"batch-cluster": "^13.0.0",
"exiftool-vendored.exe": "12.85.0",
"exiftool-vendored.pl": "12.85.0",
"exiftool-vendored.exe": "12.84.0",
"exiftool-vendored.pl": "12.84.0",
"he": "^1.2.0",
"luxon": "^3.4.4"
}
},
"exiftool-vendored.exe": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz",
"integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz",
"integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==",
"optional": true
},
"exiftool-vendored.pl": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz",
"integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz",
"integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==",
"optional": true
},
"express": {
@@ -26726,12 +26816,73 @@
"integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw=="
},
"rimraf": {
"version": "5.0.7",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.7.tgz",
"integrity": "sha512-nV6YcJo5wbLW77m+8KjH8aB/7/rxQy9SZ0HY5shnwULfS+9nmTtVXAJET5NdZmCzA4fPI/Hm1wo/Po/4mopOdg==",
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz",
"integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==",
"dev": true,
"requires": {
"glob": "^10.3.7"
"glob": "^11.0.0",
"package-json-from-dist": "^1.0.0"
},
"dependencies": {
"brace-expansion": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz",
"integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==",
"dev": true,
"requires": {
"balanced-match": "^1.0.0"
}
},
"glob": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz",
"integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==",
"dev": true,
"requires": {
"foreground-child": "^3.1.0",
"jackspeak": "^4.0.1",
"minimatch": "^10.0.0",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
}
},
"jackspeak": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz",
"integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==",
"dev": true,
"requires": {
"@isaacs/cliui": "^8.0.2",
"@pkgjs/parseargs": "^0.11.0"
}
},
"lru-cache": {
"version": "11.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz",
"integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==",
"dev": true
},
"minimatch": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
"dev": true,
"requires": {
"brace-expansion": "^2.0.1"
}
},
"path-scurry": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz",
"integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==",
"dev": true,
"requires": {
"lru-cache": "^11.0.0",
"minipass": "^7.1.2"
}
}
}
},
"rollup": {
@@ -27618,9 +27769,9 @@
}
},
"testcontainers": {
"version": "10.10.2",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.2.tgz",
"integrity": "sha512-Wbzi7enfpKFtmb7kffs6EBksPX7J6jHFWIsLYe2VOSrnscX2hRs84TNvl9Hfro8WTCtAfuY/935kunCcdlzi8A==",
"version": "10.10.3",
"resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz",
"integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==",
"dev": true,
"requires": {
"@balena/dockerignore": "^1.0.2",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.108.0",
"version": "1.109.2",
"description": "",
"author": "",
"private": true,
@@ -60,7 +60,7 @@
"class-transformer": "^0.5.1",
"class-validator": "^0.14.0",
"cookie-parser": "^1.4.6",
"exiftool-vendored": "~27.0.0",
"exiftool-vendored": "26.0.0",
"fast-glob": "^3.3.2",
"fluent-ffmpeg": "^2.1.2",
"geo-tz": "^8.0.0",
@@ -122,7 +122,7 @@
"mock-fs": "^5.2.0",
"prettier": "^3.0.2",
"prettier-plugin-organize-imports": "^4.0.0",
"rimraf": "^5.0.1",
"rimraf": "^6.0.0",
"source-map-support": "^0.5.21",
"sql-formatter": "^15.0.0",
"tsconfig-paths": "^4.2.0",

View File

@@ -43,6 +43,7 @@ export const resourcePaths = {
admin1: join(folders.geodata, 'admin1CodesASCII.txt'),
admin2: join(folders.geodata, 'admin2Codes.txt'),
cities500: join(folders.geodata, citiesFile),
naturalEarthCountriesPath: join(folders.geodata, 'ne_10m_admin_0_countries.geojson'),
},
web: {
root: folders.web,

View File

@@ -95,7 +95,7 @@ export class ServerController {
@Get('license')
@Authenticated({ admin: true })
getServerLicense(): Promise<LicenseKeyDto | null> {
getServerLicense(): Promise<LicenseResponseDto | null> {
return this.service.getLicense();
}
}

View File

@@ -49,23 +49,26 @@ function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>>
* @param options.paramIndex The index of the function parameter to chunk. Defaults to 0.
* @param options.flatten Whether to flatten the results. Defaults to false.
*/
export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
export function Chunked(
options: { paramIndex?: number; chunkSize?: number; mergeFn?: (results: any) => any } = {},
): MethodDecorator {
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
const originalMethod = descriptor.value;
const parameterIndex = options.paramIndex ?? 0;
const chunkSize = options.chunkSize || DATABASE_PARAMETER_CHUNK_SIZE;
descriptor.value = async function (...arguments_: any[]) {
const argument = arguments_[parameterIndex];
// Early return if argument length is less than or equal to the chunk size.
if (
(Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
(argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
(Array.isArray(argument) && argument.length <= chunkSize) ||
(argument instanceof Set && argument.size <= chunkSize)
) {
return await originalMethod.apply(this, arguments_);
}
return Promise.all(
chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
chunks(argument, chunkSize).map(async (chunk) => {
await Reflect.apply(originalMethod, this, [
...arguments_.slice(0, parameterIndex),
chunk,

View File

@@ -0,0 +1,186 @@
import {
Body,
Button,
Column,
Container,
Head,
Hr,
Html,
Img,
Link,
Preview,
Row,
Section,
Text,
} from '@react-email/components';
import * as CSS from 'csstype';
import * as React from 'react';
/**
* Template to be used for FUTOPay project
* Variable is {{LICENSEKEY}}
* */
export const LicenseEmail = () => (
<Html>
<Head />
<Preview>Your Immich Server License</Preview>
<Body
style={{
margin: 0,
padding: 0,
backgroundColor: '#f4f4f4',
color: 'rgb(28,28,28)',
fontFamily: 'Overpass, sans-serif',
fontSize: '18px',
lineHeight: '24px',
}}
>
<Container
style={{
width: '540px',
maxWidth: '100%',
padding: '10px',
margin: '0 auto',
}}
>
<Section
style={{
padding: '36px',
tableLayout: 'fixed',
backgroundColor: '#fefefe',
borderRadius: '16px',
textAlign: 'center' as const,
}}
>
<Img
src="https://immich.app/img/immich-logo-inline-light.png"
alt="Immich"
style={{
height: 'auto',
margin: '0 auto 48px auto',
width: '50%',
alignSelf: 'center',
color: 'white',
}}
/>
<Text style={text}>Thank you for supporting Immich and open-source software</Text>
<Text style={text}>
Your <strong>Immich</strong> license key is
</Text>
<Section
style={{
textAlign: 'center',
background: 'rgb(225, 225, 225)',
borderRadius: '16px',
marginBottom: '25px',
}}
>
<Text style={{ fontFamily: 'monospace', fontWeight: 600, color: 'rgb(66, 80, 175)' }}>
{'{{LICENSEKEY}}'}
</Text>
</Section>
{/* <Text style={text}>
To activate your instance, you can click the following button or copy and paste the link below to your
browser
</Text>
<Row>
<Column align="center">
<Button
style={button}
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
Activate
</Button>
</Column>
</Row>
<Row>
<Column align="center">
<a
style={{ marginTop: '50px', color: 'rgb(66, 80, 175)', fontSize: '0.9rem' }}
href={`https://my.immich.app/link?target=activate_license&licenseKey={{LICENSEKEY}}&activationKey={{ACTIVATIONKEY}}`}
>
https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey=
{'{{ACTIVATIONKEY}}'}
</a>
</Column>
</Row> */}
</Section>
<Section style={{ textAlign: 'center' }}>
<Row>
<Column align="center">
<Link href="https://futo.org">
<Img
src="https://futo.org/images/FutoMainLogo.svg"
alt="FUTO"
style={{
height: '24px',
marginTop: '25px',
marginBottom: '25px',
}}
/>
</Link>
</Column>
</Row>
</Section>
<Hr style={{ color: 'rgb(66, 80, 175)', marginTop: '0' }} />
<Section style={{ textAlign: 'center' }}>
<Column align="center">
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
<Img
src={`https://immich.app/img/ios-app-store-badge.png`}
alt="Immich"
style={{ height: '72px', padding: '14px' }}
/>
</Link>
<Link href="https://play.google.com/store/apps/details?id=app.alextran.immich">
<Img src={`https://immich.app/img/google-play-badge.png`} height="96px" alt="Immich" />
</Link>
</Column>
</Section>
<Text
style={{
color: '#6a737d',
fontSize: '0.8rem',
textAlign: 'center' as const,
marginTop: '14px',
}}
>
<Link href="https://immich.app">Immich</Link> project is available under GNU AGPL v3 license.
</Text>
</Container>
</Body>
</Html>
);
LicenseEmail.PreviewProps = {};
export default LicenseEmail;
const text = {
margin: '0 0 24px 0',
textAlign: 'left' as const,
fontSize: '16px',
lineHeight: '24px',
};
const button: CSS.Properties = {
backgroundColor: 'rgb(66, 80, 175)',
margin: '1em 0',
padding: '0.75em 3em',
color: '#fff',
fontSize: '1em',
fontWeight: 600,
lineHeight: 1.5,
textTransform: 'uppercase',
borderRadius: '9999px',
};

View File

@@ -12,6 +12,7 @@ import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { LibraryEntity } from 'src/entities/library.entity';
import { MemoryEntity } from 'src/entities/memory.entity';
import { MoveEntity } from 'src/entities/move.entity';
import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
import { PartnerEntity } from 'src/entities/partner.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SessionEntity } from 'src/entities/session.entity';
@@ -36,6 +37,7 @@ export const entities = [
ExifEntity,
FaceSearchEntity,
GeodataPlacesEntity,
NaturalEarthCountriesEntity,
MemoryEntity,
MoveEntity,
PartnerEntity,

View File

@@ -0,0 +1,19 @@
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity('naturalearth_countries', { synchronize: false })
export class NaturalEarthCountriesEntity {
@PrimaryGeneratedColumn()
id!: number;
@Column({ type: 'varchar', length: 50 })
admin!: string;
@Column({ type: 'varchar', length: 3 })
admin_a3!: string;
@Column({ type: 'varchar', length: 50 })
type!: string;
@Column({ type: 'polygon' })
coordinates!: string;
}

View File

@@ -12,6 +12,7 @@ export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadat
export enum SystemMetadataKey {
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
FACIAL_RECOGNITION_STATE = 'facial-recognition-state',
ADMIN_ONBOARDING = 'admin-onboarding',
SYSTEM_CONFIG = 'system-config',
VERSION_CHECK_STATE = 'version-check-state',
@@ -22,6 +23,7 @@ export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string }
export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
[SystemMetadataKey.FACIAL_RECOGNITION_STATE]: { lastRun?: string };
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;

View File

@@ -30,6 +30,6 @@ export interface IAlbumRepository extends IBulkAsset {
getAll(): Promise<AlbumEntity[]>;
create(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
update(album: Partial<AlbumEntity>): Promise<AlbumEntity>;
delete(album: AlbumEntity): Promise<void>;
delete(id: string): Promise<void>;
updateThumbnails(): Promise<number | undefined>;
}

View File

@@ -153,6 +153,10 @@ export interface IDeferrableJob extends IEntityJob {
deferred?: boolean;
}
export interface INightlyJob extends IBaseJob {
nightly?: boolean;
}
export interface IEmailJob {
to: string;
subject: string;
@@ -229,7 +233,7 @@ export type JobItem =
// Facial Recognition
| { name: JobName.QUEUE_FACE_DETECTION; data: IBaseJob }
| { name: JobName.FACE_DETECTION; data: IEntityJob }
| { name: JobName.QUEUE_FACIAL_RECOGNITION; data: IBaseJob }
| { name: JobName.QUEUE_FACIAL_RECOGNITION; data: INightlyJob }
| { name: JobName.FACIAL_RECOGNITION; data: IDeferrableJob }
| { name: JobName.GENERATE_PERSON_THUMBNAIL; data: IEntityJob }

View File

@@ -16,6 +16,7 @@ export interface ThumbnailOptions {
colorspace: string;
quality: number;
crop?: CropOptions;
processInvalidImages: boolean;
}
export interface VideoStreamInfo {

View File

@@ -64,4 +64,5 @@ export interface IPersonRepository {
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
reassignFaces(data: UpdateFacesData): Promise<number>;
update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
getLatestFaceDate(): Promise<string | undefined>;
}

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NaturalEarthCountries1720375641148 implements MigrationInterface {
name = 'NaturalEarthCountries1720375641148'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "naturalearth_countries" ("id" SERIAL NOT NULL, "admin" character varying(50) NOT NULL, "admin_a3" character varying(3) NOT NULL, "type" character varying(50) NOT NULL, "coordinates" polygon NOT NULL, CONSTRAINT "PK_21a6d86d1ab5d841648212e5353" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "naturalearth_countries"`);
}
}

View File

@@ -434,3 +434,9 @@ WHERE
(("AssetFaceEntity"."personId" = $1))
LIMIT
1
-- PersonRepository.getLatestFaceDate
SELECT
MAX("jobStatus"."facesRecognizedAt")::text AS "latestDate"
FROM
"asset_job_status" "jobStatus"

View File

@@ -5,7 +5,16 @@ import { AlbumEntity } from 'src/entities/album.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface';
import { Instrumentation } from 'src/utils/instrumentation';
import { DataSource, FindOptionsOrder, FindOptionsRelations, In, IsNull, Not, Repository } from 'typeorm';
import {
DataSource,
EntityManager,
FindOptionsOrder,
FindOptionsRelations,
In,
IsNull,
Not,
Repository,
} from 'typeorm';
const withoutDeletedUsers = <T extends AlbumEntity | null>(album: T) => {
if (album) {
@@ -255,24 +264,46 @@ export class AlbumRepository implements IAlbumRepository {
@GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] })
async addAssetIds(albumId: string, assetIds: string[]): Promise<void> {
await this.dataSource
.createQueryBuilder()
.insert()
.into('albums_assets_assets', ['albumsId', 'assetsId'])
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
.execute();
await this.addAssets(this.dataSource.manager, albumId, assetIds);
}
create(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
return this.save(album);
return this.dataSource.transaction<AlbumEntity>(async (manager) => {
const { id } = await manager.save(AlbumEntity, { ...album, assets: [] });
const assetIds = (album.assets || []).map((asset) => asset.id);
await this.addAssets(manager, id, assetIds);
return manager.findOneOrFail(AlbumEntity, {
where: { id },
relations: {
owner: true,
albumUsers: { user: true },
sharedLinks: true,
assets: true,
},
});
});
}
update(album: Partial<AlbumEntity>): Promise<AlbumEntity> {
return this.save(album);
}
async delete(album: AlbumEntity): Promise<void> {
await this.repository.remove(album);
async delete(id: string): Promise<void> {
await this.repository.delete({ id });
}
@Chunked({ paramIndex: 2, chunkSize: 30_000 })
private async addAssets(manager: EntityManager, albumId: string, assetIds: string[]): Promise<void> {
if (assetIds.length === 0) {
return;
}
await manager
.createQueryBuilder()
.insert()
.into('albums_assets_assets', ['albumsId', 'assetsId'])
.values(assetIds.map((assetId) => ({ albumsId: albumId, assetsId: assetId })))
.execute();
}
private async save(album: Partial<AlbumEntity>) {

View File

@@ -7,6 +7,7 @@ import readLine from 'node:readline';
import { citiesFile, resourcePaths } from 'src/constants';
import { AssetEntity } from 'src/entities/asset.entity';
import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
import { ILoggerRepository } from 'src/interfaces/logger.interface';
import {
@@ -28,6 +29,8 @@ export class MapRepository implements IMapRepository {
constructor(
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(GeodataPlacesEntity) private geodataPlacesRepository: Repository<GeodataPlacesEntity>,
@InjectRepository(NaturalEarthCountriesEntity)
private naturalEarthCountriesRepository: Repository<NaturalEarthCountriesEntity>,
@InjectDataSource() private dataSource: DataSource,
@Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
@Inject(ILoggerRepository) private logger: ILoggerRepository,
@@ -46,6 +49,7 @@ export class MapRepository implements IMapRepository {
}
await this.importGeodata();
await this.importNaturalEarthCountries();
await this.metadataRepository.set(SystemMetadataKey.REVERSE_GEOCODING_STATE, {
lastUpdate: geodataDate,
@@ -130,22 +134,93 @@ export class MapRepository implements IMapRepository {
.limit(1)
.getOne();
if (!response) {
if (response) {
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
const { countryCode, name: city, admin1Name } = response;
const country = getName(countryCode, 'en') ?? null;
const state = admin1Name;
return { country, state, city };
}
this.logger.warn(
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
);
const ne_response = await this.naturalEarthCountriesRepository
.createQueryBuilder('naturalearth_countries')
.where('coordinates @> point (:longitude, :latitude)', point)
.limit(1)
.getOne();
if (!ne_response) {
this.logger.warn(
`Response from database for reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
`Response from database for natural earth reverse geocoding latitude: ${point.latitude}, longitude: ${point.longitude} was null`,
);
return null;
}
this.logger.verbose(`Raw: ${JSON.stringify(response, null, 2)}`);
this.logger.verbose(`Raw: ${JSON.stringify(ne_response, ['id', 'admin', 'admin_a3', 'type'], 2)}`);
const { countryCode, name: city, admin1Name } = response;
const country = getName(countryCode, 'en') ?? null;
const state = admin1Name;
const { admin_a3 } = ne_response;
const country = getName(admin_a3, 'en') ?? null;
const state = null;
const city = null;
return { country, state, city };
}
private transformCoordinatesToPolygon(coordinates: number[][][]): string {
const pointsString = coordinates.map((point) => `(${point[0]},${point[1]})`).join(', ');
return `(${pointsString})`;
}
private async importNaturalEarthCountries() {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();
try {
await queryRunner.startTransaction();
await queryRunner.manager.clear(NaturalEarthCountriesEntity);
const fileContent = await readFile(resourcePaths.geodata.naturalEarthCountriesPath, 'utf8');
const geoJSONData = JSON.parse(fileContent);
if (geoJSONData.type !== 'FeatureCollection' || !Array.isArray(geoJSONData.features)) {
this.logger.fatal('Invalid GeoJSON FeatureCollection');
return;
}
for await (const feature of geoJSONData.features) {
for (const polygon of feature.geometry.coordinates) {
const featureRecord = new NaturalEarthCountriesEntity();
featureRecord.admin = feature.properties.ADMIN;
featureRecord.admin_a3 = feature.properties.ADM0_A3;
featureRecord.type = feature.properties.TYPE;
if (feature.geometry.type === 'MultiPolygon') {
featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon[0]);
await queryRunner.manager.save(featureRecord);
} else if (feature.geometry.type === 'Polygon') {
featureRecord.coordinates = this.transformCoordinatesToPolygon(polygon);
await queryRunner.manager.save(featureRecord);
break;
}
}
}
await queryRunner.commitTransaction();
} catch (error) {
this.logger.fatal('Error importing natural earth country data', error);
await queryRunner.rollbackTransaction();
throw error;
} finally {
await queryRunner.release();
}
}
private async importGeodata() {
const queryRunner = this.dataSource.createQueryRunner();
await queryRunner.connect();

View File

@@ -45,7 +45,8 @@ export class MediaRepository implements IMediaRepository {
}
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
const pipeline = sharp(input, { failOn: 'error', limitInputPixels: false })
// some invalid images can still be processed by sharp, but we want to fail on them by default to avoid crashes
const pipeline = sharp(input, { failOn: options.processInvalidImages ? 'none' : 'error', limitInputPixels: false })
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
.rotate();

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
import geotz from 'geo-tz';
import { DummyValue, GenerateSql } from 'src/decorators';
import { ExifEntity } from 'src/entities/exif.entity';
@@ -17,39 +17,40 @@ export class MetadataRepository implements IMetadataRepository {
@Inject(ILoggerRepository) private logger: ILoggerRepository,
) {
this.logger.setContext(MetadataRepository.name);
this.exiftool = new ExifTool({
defaultVideosToUTC: true,
backfillTimezones: true,
inferTimezoneFromDatestamps: true,
useMWG: true,
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
geoTz: (lat, lon) => geotz.find(lat, lon)[0],
// Enable exiftool LFS to parse metadata for files larger than 2GB.
readArgs: ['-api', 'largefilesupport=1'],
writeArgs: ['-api', 'largefilesupport=1', '-overwrite_original'],
});
}
private exiftool: ExifTool;
async teardown() {
await this.exiftool.end();
await exiftool.end();
}
readTags(path: string): Promise<ImmichTags | null> {
return this.exiftool.read(path).catch((error) => {
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
return null;
}) as Promise<ImmichTags | null>;
return exiftool
.read(path, undefined, {
...DefaultReadTaskOptions,
// Enable exiftool LFS to parse metadata for files larger than 2GB.
optionalArgs: ['-api', 'largefilesupport=1'],
defaultVideosToUTC: true,
backfillTimezones: true,
inferTimezoneFromDatestamps: true,
useMWG: true,
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
geoTz: (lat, lon) => geotz.find(lat, lon)[0],
})
.catch((error) => {
this.logger.warn(`Error reading exif data (${path}): ${error}`, error?.stack);
return null;
}) as Promise<ImmichTags | null>;
}
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
return this.exiftool.extractBinaryTagToBuffer(tagName, path);
return exiftool.extractBinaryTagToBuffer(tagName, path);
}
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
try {
await this.exiftool.write(path, tags);
await exiftool.write(path, tags, ['-overwrite_original']);
} catch (error) {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
}

View File

@@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import _ from 'lodash';
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity';
import { AssetEntity } from 'src/entities/asset.entity';
import { PersonEntity } from 'src/entities/person.entity';
import {
@@ -25,6 +26,7 @@ export class PersonRepository implements IPersonRepository {
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
@InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository<AssetJobStatusEntity>,
) {}
@GenerateSql({ params: [{ oldPersonId: DummyValue.UUID, newPersonId: DummyValue.UUID }] })
@@ -267,4 +269,13 @@ export class PersonRepository implements IPersonRepository {
async getRandomFace(personId: string): Promise<AssetFaceEntity | null> {
return this.assetFaceRepository.findOneBy({ personId });
}
@GenerateSql()
async getLatestFaceDate(): Promise<string | undefined> {
const result: { latestDate?: string } | undefined = await this.jobStatusRepository
.createQueryBuilder('jobStatus')
.select('MAX(jobStatus.facesRecognizedAt)::text', 'latestDate')
.getRawOne();
return result?.latestDate;
}
}

View File

@@ -302,8 +302,7 @@ describe(AlbumService.name, () => {
describe('delete', () => {
it('should throw an error for an album not found', async () => {
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set([albumStub.sharedWithAdmin.id]));
albumMock.getById.mockResolvedValue(null);
accessMock.album.checkOwnerAccess.mockResolvedValue(new Set());
await expect(sut.delete(authStub.admin, albumStub.sharedWithAdmin.id)).rejects.toBeInstanceOf(
BadRequestException,
@@ -329,7 +328,7 @@ describe(AlbumService.name, () => {
await sut.delete(authStub.admin, albumStub.empty.id);
expect(albumMock.delete).toHaveBeenCalledTimes(1);
expect(albumMock.delete).toHaveBeenCalledWith(albumStub.empty);
expect(albumMock.delete).toHaveBeenCalledWith(albumStub.empty.id);
});
});

View File

@@ -165,10 +165,7 @@ export class AlbumService {
async delete(auth: AuthDto, id: string): Promise<void> {
await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id);
const album = await this.findOrFail(id, { withAssets: false });
await this.albumRepository.delete(album);
await this.albumRepository.delete(id);
}
async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise<BulkIdResponseDto[]> {

View File

@@ -71,7 +71,7 @@ describe(JobService.name, () => {
{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
{ name: JobName.CLEAN_OLD_AUDIT_LOGS },
{ name: JobName.USER_SYNC_USAGE },
{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } },
{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } },
{ name: JobName.CLEAN_OLD_SESSION_TOKENS },
]);
});

View File

@@ -210,7 +210,7 @@ export class JobService {
{ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force: false } },
{ name: JobName.CLEAN_OLD_AUDIT_LOGS },
{ name: JobName.USER_SYNC_USAGE },
{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } },
{ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false, nightly: true } },
{ name: JobName.CLEAN_OLD_SESSION_TOKENS },
]);
}

View File

@@ -296,6 +296,7 @@ describe(MediaService.name, () => {
format,
quality: 80,
colorspace: Colorspace.SRGB,
processInvalidImages: false,
});
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath });
});
@@ -326,6 +327,7 @@ describe(MediaService.name, () => {
format: ImageFormat.JPEG,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
);
expect(assetMock.update).toHaveBeenCalledWith({
@@ -468,6 +470,7 @@ describe(MediaService.name, () => {
format,
quality: 80,
colorspace: Colorspace.SRGB,
processInvalidImages: false,
});
expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath });
},
@@ -498,6 +501,7 @@ describe(MediaService.name, () => {
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
);
expect(assetMock.update).toHaveBeenCalledWith({
@@ -524,6 +528,7 @@ describe(MediaService.name, () => {
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
],
]);
@@ -548,6 +553,7 @@ describe(MediaService.name, () => {
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
],
]);
@@ -570,6 +576,7 @@ describe(MediaService.name, () => {
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
);
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
@@ -590,11 +597,34 @@ describe(MediaService.name, () => {
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: false,
},
);
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
});
it('should process invalid images if enabled', async () => {
vi.stubEnv('IMMICH_PROCESS_INVALID_IMAGES', 'true');
assetMock.getByIds.mockResolvedValue([assetStub.imageDng]);
await sut.handleGenerateThumbnail({ id: assetStub.image.id });
expect(mediaMock.generateThumbnail).toHaveBeenCalledWith(
assetStub.imageDng.originalPath,
'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp',
{
format: ImageFormat.WEBP,
size: 250,
quality: 80,
colorspace: Colorspace.P3,
processInvalidImages: true,
},
);
expect(mediaMock.getImageDimensions).not.toHaveBeenCalled();
vi.unstubAllEnvs();
});
describe('handleGenerateThumbhash', () => {
it('should skip thumbhash generation if asset not found', async () => {
assetMock.getByIds.mockResolvedValue([]);

View File

@@ -199,7 +199,13 @@ export class MediaService {
try {
const useExtracted = didExtract && (await this.shouldUseExtractedImage(extractedPath, image.previewSize));
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : image.colorspace;
const imageOptions = { format, size, colorspace, quality: image.quality };
const imageOptions = {
format,
size,
colorspace,
quality: image.quality,
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
};
const outputPath = useExtracted ? extractedPath : asset.originalPath;
await this.mediaRepository.generateThumbnail(outputPath, path, imageOptions);

View File

@@ -3,6 +3,7 @@ import { Colorspace } from 'src/config';
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
@@ -539,6 +540,7 @@ describe(PersonService.name, () => {
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
expect(jobMock.queueAll).not.toHaveBeenCalled();
expect(systemMock.get).toHaveBeenCalled();
expect(systemMock.set).not.toHaveBeenCalled();
});
it('should skip if recognition jobs are already queued', async () => {
@@ -546,6 +548,7 @@ describe(PersonService.name, () => {
await expect(sut.handleQueueRecognizeFaces({})).resolves.toBe(JobStatus.SKIPPED);
expect(jobMock.queueAll).not.toHaveBeenCalled();
expect(systemMock.set).not.toHaveBeenCalled();
});
it('should queue missing assets', async () => {
@@ -564,6 +567,9 @@ describe(PersonService.name, () => {
data: { id: faceStub.face1.id, deferred: false },
},
]);
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String),
});
});
it('should queue all assets', async () => {
@@ -586,6 +592,59 @@ describe(PersonService.name, () => {
data: { id: faceStub.face1.id, deferred: false },
},
]);
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String),
});
});
it('should run nightly if new face has been added since last run', async () => {
personMock.getLatestFaceDate.mockResolvedValue(new Date().toISOString());
personMock.getAllFaces.mockResolvedValue({
items: [faceStub.face1],
hasNextPage: false,
});
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
personMock.getAll.mockResolvedValue({
items: [],
hasNextPage: false,
});
personMock.getAllFaces.mockResolvedValue({
items: [faceStub.face1],
hasNextPage: false,
});
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
expect(personMock.getLatestFaceDate).toHaveBeenCalledOnce();
expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
expect(jobMock.queueAll).toHaveBeenCalledWith([
{
name: JobName.FACIAL_RECOGNITION,
data: { id: faceStub.face1.id, deferred: false },
},
]);
expect(systemMock.set).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE, {
lastRun: expect.any(String),
});
});
it('should skip nightly if no new face has been added since last run', async () => {
const lastRun = new Date();
systemMock.get.mockResolvedValue({ lastRun: lastRun.toISOString() });
personMock.getLatestFaceDate.mockResolvedValue(new Date(lastRun.getTime() - 1).toISOString());
personMock.getAllFaces.mockResolvedValue({
items: [faceStub.face1],
hasNextPage: false,
});
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
expect(personMock.getLatestFaceDate).toHaveBeenCalledOnce();
expect(personMock.getAllFaces).not.toHaveBeenCalled();
expect(jobMock.queueAll).not.toHaveBeenCalled();
expect(systemMock.set).not.toHaveBeenCalled();
});
it('should delete existing people and faces if forced', async () => {
@@ -899,6 +958,7 @@ describe(PersonService.name, () => {
width: 274,
height: 274,
},
processInvalidImages: false,
},
);
expect(personMock.update).toHaveBeenCalledWith({
@@ -928,6 +988,7 @@ describe(PersonService.name, () => {
width: 510,
height: 510,
},
processInvalidImages: false,
},
);
});
@@ -953,6 +1014,7 @@ describe(PersonService.name, () => {
width: 408,
height: 408,
},
processInvalidImages: false,
},
);
});
@@ -979,6 +1041,7 @@ describe(PersonService.name, () => {
width: 588,
height: 588,
},
processInvalidImages: false,
},
);
});

View File

@@ -26,6 +26,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
import { AssetEntity, AssetType } from 'src/entities/asset.entity';
import { PersonPathType } from 'src/entities/move.entity';
import { PersonEntity } from 'src/entities/person.entity';
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
import { IAccessRepository } from 'src/interfaces/access.interface';
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
@@ -34,6 +35,7 @@ import {
IDeferrableJob,
IEntityJob,
IJobRepository,
INightlyJob,
JOBS_ASSET_PAGINATION_SIZE,
JobItem,
JobName,
@@ -67,7 +69,7 @@ export class PersonService {
@Inject(IMoveRepository) moveRepository: IMoveRepository,
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
@Inject(IPersonRepository) private repository: IPersonRepository,
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
@Inject(ISystemMetadataRepository) private systemMetadataRepository: ISystemMetadataRepository,
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
@Inject(IJobRepository) private jobRepository: IJobRepository,
@Inject(ISearchRepository) private smartInfoRepository: ISearchRepository,
@@ -376,13 +378,26 @@ export class PersonService {
return JobStatus.SUCCESS;
}
async handleQueueRecognizeFaces({ force }: IBaseJob): Promise<JobStatus> {
async handleQueueRecognizeFaces({ force, nightly }: INightlyJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
if (!isFacialRecognitionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
await this.jobRepository.waitForQueueCompletion(QueueName.THUMBNAIL_GENERATION, QueueName.FACE_DETECTION);
if (nightly) {
const [state, latestFaceDate] = await Promise.all([
this.systemMetadataRepository.get(SystemMetadataKey.FACIAL_RECOGNITION_STATE),
this.repository.getLatestFaceDate(),
]);
if (state?.lastRun && latestFaceDate && state.lastRun > latestFaceDate) {
this.logger.debug('Skipping facial recognition nightly since no face has been added since the last run');
return JobStatus.SKIPPED;
}
}
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
if (force) {
@@ -394,6 +409,7 @@ export class PersonService {
return JobStatus.SKIPPED;
}
const lastRun = new Date().toISOString();
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.repository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull() } }),
);
@@ -404,6 +420,8 @@ export class PersonService {
);
}
await this.systemMetadataRepository.set(SystemMetadataKey.FACIAL_RECOGNITION_STATE, { lastRun });
return JobStatus.SUCCESS;
}
@@ -541,6 +559,7 @@ export class PersonService {
colorspace: image.colorspace,
quality: image.quality,
crop: this.getCrop({ old: { width: oldWidth, height: oldHeight }, new: { width, height } }, { x1, y1, x2, y2 }),
processInvalidImages: process.env.IMMICH_PROCESS_INVALID_IMAGES === 'true',
} as const;
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);

View File

@@ -164,7 +164,7 @@ export class ServerService implements OnEvents {
await this.systemMetadataRepository.delete(SystemMetadataKey.LICENSE);
}
async getLicense(): Promise<LicenseKeyDto | null> {
async getLicense(): Promise<LicenseResponseDto | null> {
return this.systemMetadataRepository.get(SystemMetadataKey.LICENSE);
}

View File

@@ -145,7 +145,7 @@ export class StorageTemplateService implements OnEvents {
return JobStatus.SKIPPED;
}
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
this.assetRepository.getAll(pagination, { withExif: true }),
this.assetRepository.getAll(pagination, { withExif: true, withArchived: true }),
);
const users = await this.userRepository.getList();

View File

@@ -29,5 +29,6 @@ export const newPersonRepositoryMock = (): Mocked<IPersonRepository> => {
getFaceById: vitest.fn(),
getFaceByIdWithAssets: vitest.fn(),
getNumberOfPeople: vitest.fn(),
getLatestFaceDate: vitest.fn(),
};
};

109
web/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-web",
"version": "1.108.0",
"version": "1.109.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-web",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@formatjs/icu-messageformat-parser": "^2.7.8",
@@ -47,6 +47,7 @@
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.1",
"autoprefixer": "^10.4.17",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",
@@ -69,7 +70,7 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
@@ -2368,17 +2369,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/type-utils": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -2402,16 +2403,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.15.0.tgz",
"integrity": "sha512-k9fYuQNnypLFcqORNClRykkGOMOj+pV6V91R4GO/l1FDGwpqmSwoOQrOHo3cGaH63e+D3ZiCAOsuS/D2c99j/A==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4"
},
"engines": {
@@ -2431,14 +2432,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.15.0.tgz",
"integrity": "sha512-Q/1yrF/XbxOTvttNVPihxh1b9fxamjEoz2Os/Pe38OHwxC24CyCqXxGTOdpb4lt6HYtqw9HetA/Rf6gDGaMPlw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2449,14 +2450,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -2477,9 +2478,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.15.0.tgz",
"integrity": "sha512-aV1+B1+ySXbQH0pLK0rx66I3IkiZNidYobyfn0WFsdGhSXw+P3YOqeTq5GED458SfB24tg+ux3S+9g118hjlTw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -2491,14 +2492,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.15.0.tgz",
"integrity": "sha512-gjyB/rHAopL/XxfmYThQbXbzRMGhZzGw6KpcMbfe8Q3nNQKStpxnUKeXb0KiN/fFDR42Z43szs6rY7eHk0zdGQ==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -2559,16 +2560,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
"@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.16.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -2582,13 +2583,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.15.0.tgz",
"integrity": "sha512-Hqgy/ETgpt2L5xueA/zHHIl4fJI2O4XUE9l4+OIfbJIRSnTJb/QscncdqqZzofQegIJugRIF57OJea1khw2SDw==",
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -3740,6 +3741,18 @@
"resolved": "https://registry.npmjs.org/dom-to-image/-/dom-to-image-2.6.0.tgz",
"integrity": "sha512-Dt0QdaHmLpjURjU7Tnu3AgYSF2LuOmksSGsUcE6ItvJoCWTBEmiMXcqBdNSAm9+QbbwD7JMoVsuuKX6ZVQv1qA=="
},
"node_modules/dotenv": {
"version": "16.4.5",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz",
"integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==",
"dev": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://dotenvx.com"
}
},
"node_modules/earcut": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz",
@@ -8150,9 +8163,9 @@
}
},
"node_modules/svelte-maplibre": {
"version": "0.9.8",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.8.tgz",
"integrity": "sha512-z6YyJv1sT8AHJuzuzd+30M9PQMllFnGBpHvSJ5BlwFQF/yP4xdJY9+ynF9ziywJIzGMjuvTiCeEXZSY0fhsTAA==",
"version": "0.9.9",
"resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.9.tgz",
"integrity": "sha512-y0NbKGquYCtQQi3vF1M09++Gg8TR5u/4zie1Rb2FIQI8XpvlBJJbBOsY8rkAGjRkH8t2BBtGstCRuoVHzkq3lA==",
"license": "MIT",
"dependencies": {
"d3-geo": "^3.1.0",

View File

@@ -1,6 +1,6 @@
{
"name": "immich-web",
"version": "1.108.0",
"version": "1.109.2",
"license": "GNU Affero General Public License version 3",
"scripts": {
"dev": "vite dev --host 0.0.0.0 --port 3000",
@@ -40,6 +40,7 @@
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.1",
"autoprefixer": "^10.4.17",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-svelte": "^2.35.1",

5
web/src/app.d.ts vendored
View File

@@ -27,3 +27,8 @@ declare namespace svelteHTML {
'on:zoomImage'?: () => void;
}
}
declare module '$env/static/public' {
export const PUBLIC_IMMICH_PAY_HOST: string;
export const PUBLIC_IMMICH_BUY_HOST: string;
}

View File

@@ -27,7 +27,7 @@
class="text-immich-primary dark:text-immich-dark-primary">{value}</span
>
{#if unit}
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{ByteUnit[unit]}</span>
<span class="absolute -top-5 right-2 text-base font-light text-gray-400">{unit}</span>
{/if}
</div>
</div>

View File

@@ -41,8 +41,7 @@
{/if}
</td>
<td class="text-md text-ellipsis text-center sm:w-2/12 md:w-2/12 xl:w-[15%] 2xl:w-[12%]">
{album.assetCount}
{album.assetCount > 1 ? `items` : `item`}
{$t('items_count', { values: { count: album.assetCount } })}
</td>
<td class="text-md hidden text-ellipsis text-center sm:block w-3/12 xl:w-[15%] 2xl:w-[12%]">
{dateLocaleString(album.updatedAt)}

View File

@@ -26,7 +26,7 @@
}
</script>
{#if asset.exifInfo?.city}
{#if asset.exifInfo?.country}
<button
type="button"
class="flex w-full text-left justify-between place-items-start gap-4 py-4"
@@ -39,7 +39,9 @@
<div><Icon path={mdiMapMarkerOutline} size="24" /></div>
<div>
<p>{asset.exifInfo.city}</p>
{#if asset.exifInfo?.city}
<p>{asset.exifInfo.city}</p>
{/if}
{#if asset.exifInfo?.state}
<div class="flex gap-2 text-sm">
<p>{asset.exifInfo.state}</p>

View File

@@ -8,6 +8,7 @@
import { mdiChevronLeft, mdiChevronRight } from '@mdi/js';
import { onMount } from 'svelte';
import { fade } from 'svelte/transition';
import { t } from 'svelte-i18n';
$: shouldRender = $memoryStore?.length > 0;
@@ -76,7 +77,7 @@
<img
class="h-full w-full rounded-xl object-cover"
src={getAssetThumbnailUrl(memory.assets[0].id)}
alt={`Memory Lane ${$getAltText(memory.assets[0])}`}
alt={$t('memory_lane_title', { values: { title: $getAltText(memory.assets[0]) } })}
draggable="false"
/>
<p class="absolute bottom-2 left-4 z-10 text-lg text-white">

View File

@@ -39,7 +39,7 @@
} else if (width === 'narrow') {
modalWidth = 'w-[28rem]';
} else {
modalWidth = 'sm:max-w-lg';
modalWidth = 'sm:max-w-4xl';
}
}
</script>

View File

@@ -0,0 +1,18 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { t } from 'svelte-i18n';
import { mdiPartyPopper } from '@mdi/js';
export let onDone: () => void;
</script>
<div class="m-auto w-3/4 text-center flex flex-col place-content-center place-items-center mb-6 dark:text-white">
<Icon path={mdiPartyPopper} class="text-immich-primary dark:text-immich-dark-primary" size="96" />
<p class="text-4xl mt-8 font-bold">{$t('license_activated_title')}</p>
<p class="text-lg mt-6">{$t('license_activated_subtitle')}</p>
<div class="mt-10 w-full">
<Button fullwidth on:click={onDone}>OK</Button>
</div>
</div>

View File

@@ -0,0 +1,70 @@
<script lang="ts">
import { user } from '$lib/stores/user.store';
import { handleError } from '$lib/utils/handle-error';
import ServerLicenseCard from './server-license-card.svelte';
import UserLicenseCard from './user-license-card.svelte';
import { activateLicense, getActivationKey } from '$lib/utils/license-utils';
import Button from '$lib/components/elements/buttons/button.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { licenseStore } from '$lib/stores/license.store';
import { t } from 'svelte-i18n';
export let onActivate: () => void;
let licenseKey = '';
let isLoading = false;
const activate = async () => {
try {
licenseKey = licenseKey.trim();
isLoading = true;
const activationKey = await getActivationKey(licenseKey);
await activateLicense(licenseKey, activationKey);
onActivate();
licenseStore.setLicenseStatus(true);
} catch (error) {
handleError(error, $t('license_failed_activation'));
} finally {
isLoading = false;
}
};
</script>
<section class="p-4">
<div>
<h1 class="text-4xl font-bold text-immich-primary dark:text-immich-dark-primary tracking-wider">
{$t('license_license_title')}
</h1>
<p class="text-lg mt-2 dark:text-immich-gray">{$t('license_license_subtitle')}</p>
</div>
<div class="flex gap-6 mt-4 justify-between">
{#if $user.isAdmin}
<ServerLicenseCard />
{/if}
<UserLicenseCard />
</div>
<div class="mt-6">
<p class="dark:text-immich-gray">{$t('license_input_suggestion')}</p>
<form class="mt-2 flex gap-2" on:submit={activate}>
<input
class="immich-form-input w-full"
id="licensekey"
type="text"
bind:value={licenseKey}
required
placeholder="IMCL-0KEY-0CAN-00BE-FOUD-FROM-YOUR-EMAIL-INBX"
disabled={isLoading}
/>
<Button type="submit" rounded="lg"
>{#if isLoading}
<LoadingSpinner />
{:else}
{$t('license_button_activate')}
{/if}</Button
>
</form>
</div>
</section>

View File

@@ -0,0 +1,25 @@
<script lang="ts">
import FullScreenModal from '$lib/components/shared-components/full-screen-modal.svelte';
import LicenseActivationSuccess from '$lib/components/shared-components/license/license-activation-success.svelte';
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
export let onClose: () => void;
let showLicenseActivated = false;
</script>
<Portal>
<FullScreenModal showLogo title={''} {onClose} width="wide">
{#if showLicenseActivated}
<LicenseActivationSuccess onDone={onClose} />
{:else}
<LicenseContent
onActivate={() => {
showLicenseActivated = true;
}}
/>
{/if}
</FullScreenModal>
</Portal>

View File

@@ -0,0 +1,44 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { ImmichLicense } from '$lib/constants';
import { getLicenseLink } from '$lib/utils/license-utils';
import { mdiCheckCircleOutline, mdiServer } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
<!-- SERVER LICENSE -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiServer} size="56" />
<p class="font-semibold text-lg mt-1">{$t('license_server_title')}</p>
</div>
<div class="mt-4 dark:text-immich-gray">
<p class="text-6xl font-bold">$99<span class="text-2xl font-medium">.99</span></p>
<p>{$t('license_per_server')}</p>
</div>
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">{$t('license_server_description_1')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">{$t('license_lifetime_description')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">{$t('license_server_description_2')}</p>
</div>
</div>
<a href={getLicenseLink(ImmichLicense.Server)}>
<Button fullwidth>{$t('license_button_select')}</Button>
</a>
</div>
</div>

View File

@@ -0,0 +1,39 @@
<script lang="ts">
import Button from '$lib/components/elements/buttons/button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import { ImmichLicense } from '$lib/constants';
import { getLicenseLink } from '$lib/utils/license-utils';
import { mdiAccount, mdiCheckCircleOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
</script>
<!-- USER LICENSE -->
<div class="border border-gray-300 dark:border-gray-800 w-[375px] p-8 rounded-3xl bg-gray-100 dark:bg-gray-900">
<div class="text-immich-primary dark:text-immich-dark-primary">
<Icon path={mdiAccount} size="56" />
<p class="font-semibold text-lg mt-1">{$t('license_individual_title')}</p>
</div>
<div class="mt-4 dark:text-immich-gray">
<p class="text-6xl font-bold">$24<span class="text-2xl font-medium">.99</span></p>
<p>{$t('license_per_user')}</p>
</div>
<div class="flex flex-col justify-between h-[200px] dark:text-immich-gray">
<div class="mt-6 flex flex-col gap-1">
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">{$t('license_individual_description_1')}</p>
</div>
<div class="grid grid-cols-[36px_auto]">
<Icon path={mdiCheckCircleOutline} size="24" class="text-green-500 self-center" />
<p class="self-center">{$t('license_lifetime_description')}</p>
</div>
</div>
<a href={getLicenseLink(ImmichLicense.Client)}>
<Button fullwidth>{$t('license_button_select')}</Button>
</a>
</div>
</div>

View File

@@ -31,6 +31,7 @@
const logOut = async () => {
const { redirectUri } = await logout();
if (redirectUri.startsWith('/')) {
await goto(redirectUri);
} else {

View File

@@ -45,6 +45,24 @@
}
</script>
<!--
@component
Allow rendering a component in a different part of the DOM.
### Props
- `target` - HTMLElement i.e "body", "html", default is "body"
### Default Slot
Used for every occurrence of an HTML tag in a message
- `tag` - Name of the tag
@example
```html
<Portal target="body">
<p>Your component in here</p>
</Portal>
```
-->
<script lang="ts">
/**
* DOM Element or CSS Selector

View File

@@ -52,13 +52,13 @@
bind:value={context}
/>
{:else}
<label for="file-name-input" class="immich-form-label">Search by file name or extension</label>
<label for="file-name-input" class="immich-form-label">{$t('search_by_filename')}</label>
<input
class="immich-form-input hover:cursor-text w-full !mt-1"
type="text"
id="file-name-input"
name="file-name"
placeholder="i.e. IMG_1234.JPG or PNG"
placeholder={$t('search_by_filename_example')}
bind:value={filename}
aria-labelledby="file-name-label"
/>

View File

@@ -1,7 +1,7 @@
<script lang="ts">
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
import SideBarLink from '$lib/components/shared-components/side-bar/side-bar-link.svelte';
import SideBarSection from '$lib/components/shared-components/side-bar/side-bar-section.svelte';
import StatusBox from '$lib/components/shared-components/status-box.svelte';
import { AppRoute } from '$lib/constants';
import { mdiAccountMultipleOutline, mdiBookshelf, mdiCog, mdiServer, mdiSync, mdiTools } from '@mdi/js';
import { t } from 'svelte-i18n';
@@ -17,7 +17,5 @@
<SideBarLink title={$t('repair')} routeId={AppRoute.ADMIN_REPAIR} icon={mdiTools} preloadData={false} />
</nav>
<div class="mb-6 mt-auto">
<StatusBox />
</div>
<BottomInfo />
</SideBarSection>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import LicenseInfo from './license-info.svelte';
import ServerStatus from './server-status.svelte';
import StorageSpace from './storage-space.svelte';
</script>
<div class="mt-auto">
<StorageSpace />
</div>
<div class="mb-2">
<LicenseInfo />
</div>
<div class="mb-6">
<ServerStatus />
</div>

View File

@@ -0,0 +1,117 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import { mdiClose, mdiInformationOutline, mdiLicense } from '@mdi/js';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import Button from '$lib/components/elements/buttons/button.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import LicenseModal from '$lib/components/shared-components/license/license-modal.svelte';
import { licenseStore } from '$lib/stores/license.store';
import { t } from 'svelte-i18n';
import { goto } from '$app/navigation';
import { AppRoute } from '$lib/constants';
import { getAccountAge } from '$lib/utils/auth';
import { fade } from 'svelte/transition';
let showMessage = false;
let isOpen = false;
let hoverMessage = false;
let hoverButton = false;
const { isLicenseActivated } = licenseStore;
const openLicenseModal = () => {
isOpen = true;
showMessage = false;
};
const onButtonHover = () => {
showMessage = true;
hoverButton = true;
};
$: if (showMessage && !hoverMessage && !hoverButton) {
setTimeout(() => {
if (!hoverMessage && !hoverButton) {
showMessage = false;
}
}, 300);
}
</script>
{#if isOpen}
<LicenseModal onClose={() => (isOpen = false)} />
{/if}
<div class="hidden md:block license-status pl-4 text-sm">
{#if $isLicenseActivated}
<button
on:click={() => goto(`${AppRoute.USER_SETTINGS}?isOpen=user-license-settings`)}
class="w-full"
type="button"
>
<div class="flex gap-1 mt-2 place-items-center dark:bg-immich-dark-primary/10 bg-gray-100 py-3 px-2 rounded-lg">
<Icon path={mdiLicense} size="18" class="text-immich-primary dark:text-immich-dark-primary" />
<p class="dark:text-gray-100">{$t('license_info_licensed')}</p>
</div>
</button>
{:else}
<button
type="button"
on:click={openLicenseModal}
on:mouseover={onButtonHover}
on:mouseleave={() => (hoverButton = false)}
on:focus={onButtonHover}
on:blur={() => (hoverButton = false)}
class="py-3 px-2 flex justify-between place-items-center place-content-center border border-gray-300 dark:border-immich-dark-primary/50 mt-2 rounded-lg shadow-sm dark:bg-immich-dark-primary/10 w-full"
>
<div class="flex place-items-center place-content-center gap-1">
<Icon path={mdiLicense} size="18" class="text-immich-dark-gray/75 dark:text-immich-gray/85" />
<p class="text-immich-dark-gray/75 dark:text-immich-gray">{$t('license_info_unlicensed')}</p>
</div>
<div class="text-immich-primary dark:text-immich-dark-primary flex place-items-center gap-[2px] font-medium">
{$t('license_button_buy')}
<span role="contentinfo">
<Icon path={mdiInformationOutline}></Icon>
</span>
</div>
</button>
{/if}
</div>
<Portal target="body">
{#if showMessage && getAccountAge() > 14}
<div
class="w-64 absolute bottom-[75px] left-[255px] bg-white dark:bg-gray-800 dark:text-white text-black rounded-xl z-10 shadow-2xl px-4 py-5"
transition:fade={{ duration: 150 }}
on:mouseover={() => (hoverMessage = true)}
on:mouseleave={() => (hoverMessage = false)}
on:focus={() => (hoverMessage = true)}
on:blur={() => (hoverMessage = false)}
role="dialog"
>
<div class="flex justify-between place-items-center">
<Icon path={mdiLicense} size="44" class="text-immich-dark-gray/75 dark:text-immich-gray" />
<CircleIconButton
icon={mdiClose}
on:click={() => {
showMessage = false;
}}
title={$t('close')}
size="18"
class="text-immich-dark-gray/85 dark:text-immich-gray"
/>
</div>
<h1 class="text-lg font-medium my-3">{$t('license_trial_info_1')}</h1>
<p class="text-immich-dark-gray/80 dark:text-immich-gray text-balance">
{$t('license_trial_info_2')}
<span class="text-immich-primary dark:text-immich-dark-primary font-semibold">
{$t('license_trial_info_3', { values: { accountAge: getAccountAge() } })}</span
>. {$t('license_trial_info_4')}
</p>
<div class="mt-3">
<Button size="sm" fullwidth on:click={openLicenseModal}>{$t('license_button_buy_license')}</Button>
</div>
</div>
{/if}
</Portal>

View File

@@ -0,0 +1,49 @@
<script lang="ts">
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
import { websocketStore } from '$lib/stores/websocket';
import { requestServerInfo } from '$lib/utils/auth';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
const { serverVersion, connected } = websocketStore;
let isOpen = false;
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
let aboutInfo: ServerAboutResponseDto;
onMount(async () => {
await requestServerInfo();
aboutInfo = await getAboutInfo();
});
</script>
{#if isOpen}
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
{/if}
<div
class="text-sm hidden group-hover:sm:flex md:flex pl-5 pr-1 place-items-center place-content-center justify-between"
>
{#if $connected}
<div class="flex gap-2 place-items-center place-content-center">
<div class="w-[7px] h-[7px] bg-green-500 rounded-full" />
<p class="dark:text-immich-gray">{$t('server_online')}</p>
</div>
{:else}
<div class="flex gap-2 place-items-center place-content-center">
<div class="w-[7px] h-[7px] bg-red-500 rounded-full" />
<p class="text-red-500">{$t('server_offline')}</p>
</div>
{/if}
<div class="flex justify-between justify-items-center">
{#if $connected && version}
<button type="button" on:click={() => (isOpen = true)} class="dark:text-immich-gray">{version}</button>
{:else}
<p class="text-red-500">{$t('unknown')}</p>
{/if}
</div>
</div>

View File

@@ -21,12 +21,12 @@
mdiToolbox,
mdiToolboxOutline,
} from '@mdi/js';
import StatusBox from '../status-box.svelte';
import SideBarSection from './side-bar-section.svelte';
import SideBarLink from './side-bar-link.svelte';
import MoreInformationAssets from '$lib/components/shared-components/side-bar/more-information-assets.svelte';
import MoreInformationAlbums from '$lib/components/shared-components/side-bar/more-information-albums.svelte';
import { t } from 'svelte-i18n';
import BottomInfo from '$lib/components/shared-components/side-bar/bottom-info.svelte';
let isArchiveSelected: boolean;
let isFavoritesSelected: boolean;
@@ -136,8 +136,5 @@
{/if}
</nav>
<!-- Status Box -->
<div class="mb-6 mt-auto">
<StatusBox />
</div>
<BottomInfo />
</SideBarSection>

View File

@@ -0,0 +1,82 @@
<script lang="ts">
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
import { locale } from '$lib/stores/preferences.store';
import { serverInfo } from '$lib/stores/server-info.store';
import { user } from '$lib/stores/user.store';
import { requestServerInfo } from '$lib/utils/auth';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { getByteUnitString } from '../../../utils/byte-units';
import LoadingSpinner from '../loading-spinner.svelte';
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
let usageClasses = '';
let isOpen = false;
$: hasQuota = $user?.quotaSizeInBytes !== null;
$: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
$: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
let aboutInfo: ServerAboutResponseDto;
const onUpdate = () => {
usageClasses = getUsageClass();
};
const getUsageClass = () => {
if (usedPercentage >= 95) {
return 'bg-red-500';
}
if (usedPercentage > 80) {
return 'bg-yellow-500';
}
return 'bg-immich-primary dark:bg-immich-dark-primary';
};
$: $user && onUpdate();
onMount(async () => {
await requestServerInfo();
aboutInfo = await getAboutInfo();
});
</script>
{#if isOpen}
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
{/if}
<div
class="hidden md:block storage-status p-4 bg-gray-100 dark:bg-immich-dark-primary/10 ml-4 rounded-lg text-sm"
title={$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale, 3),
available: getByteUnitString(availableBytes, $locale, 3),
},
})}
>
<div class="hidden group-hover:sm:block md:block">
<p class="font-medium text-immich-dark-gray dark:text-white mb-2">{$t('storage')}</p>
{#if $serverInfo}
<p class="text-gray-500 dark:text-gray-300">
{$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale),
available: getByteUnitString(availableBytes, $locale),
},
})}
</p>
<div class="mt-4 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
</div>
{:else}
<div class="mt-2">
<LoadingSpinner />
</div>
{/if}
</div>
</div>

View File

@@ -1,125 +0,0 @@
<script lang="ts">
import Icon from '$lib/components/elements/icon.svelte';
import ServerAboutModal from '$lib/components/shared-components/server-about-modal.svelte';
import { locale } from '$lib/stores/preferences.store';
import { serverInfo } from '$lib/stores/server-info.store';
import { user } from '$lib/stores/user.store';
import { websocketStore } from '$lib/stores/websocket';
import { requestServerInfo } from '$lib/utils/auth';
import { mdiChartPie, mdiDns } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { getByteUnitString } from '../../utils/byte-units';
import LoadingSpinner from './loading-spinner.svelte';
import { getAboutInfo, type ServerAboutResponseDto } from '@immich/sdk';
const { serverVersion, connected } = websocketStore;
let usageClasses = '';
let isOpen = false;
$: version = $serverVersion ? `v${$serverVersion.major}.${$serverVersion.minor}.${$serverVersion.patch}` : null;
$: hasQuota = $user?.quotaSizeInBytes !== null;
$: availableBytes = (hasQuota ? $user?.quotaSizeInBytes : $serverInfo?.diskSizeRaw) || 0;
$: usedBytes = (hasQuota ? $user?.quotaUsageInBytes : $serverInfo?.diskUseRaw) || 0;
$: usedPercentage = Math.round((usedBytes / availableBytes) * 100);
let aboutInfo: ServerAboutResponseDto;
const onUpdate = () => {
usageClasses = getUsageClass();
};
const getUsageClass = () => {
if (usedPercentage >= 95) {
return 'bg-red-500';
}
if (usedPercentage > 80) {
return 'bg-yellow-500';
}
return 'bg-immich-primary dark:bg-immich-dark-primary';
};
$: $user && onUpdate();
onMount(async () => {
await requestServerInfo();
aboutInfo = await getAboutInfo();
});
</script>
{#if isOpen}
<ServerAboutModal onClose={() => (isOpen = false)} info={aboutInfo} />
{/if}
<div class="dark:text-immich-dark-fg">
<div
class="storage-status grid grid-cols-[64px_auto]"
title={$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale, 3),
available: getByteUnitString(availableBytes, $locale, 3),
},
})}
>
<div class="pb-[2.15rem] pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
<Icon path={mdiChartPie} size="24" />
</div>
<div class="hidden group-hover:sm:block md:block">
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('storage')}</p>
{#if $serverInfo}
<div class="my-2 h-[7px] w-full rounded-full bg-gray-200 dark:bg-gray-700">
<div class="h-[7px] rounded-full {usageClasses}" style="width: {usedPercentage}%" />
</div>
<p class="text-xs">
{$t('storage_usage', {
values: {
used: getByteUnitString(usedBytes, $locale),
available: getByteUnitString(availableBytes, $locale),
},
})}
</p>
{:else}
<div class="mt-2">
<LoadingSpinner />
</div>
{/if}
</div>
</div>
<div>
<hr class="my-4 ml-5 dark:border-immich-dark-gray" />
</div>
<div class="server-status grid grid-cols-[64px_auto]">
<div class="pb-11 pl-5 pr-6 text-immich-primary dark:text-immich-dark-primary group-hover:sm:pb-0 md:pb-0">
<Icon path={mdiDns} size="26" />
</div>
<div class="hidden text-xs group-hover:sm:block md:block">
<p class="text-sm font-medium text-immich-primary dark:text-immich-dark-primary">{$t('server')}</p>
<div class="mt-2 flex justify-between justify-items-center">
<p>{$t('status')}</p>
{#if $connected}
<p class="font-medium text-immich-primary dark:text-immich-dark-primary">{$t('online')}</p>
{:else}
<p class="font-medium text-red-500">{$t('offline')}</p>
{/if}
</div>
<div class="mt-2 flex justify-between justify-items-center">
<p>{$t('version')}</p>
{#if $connected && version}
<button
type="button"
on:click={() => (isOpen = true)}
class="font-medium text-immich-primary dark:text-immich-dark-primary">{version}</button
>
{:else}
<p class="font-medium text-red-500">{$t('unknown')}</p>
{/if}
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,160 @@
<script lang="ts">
import { fade } from 'svelte/transition';
import { onMount } from 'svelte';
import { licenseStore } from '$lib/stores/license.store';
import { user } from '$lib/stores/user.store';
import {
deleteServerLicense,
deleteUserLicense,
getAboutInfo,
getMyUser,
getServerLicense,
type LicenseResponseDto,
} from '@immich/sdk';
import Icon from '$lib/components/elements/icon.svelte';
import { mdiLicense } from '@mdi/js';
import Button from '$lib/components/elements/buttons/button.svelte';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { handleError } from '$lib/utils/handle-error';
import LicenseContent from '$lib/components/shared-components/license/license-content.svelte';
import { t } from 'svelte-i18n';
import { getAccountAge } from '$lib/utils/auth';
const { isLicenseActivated } = licenseStore;
let isServerLicense = false;
let serverLicenseInfo: LicenseResponseDto | null = null;
const accountAge = getAccountAge();
const checkLicenseInfo = async () => {
const serverInfo = await getAboutInfo();
isServerLicense = serverInfo.licensed;
const userInfo = await getMyUser();
if (userInfo.license) {
$user = { ...$user, license: userInfo.license };
}
if (isServerLicense && $user.isAdmin) {
serverLicenseInfo = (await getServerLicense()) as LicenseResponseDto | null;
}
};
onMount(async () => {
if (!$isLicenseActivated) {
return;
}
await checkLicenseInfo();
});
const removeUserLicense = async () => {
try {
const isConfirmed = await dialogController.show({
title: 'Remove License',
prompt: 'Are you sure you want to remove the license?',
confirmText: 'Remove',
cancelText: 'Cancel',
});
if (!isConfirmed) {
return;
}
await deleteUserLicense();
licenseStore.setLicenseStatus(false);
} catch (error) {
handleError(error, 'Failed to remove license');
}
};
const removeServerLicense = async () => {
try {
const isConfirmed = await dialogController.show({
title: 'Remove License',
prompt: 'Are you sure you want to remove the Server license?',
confirmText: 'Remove',
cancelText: 'Cancel',
});
if (!isConfirmed) {
return;
}
await deleteServerLicense();
licenseStore.setLicenseStatus(false);
} catch (error) {
handleError(error, 'Failed to remove license');
}
};
const onLicenseActivated = async () => {
licenseStore.setLicenseStatus(true);
await checkLicenseInfo();
};
</script>
<section class="my-4">
<div in:fade={{ duration: 500 }}>
{#if $isLicenseActivated}
{#if isServerLicense}
<div
class="bg-gray-50 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
>
<Icon path={mdiLicense} size="56" class="text-immich-primary dark:text-immich-dark-primary" />
<div>
<p class="text-immich-primary dark:text-immich-dark-primary font-semibold text-lg">Server License</p>
{#if $user.isAdmin && serverLicenseInfo?.activatedAt}
<p class="dark:text-white text-sm mt-1 col-start-2">
Activated on {new Date(serverLicenseInfo?.activatedAt).toLocaleDateString()}
</p>
{:else}
<p class="dark:text-white">Your license is managed by the admin</p>
{/if}
</div>
</div>
{#if $user.isAdmin}
<div class="text-right mt-4">
<Button size="sm" color="red" on:click={removeServerLicense}>Remove license</Button>
</div>
{/if}
{:else}
<div
class="bg-gray-50 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-6 pr-12 rounded-xl flex place-content-center gap-4"
>
<Icon path={mdiLicense} size="56" class="text-immich-primary dark:text-immich-dark-primary" />
<div>
<p class="text-immich-primary dark:text-immich-dark-primary font-semibold text-lg">Individual License</p>
{#if $user.license?.activatedAt}
<p class="dark:text-white text-sm mt-1 col-start-2">
Activated on {new Date($user.license?.activatedAt).toLocaleDateString()}
</p>
{/if}
</div>
</div>
<div class="text-right mt-4">
<Button size="sm" color="red" on:click={removeUserLicense}>Remove license</Button>
</div>
{/if}
{:else}
{#if accountAge > 14}
<div
class="text-center bg-gray-100 border border-immich-dark-primary/50 dark:bg-immich-dark-primary/15 p-4 rounded-xl"
>
<p class="text-immich-dark-gray/80 dark:text-immich-gray text-balance">
{$t('license_trial_info_2')}
<span class="text-immich-primary dark:text-immich-dark-primary font-semibold">
{$t('license_trial_info_3', { values: { accountAge } })}</span
>. {$t('license_trial_info_4')}
</p>
</div>
{/if}
<LicenseContent onActivate={onLicenseActivated} />
{/if}
</div>
</section>

View File

@@ -18,6 +18,7 @@
import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte';
import { t } from 'svelte-i18n';
import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte';
import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte';
export let keys: ApiKeyResponseDto[] = [];
export let sessions: SessionResponseDto[] = [];
@@ -52,6 +53,14 @@
<DownloadSettings />
</SettingAccordion>
<SettingAccordion
key="user-license-settings"
title={$t('user_license_settings')}
subtitle={$t('user_license_settings_description')}
>
<LicenseSettings />
</SettingAccordion>
<SettingAccordion key="memories" title={$t('memories')} subtitle={$t('memories_setting_description')}>
<MemoriesSettings />
</SettingAccordion>

View File

@@ -34,6 +34,7 @@ export enum AppRoute {
MEMORY = '/memory',
TRASH = '/trash',
PARTNERS = '/partners',
BUY = '/buy',
AUTH_LOGIN = '/auth/login',
AUTH_REGISTER = '/auth/register',
@@ -309,3 +310,8 @@ export const langs = [
},
{ name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) },
];
export enum ImmichLicense {
Client = 'immich-client',
Server = 'immich-server',
}

View File

@@ -3,7 +3,7 @@
"account": "الحساب",
"account_settings": "إعدادات الحساب",
"acknowledge": "أُدرك ذلك",
"action": "العمل",
"action": "التحكم",
"actions": "العمليات",
"active": "نشط",
"activity": "النشاط",
@@ -48,7 +48,7 @@
"exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند مسح مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
"external_library_created_at": "مكتبة خارجيه (تم انشاؤها في {date})",
"external_library_management": "إدارة المكتبة الخارجية",
"face_detection": "التعرف على الوجه",
"face_detection": "إكتشاف الوجوه",
"face_detection_description": "اكتشاف الوجوه في الملفات باستخدام التعلم الآلي. بالنسبة للفيديوهات، يتم النظر فقط في الصورة المصغرة. خيار \"الكل\" يعيد معالجة جميع الأصول. خيار \"مفقود\" يضع في قائمة الانتظار الأصول التي لم يتم معالجتها بعد. سيتم وضع الوجوه المكتشفة في قائمة انتظار التعرف على الوجه بعد اكتمال اكتشاف الوجه، مما يجمعها في أشخاص موجودين أو جدد.",
"facial_recognition_job_description": "تجميع الوجوه المكتشفة في أشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"الكل\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
"failed_job_command": "فشل الأمر {command} للمهمة: {job}",
@@ -127,16 +127,18 @@
"manage_log_settings": "إدارة إعدادات السجلات",
"map_dark_style": "النمط الداكن",
"map_enable_description": "تمكين ميزات الخرائط",
"map_gps_settings": "إعدادات الخريطة ونظام تحديد المواقع",
"map_gps_settings_description": "إدارة إعدادات الخريطة و نظام تحديد المواقع (عكس الترميز الجغرافي)",
"map_light_style": "النمط الفاتح",
"map_manage_reverse_geocoding_settings": "إدارة إعدادات <link>التكوين الجغرافي المعكوس</link>",
"map_reverse_geocoding": "عكس الترميز الجغرافي",
"map_reverse_geocoding_enable_description": "تمكين عكس الترميز الجغرافي",
"map_reverse_geocoding_settings": "إعدادات عكس الترميز الجغرافي",
"map_settings": "إعدادات الخريطة ونظام تحديد المواقع",
"map_settings": "إعدادات الخريطة",
"map_settings_description": "إدارة إعدادات الخريطة",
"map_style_description": "عنوان URL لسمة الخريطة style.json",
"metadata_extraction_job": "استخراج البيانات الوصفية",
"metadata_extraction_job_description": "استخراج معلومات البيانات الوصفية من كل أصل، مثل إحداثات الموقع والدقة",
"metadata_extraction_job_description": "استخراج معلومات البيانات الوصفية من كل أصل، مثل إحداثيات الموقع والدقة",
"migration_job": "ترحيل",
"migration_job_description": "ترحيل الصور المصغرة للأصول والوجوه إلى أحدث هيكل مجلدات",
"no_paths_added": "لم يتم إضافة أي مسارات",
@@ -172,6 +174,8 @@
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما يكون 'app.immich:/' هو عنوان URI لإعادة التوجيه غير الصالح.",
"oauth_profile_signing_algorithm": "خوارزمية توقيع الملف الشخصي",
"oauth_profile_signing_algorithm_description": "الخوارزمية المستخدمة للتوقيع على ملف تعريف المستخدم.",
"oauth_scope": "النطاق",
"oauth_settings": "OAuth",
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
@@ -344,9 +348,9 @@
"album_user_left": "تم ترك {album}",
"album_user_removed": "تم إزالة {user}",
"album_with_link_access": "السماح لأي شخص لديه الرابط برؤية الصور والأشخاص الموجودين في هذا الألبوم.",
"albums": "ألبومات",
"albums": "الألبومات",
"albums_count": "{count, plural, one {{count, number} ألبوم} other {{count, number} ألبومات}}",
"all": "الجميع",
"all": "الكل",
"all_albums": "جميع الألبومات",
"all_people": "جميع الأشخاص",
"all_videos": "جميع الفيديوهات",
@@ -382,7 +386,7 @@
"assets": "أصول",
"assets_added_count": "تمت إضافة {count, plural, one {# أصل} other {# أصول}}",
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# أصل} other {# أصول}} إلى الألبوم",
"assets_added_to_name_count": "تمت إضافة {count, plural, one {# أصل} other {# أصول}} إلى {name}",
"assets_added_to_name_count": "تم إضافة {count, plural, one {# أصل } other {# أصول }} إلى {hasName, select, true {<b>{name}</b>} other {البوم جديد}}",
"assets_count": "{count, plural, one {# أصل} other {# أصول}}",
"assets_moved_to_trash_count": "تم نقل {count, plural, one {# أصل} other {# أصول}} إلى سلة المهملات",
"assets_permanently_deleted_count": "تم حذف {count, plural, one {# أصل} other {# أصول}} بشكل دائم",
@@ -498,7 +502,7 @@
"direction": "الإتجاه",
"disabled": "معطل",
"disallow_edits": "منع التعديلات",
"discover": "يكتشف",
"discover": "إكتشاف",
"dismiss_all_errors": "تجاهل كافة الأخطاء",
"dismiss_error": "تجاهل الخطأ",
"display_options": "عرض الخيارات",
@@ -541,7 +545,7 @@
"edit_user": "تعديل المستخدم",
"edited": "تم تحريره",
"editor": "",
"email": "بريد إلكتروني",
"email": "البريد إلكتروني",
"empty": "",
"empty_album": "",
"empty_trash": "افرغ سله المهملات",
@@ -686,7 +690,7 @@
"expire_after": "تنتهي بعد",
"expired": "منتهي الصلاحية",
"expires_date": "تنتهي الصلاحية في {date}",
"explore": "يستكشف",
"explore": "استكشاف",
"export": "تصدير",
"export_as_json": "تصدير كـ JSON",
"extension": "الإمتداد",
@@ -720,7 +724,7 @@
"group_no": "بدون تجميع",
"group_owner": "تجميع حسب المالك",
"group_year": "تجميع حسب السنة",
"has_quota": يده حصة",
"has_quota": "له حصة",
"hi_user": "مرحبا {name} ({email})",
"hide_all_people": "إخفاء جميع الأشخاص",
"hide_gallery": "اخفاء المعرض",
@@ -815,7 +819,7 @@
"merged_people_count": "دمج {count, plural, one {شخص واحد} other {# أشخاص}}",
"minimize": "تصغير",
"minute": "دقيقة",
"missing": "الناقصة",
"missing": "المفقودة",
"model": "نموذج",
"month": "شهر",
"more": "المزيد",
@@ -900,7 +904,7 @@
"pause_memories": "إيقاف الذكريات مؤقتا",
"paused": "تم الإيقاف مؤقتًا",
"pending": "قيد الانتظار",
"people": "الناس",
"people": "الأشخاص",
"people_edits_count": "تم تعديل {count, plural, one {# شخص } other {# أشخاص }}",
"people_sidebar_description": "عرض رابط للأشخاص في الشريط الجانبي",
"perform_library_tasks": "",
@@ -920,7 +924,7 @@
"photos_from_previous_years": "صور من السنوات السابقة",
"pick_a_location": "اختر موقعًا",
"place": "مكان",
"places": "أماكن",
"places": "الأماكن",
"play": "تشغيل",
"play_memories": "تشغيل الذكريات",
"play_motion_photo": "تشغيل الصور المتحركة",
@@ -1127,7 +1131,7 @@
"toggle_theme": "تبديل السمة",
"toggle_visibility": "تبديل الرؤية",
"total_usage": "الاستخدام الإجمالي",
"trash": "نفايات",
"trash": "المهملات",
"trash_all": "نقل الكل إلى سلة المهملات",
"trash_count": "المهملات {count}",
"trash_delete_asset": "نقل إلى سهلة المهملات / حذف الأصل",
@@ -1156,7 +1160,7 @@
"up_next": "التالي",
"updated_password": "كلمة المرور المحدثة",
"upload": "رفع وتحميل",
"upload_concurrency": "تزامن الرفع",
"upload_concurrency": "رفع",
"upload_errors": "إكتمل الرفع مع {count, plural, one {# خطأ } other {# أخطاء }}, قم بتحديث الصفحة لرؤية الأصول الجديدة التي تم رفعها.",
"upload_progress": "متبقية {remaining} - معالجة {processed}/{total}",
"upload_skipped_duplicates": "تم تخطي {count, plural, one {# أصل مكرر} other {# أصول مكررة }}",
@@ -1183,7 +1187,7 @@
"video": "شريط فيديو",
"video_hover_setting": "تشغيل الصورة المصغرة للفيديو عند التمرير",
"video_hover_setting_description": "تشغيل الصورة المصغرة للفيديو عند تحريك الماوس فوق العنصر. حتى عند التعطيل، يمكن بدء التشغيل عن طريق التمرير فوق رمز التشغيل.",
"videos": "أشرطة فيديو",
"videos": "الفيديو",
"videos_count": "{count, plural, one {# مقطع فيديو } other {# مقاطع الفيديو }}",
"view": "عرض",
"view_album": "عرض الألبوم",

View File

@@ -47,7 +47,7 @@
"external_library_created_at": "Външна библиотека (създадена на {date})",
"external_library_management": "Управление на външните библиотеки",
"face_detection": "Откриване на лица",
"face_detection_description": "Разпознаване на лица в ресурси чрез машинно обучение. За видеата се използва само миниатюрата. \"Всички\" обработва отново всички ресурси. \"Липсващи\" зарежда за обработка расурси, които на се обработени все още. Откритите лица ще бъдат подредени в опашка за разпознаване на лица след завършване на функцията за откриване на лица, като се групират в съществуващи или нови хора.",
"face_detection_description": "Да се разпознават лица в ресурси чрез машинно обучение. За видеата се използва само миниатюрата. \"Всички\" обработва отново всички ресурси. \"Липсващи\" зарежда за обработка расурси, които на се обработени все още. Откритите лица ще бъдат подредени в опашка за разпознаване на лица след завършване на функцията за откриване на лица, като се групират в съществуващи или нови хора.",
"facial_recognition_job_description": "Групирай откритите лица в хора. Тази стъпка се извършва след завършване на функцията за откриване на лица. \"Всички\" пре-/групира всички лица. \"Липсващи\" подрежда в опашка за разпознаване всички лица, за които няма назначен човек.",
"failed_job_command": "Командата {command} е неуспешна за задача: {job}",
"force_delete_user_warning": "ВНИМАНИЕ: Това веднага ще изтрие потребителя и всичките му ресурси. Действието е необратимо и файловете не могат да бъдат възстановени.",
@@ -67,77 +67,80 @@
"image_thumbnail_format": "Формат на миниатюрните изображения",
"image_thumbnail_resolution": "Резолюция на миниатюрните изображения",
"image_thumbnail_resolution_description": "Използва се при разглеждане на групи от снимки (основна времева линия, изглед на албум и др.). По-високите резолюции могат да запазят повече детайли, но отнемат повече време за кодиране, имат по-големи размери на файловете и могат да намалят отзивчивостта на приложението.",
"job_concurrency": "",
"job_concurrency": "Паралелност на {job}",
"job_not_concurrency_safe": "Тази задача не е безопасна за паралелно изпълнение.",
"job_settings": "Настройки за задачите",
"job_settings_description": "Управление за паралелност на задачите",
"job_settings_description": "Управление на паралелността на задачите",
"job_status": "Статус на задачата",
"jobs_delayed": "",
"jobs_failed": "",
"jobs_delayed": "{jobCount, plural, other {# delayed}}",
"jobs_failed": "{jobCount, plural, other {# failed}}",
"library_created": "Създадена библиотека: {library}",
"library_cron_expression": "",
"library_cron_expression_description": "",
"library_cron_expression_presets": "",
"library_deleted": "Библиотека изтрита",
"library_import_path_description": "Посочете папка за импортиране. Тази папка, вкл. подпапките, ще бъдат сканирани за изображения и видеоклипове.",
"library_cron_expression": "Cron израз",
"library_cron_expression_description": "Задайте интервала за сканиране чрез cron интервал. За повече информация, вижте например <link>Crontab Guru</link>",
"library_cron_expression_presets": "Предварителни настройки на Cron израза",
"library_deleted": "Библиотека е изтрита",
"library_import_path_description": "Посочете папка за импортиране. Тази папка, включително подпапките, ще бъдат сканирани за изображения и видеоклипове.",
"library_scanning": "Периодично сканиране",
"library_scanning_description": "Конфигурация за периодично сканиране на библиотеката",
"library_scanning_description": "Конфигурирай периодично сканиране на библиотеката",
"library_scanning_enable_description": "Включване на периодичното сканиране на библиотеката",
"library_settings": "Външна библиотека",
"library_settings_description": "Управление настройките за външна библиотека",
"library_settings_description": "Управление на настройките за външна библиотека",
"library_tasks_description": "Извършване на задачи за библиотеката",
"library_watching_enable_description": "Наблюдаване за промяна на файловете във външаната библиотека",
"library_watching_enable_description": "Наблюдаване за промяна на файловете във външната библиотека",
"library_watching_settings": "Наблюдаване на библиотеката (ЕКСПЕРИМЕНТАЛНО)",
"library_watching_settings_description": "Автоматично наблюдавай за променени файлове",
"logging_enable_description": "Включване на запис/логове",
"logging_enable_description": "Включване на запис (логове)",
"logging_level_description": "Когато е включено, какво ниво на записване да се използва.",
"logging_settings": "Записване",
"machine_learning_clip_model": "",
"machine_learning_clip_model_description": "",
"machine_learning_clip_model": "CLIP модел",
"machine_learning_clip_model_description": "Името на CLIP модела, посочен <link>тук</link>. Имайте предвид, че при промяна на модела трябва да стартирате отново задачата \"Интелигентно Търсене\" за всички изображения.",
"machine_learning_duplicate_detection": "Откриване на дубликати",
"machine_learning_duplicate_detection_enabled": "Включване откриването на дубликати",
"machine_learning_duplicate_detection_enabled_description": "Напълно идентични файлове ще бъдат дедупликирани дори, когато е изключено.",
"machine_learning_duplicate_detection_setting_description": "",
"machine_learning_duplicate_detection_enabled": "Включване на откриването на дубликати",
"machine_learning_duplicate_detection_enabled_description": "Напълно идентични ресурси ще бъдат дедублирани дори когато е изключено.",
"machine_learning_duplicate_detection_setting_description": "Използване на CLIP вграждания за откриване на вероятни дублирания",
"machine_learning_enabled": "Включване на машинното обучение",
"machine_learning_enabled_description": "",
"machine_learning_enabled_description": "Ако е изключено, всички функции на машинно обучение ще бъдат деактивирани, независимо от посочените по-долу настройки.",
"machine_learning_facial_recognition": "Лицево разпознаване",
"machine_learning_facial_recognition_description": "Откриване, разпознаване и групиране на лица в изображенията",
"machine_learning_facial_recognition_model": "Модел за лицево разпознаване",
"machine_learning_facial_recognition_model_description": "Моделите са изброени в низходящ ред по размер. По-големите модели са по-бавни и използват повече памет, но дават по-добри резултати. Имайте предвид, че при промяна на модела, трябва да стартирате отново задачата за откриване на лица за всички изображения.",
"machine_learning_facial_recognition_setting": "Включване на лицевото разпознаване",
"machine_learning_facial_recognition_setting_description": "Когато е изключено, изображенията няма да бъдат кодирани за лицево разпознаване и секцията \"Хора\" няма да бъде попълнена.",
"machine_learning_facial_recognition_setting": "Включване на лицево разпознаване",
"machine_learning_facial_recognition_setting_description": "Когато е изключено, изображенията няма да бъдат кодирани за лицево разпознаване и секцията \"Хора\", в страницата \"Разгледай\", няма да бъде попълнена.",
"machine_learning_max_detection_distance": "Максимално разстояние за откриване",
"machine_learning_max_detection_distance_description": "",
"machine_learning_max_detection_distance_description": "Максимална разстояние между две изображения, за да се считат за дубликати, в диапазона 0,001-0,1. По-високите стойности ще открият повече дубликати, но могат да доведат до фалшиви положителни резултати.",
"machine_learning_max_recognition_distance": "Максимално разстояние за разпознаване",
"machine_learning_max_recognition_distance_description": "",
"machine_learning_min_detection_score": "",
"machine_learning_max_recognition_distance_description": "Максимално разстояние между две лица, за да се считат за едно и също лице, в диапазона 0-2. Намаляването му може да предотврати определянето на две лица като едно и също лице, а увеличаването му може да предотврати определянето на едно и също лице като две различни лица. Имайте предвид, че е по-лесно да се слеят две лица, отколкото да се раздели едно лице на две, така че по възможност изберете по-ниска стойност.",
"machine_learning_min_detection_score": "Минимална оценка за откриване",
"machine_learning_min_detection_score_description": "Минимална оценка на доверието, за да бъде считано лице като открито - от 0 до 1. По-ниските стойности ще открият повече лица, но може да доведат до фалшиви положителни резултати.",
"machine_learning_min_recognized_faces": "Минимум разпознати лица",
"machine_learning_min_recognized_faces_description": "",
"machine_learning_settings": "",
"machine_learning_settings_description": "",
"machine_learning_min_recognized_faces_description": "Минималният брой разпознати лица, необходими за създаването на лице. Увеличаването му прави разпознаването на лица по-прецизно за сметка на увеличаването на вероятността дадено лице да не бъде причислено към лице.",
"machine_learning_settings": "Настройки на машинното обучение",
"machine_learning_settings_description": "Управление на функциите и настройките за машинно обучение",
"machine_learning_smart_search": "Интелигентно Търсене",
"machine_learning_smart_search_description": "",
"machine_learning_smart_search_enabled": "",
"machine_learning_smart_search_enabled_description": "",
"machine_learning_url_description": "",
"machine_learning_smart_search_description": "Семантично търсене на изображения с помощта на CLIP вграждания",
"machine_learning_smart_search_enabled": "Включване на Интелигентно Търсене",
"machine_learning_smart_search_enabled_description": "Ако е деактивирано, изображенията няма да бъдат кодирани за Интелигентно Търсене.",
"machine_learning_url_description": "URL адрес на сървъра за машинно обучение",
"manage_concurrency": "Управление на паралелност",
"manage_log_settings": "",
"map_dark_style": "Тъмен Стил",
"map_enable_description": "",
"manage_log_settings": "Управление на настройките на записване",
"map_dark_style": "Тъмен стил",
"map_enable_description": "Активиране на картата",
"map_gps_settings": "Настройки на картата и GPS",
"map_gps_settings_description": "Управление на настройките на картата и GPS (обратно геокодиране)",
"map_light_style": "Светъл стил",
"map_reverse_geocoding": "",
"map_reverse_geocoding_enable_description": "",
"map_reverse_geocoding_settings": "",
"map_settings": "",
"map_settings_description": "",
"map_style_description": "",
"map_manage_reverse_geocoding_settings": "Управление на настройките за <link>обратно геокодиране</link>",
"map_reverse_geocoding": "Обратно геокодиране",
"map_reverse_geocoding_enable_description": "Включване на обратно геокодиране",
"map_reverse_geocoding_settings": "Настройки на опбратно геокодиране",
"map_settings": "Настройки на картата",
"map_settings_description": "Управление на настройките на картата",
"map_style_description": "URL адрес към файл \"style.json\" за задаване на стил на картата",
"metadata_extraction_job": "Извличане на метаданни",
"metadata_extraction_job_description": "",
"metadata_extraction_job_description": "Извличане на метаданни от всеки ресурс, като GPS и резолюция",
"migration_job": "Миграция",
"migration_job_description": "",
"no_paths_added": "",
"no_pattern_added": "",
"migration_job_description": "Мигриране на миниатюрите за ресурси и лица към най-новата структура на папките",
"no_paths_added": "Няма добавени пътища",
"no_pattern_added": "Няма добавен модел",
"note_apply_storage_label_previous_assets": "",
"note_cannot_be_changed_later": "",
"note_unlimited_quota": "",
@@ -207,7 +210,7 @@
"sidecar_job": "",
"sidecar_job_description": "",
"slideshow_duration_description": "",
"smart_search_job_description": "",
"smart_search_job_description": "Извършване на машинно обучение върху ресурси за подпомагане на Интелигентното Търсене",
"storage_template_enable_description": "",
"storage_template_hash_verification_enabled": "",
"storage_template_hash_verification_enabled_description": "",
@@ -550,7 +553,7 @@
"expand_all": "",
"expire_after": "",
"expired": "Изтекло",
"explore": "",
"explore": "Разгледай",
"export": "Експорт",
"export_as_json": "",
"extension": "Разширение",
@@ -676,7 +679,7 @@
"no_assets_message": "",
"no_duplicates_found": "",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_explore_results_message": "Качете още снимки, за да разгледате колекцията си.",
"no_favorites_message": "",
"no_libraries_message": "",
"no_name": "",

View File

@@ -5,11 +5,11 @@
"acknowledge": "Reconeix",
"action": "Acció",
"actions": "Accions",
"active": "Activar",
"active": "Actiu",
"activity": "Activitat",
"activity_changed": "L'activitat està {enabled, select, true {enabled} other {disabled}}",
"add": "Agregar",
"add_a_description": "Afegeix una descripció",
"add_a_description": "Afegir una descripció",
"add_a_location": "Afegeix una ubicació",
"add_a_name": "Afegeix un nom",
"add_a_title": "Afegeix un títol",
@@ -17,27 +17,27 @@
"add_import_path": "Afegeix un camí d'importació",
"add_location": "Afegeix la ubicació",
"add_more_users": "Afegeix més usuaris",
"add_partner": "Afegeix company",
"add_partner": "Afegir company/a",
"add_path": "Afegeix un camí",
"add_photos": "Afegeix fotografies",
"add_to": "Afegeix a...",
"add_to_album": "Afegeix a l'àlbum",
"add_to_shared_album": "Afegeix a l'àlbum compartit",
"added_to_archive": "Afegit a l'arxivat",
"added_to_favorites": "Afegit a preferits",
"added_to_favorites_count": "{count} afegits a preferits",
"added_to_favorites": "Afegit als preferits",
"added_to_favorites_count": "{count} afegits als preferits",
"admin": {
"add_exclusion_pattern_description": "Afegeix patrons d'eclusió. És permès de l'ús de *, **, i ? (globbing). Per a ignorar els fitxers de qualsevol directori anomenat \"Raw\" introduïu \"**/Raw/**\". Per a ignorar els fitxers acabats en \".tif\" introduïu \"**/*.tif\". Per a ignorar un camí absolut, utilitzeu \"/camí/a/ignorar/**\".",
"authentication_settings": "Arrenjaments d'autenticació",
"authentication_settings_description": "Gestiona la contrasenya, OAuth, i altres arrenjaments d'autenticació",
"authentication_settings": "Configuració de l'autenticació",
"authentication_settings_description": "Gestiona la contrasenya, OAuth i altres configuracions de l'autenticació",
"authentication_settings_disable_all": "Estàs segur que vols desactivar tots els mètodes d'inici de sessió? L'inici de sessió quedarà completament desactivat.",
"authentication_settings_reenable": "Per a tornar a habilitar, empra una <link>Comanda de Servidor</link>.",
"background_task_job": "Tasques en segon pla",
"check_all": "Marca-ho tot",
"cleared_jobs": "Treballs esborrats per a: {job}",
"cleared_jobs": "Tasques esborrades per a: {job}",
"config_set_by_file": "La configuració està definida per un fitxer de configuració",
"confirm_delete_library": "Esteu segur que voleu eliminar la biblioteca {library}?",
"confirm_delete_library_assets": "Estàs segur que vols esborrar aquesta biblioteca? Això esborrarà {count, plural, one {# contained asset} other {all # contained assets}} d'Immich i no es podrà desfer. Els fitxers romandran al disc.",
"confirm_delete_library": "Esteu segurs que voleu eliminar la llibreria {library}?",
"confirm_delete_library_assets": "Esteu segurs que voleu esborrar aquesta llibreria? Això esborrarà {count, plural, one {# contained asset} other {all # contained assets}} d'Immich i no es podrà desfer. Els fitxers romandran al disc.",
"confirm_email_below": "Per a confirmar, escriviu \"{email}\" a sota",
"confirm_reprocess_all_faces": "Esteu segur que voleu reprocessar totes les cares? Això també esborrarà la gent que heu anomenat.",
"confirm_user_password_reset": "Esteu segur que voleu reinicialitzar la contrasenya de l'usuari {user}?",
@@ -45,9 +45,9 @@
"disable_login": "Deshabiliteu l'inici de sessió",
"disabled": "Deshabilitat",
"duplicate_detection_job_description": "Executa l'aprenentatge automàtic en els elements per a detectar imatges semblants. Fa servir l'Smart Search",
"exclusion_pattern_description": "Els patrons d'exclusió et permeten ignorar fitxers i carpetes quan escaneges una biblioteca. Això és útil si tens carpetes que contenen fitxer que no vols importar, com els fitxers RAW.",
"external_library_created_at": "Biblioteca externa (creada el {date})",
"external_library_management": "Gestió de la biblioteca externa",
"exclusion_pattern_description": "Els patrons d'exclusió permeten ignorar fitxers i carpetes quan escanegeu una biblioteca. Això és útil si teniu carpetes que contenen fitxer que no vols importar, com els fitxers RAW.",
"external_library_created_at": "Llibreria externa (creada el {date})",
"external_library_management": "Gestió de llibreries externes",
"face_detection": "Detecció de cares",
"face_detection_description": "Detecta les cares fent servir aprenentatge automàtic. Per a videos només és té en compte la miniatura. \"Tot\" reprocessa tots els elements. \"Pendent\" encua els elements que encar no han estat processats. Les cares detectades s'encuaran per al Reconeixement Facial després de completar la Detecció Facial, tot agrupant-les entre noves persones o les ja existents.",
"facial_recognition_job_description": "Agrupa les cares detectades per persona. Aquest pas s'executa després de completar la Detecció Facial. \"Tot\" reagrupa totes les cares. \"Pendent\" encua les cares que no tenen cap persona assignada.",
@@ -64,14 +64,14 @@
"image_preview_resolution_description": "S'empra al visualitzar una única fotografia i per a l'Aprenentatge Automàtic. L'alta resolució por preservar més detalls però es triga més a codificar, té fitxers més pesats i pot reduir la resposta de l'aplicació.",
"image_quality": "Qualitat",
"image_quality_description": "Qualitat d'imatge de 1 a 100. Un valor més alt millora la qualitat però genera fitxers més pesats.",
"image_settings": "Arranjaments d'imatge",
"image_settings": "Configuració d'imatges",
"image_settings_description": "Gestiona la qualitat i resolució de les imatges generades",
"image_thumbnail_format": "Format de la miniatura",
"image_thumbnail_resolution": "Resolució de la miniatura",
"image_thumbnail_resolution_description": "S'empra per a veure grups de fotos (cronologia, vista d'àlbum, etc.). L'alta resolució pot preservar més detalls però triguen més en codificar-se, tenen fitxers més pesats i poden reduir la reactivitat de l'aplicació.",
"job_concurrency": "{job} concurrència",
"job_not_concurrency_safe": "Aquesta tasca no és segura per a la conconcurrència.",
"job_settings": "Arranjaments de les tasques",
"job_settings": "Configuració de les tasques",
"job_settings_description": "Gestiona la concurrència de tasques",
"job_status": "Estat de la tasca",
"jobs_delayed": "{jobCount, plural, other {# posposades}}",
@@ -85,16 +85,16 @@
"library_scanning": "Escaneig periòdic",
"library_scanning_description": "Configurar l'escaneig periòdic de bilbioteques",
"library_scanning_enable_description": "Habilita l'escaneig periòdic de biblioteques",
"library_settings": "Bilbioteca externa",
"library_settings_description": "Gestiona la configuració de la biblioteca externa",
"library_tasks_description": "Realitzar tasques de la bilbioteca",
"library_watching_enable_description": "Consultar biblioteques externes per detectar canvis en fitxers",
"library_settings": "Bilbioteques externes",
"library_settings_description": "Gestiona la configuració de les llibreries externes",
"library_tasks_description": "Realitza tasques de la bilbioteca",
"library_watching_enable_description": "Consultar llibreries externes per detectar canvis en fitxers",
"library_watching_settings": "Monitoratge de bilbioteca (EXPERIMENTAL)",
"library_watching_settings_description": "Monitorització automàtica de fitxers modificats",
"logging_enable_description": "Habilita el logging",
"logging_level_description": "Quan està habilitat, quin nivell de registre emprar.",
"logging_enable_description": "Habilitar el registrament",
"logging_level_description": "Quan està habilitat, quin nivell de registre es vol emprar.",
"logging_settings": "Registre",
"machine_learning_clip_model": "Model de CLIP",
"machine_learning_clip_model": "Model CLIP",
"machine_learning_clip_model_description": "El nom d'un model CLIP que apareix a <link>aquí</link>. Tingues en compte que has de tornar a executar l'Smart Search' per a totes les imatges quan es canvia de model.",
"machine_learning_duplicate_detection": "Detecció de duplicats",
"machine_learning_duplicate_detection_enabled": "Activa detecció de duplicats",
@@ -124,24 +124,25 @@
"machine_learning_smart_search_enabled_description": "Si està deshabilitat les imatges no es codificaran per la cerca intel·ligent.",
"machine_learning_url_description": "URL del servidor d'aprenentatge automàtic",
"manage_concurrency": "Gestiona la concurrència",
"manage_log_settings": "Gestiona la configuració de log",
"manage_log_settings": "Gestiona la configuració del registre",
"map_dark_style": "Tema fosc",
"map_enable_description": "Habilita característiques del mapa",
"map_gps_settings": "Configuració de mapa i GPS",
"map_light_style": "Tema clar",
"map_manage_reverse_geocoding_settings": "Gestiona els paràmetres de <link>geocodificació inversa</link>",
"map_reverse_geocoding": "Geocodificació inversa",
"map_reverse_geocoding_enable_description": "Habilita la geocodificació inversa",
"map_reverse_geocoding_settings": "Configuració de Geocodificació Inversa",
"map_settings": "Configuració de Mapa i GPS",
"map_settings": "Configuració del mapa i GPS",
"map_settings_description": "Gestiona la configuració del mapa",
"map_style_description": "URL a un tema del mapa style.json",
"metadata_extraction_job": "Extreu metadades",
"metadata_extraction_job": "Extreure metadades",
"metadata_extraction_job_description": "Extreu l'informació de metadades de cada element, com per exemple el GPS i la resolució",
"migration_job": "Migració",
"migration_job_description": "Migra les miniatures d'elements i cares cap a la nova estructura de carpetes",
"no_paths_added": "Cap camí afegit",
"no_pattern_added": "Cap patró aplicat",
"note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiqueta d'emmagatzematge a elements pujats prèviament, executa la",
"note_apply_storage_label_previous_assets": "Nota: Per aplicar l'etiquetatge d'emmagatzematge a elements pujats prèviament, executa la",
"note_cannot_be_changed_later": "NOTA: Això és irreversible!",
"note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada",
"notification_email_from_address": "Des de l'adreça",
@@ -172,19 +173,20 @@
"oauth_mobile_redirect_uri": "URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override": "Sobreescriu l'URI de redirecció mòbil",
"oauth_mobile_redirect_uri_override_description": "Habilita quan 'app.immich:/' és una URI de redirecció invàlida.",
"oauth_profile_signing_algorithm_description": "Algoritme utilitzat per signar el perfil dusuari.",
"oauth_scope": "Abast",
"oauth_settings": "OAuth",
"oauth_settings_description": "Gestiona la configuració de l'inici de sessió OAuth",
"oauth_settings_more_details": "Per a més detalls sobre aquesta funcionalitat, consulteu la <link>documentació</link>.",
"oauth_signing_algorithm": "Algorisme de signatura",
"oauth_storage_label_claim": "Petició d'etiqueta d'emmagatzematge",
"oauth_storage_label_claim_description": "Estableix automàticament l'etiqueta d'emmagatzematge de l'usuari a aquest valor.",
"oauth_storage_label_claim": "Petició d'etiquetatge d'emmagatzematge",
"oauth_storage_label_claim_description": "Estableix automàticament l'etiquetatge d'emmagatzematge de l'usuari a aquest valor.",
"oauth_storage_quota_claim": "Quota d'emmagatzematge reclamada",
"oauth_storage_quota_claim_description": "Estableix automàticament la quota d'emmagatzematge de l'usuari al valor d'aquest paràmetre.",
"oauth_storage_quota_default": "Quota d'emmagatzematge predeterminada (GiB)",
"oauth_storage_quota_default_description": "Quota disponible en GB quan no s'estableixi cap valor (Entreu 0 per a quota il·limitada).",
"offline_paths": "Rutes sense connexió",
"offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una biblioteca externa.",
"offline_paths_description": "Aquests resultats poden ser deguts a l'eliminació manual de fitxers que no formen part d'una llibreria externa.",
"password_enable_description": "Inicia sessió amb correu electrònic i contrasenya",
"password_settings": "Inici de sessió amb contrasenya",
"password_settings_description": "Gestiona la configuració de l'inici de sessió amb contrasenya",
@@ -199,61 +201,63 @@
"repaired_items": "Corregit {count, plural, one {# item} other {# items}}",
"require_password_change_on_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
"reset_settings_to_default": "Restablir configuracions per defecte",
"reset_settings_to_recent_saved": "Restaurar la configuració guardada més recent",
"scanning_library_for_changed_files": "Escanejant biblioteca per trobar fitxers modificats",
"scanning_library_for_new_files": "Escanejant biblioteca per trobar fitxers nous",
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
"scanning_library_for_changed_files": "Escanejant llibreria per trobar fitxers modificats",
"scanning_library_for_new_files": "Escanejant llibreria per trobar fitxers nous",
"send_welcome_email": "Enviar correu electrònic de benvinguda",
"server_external_domain_settings": "Domini extern",
"server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
"server_settings": "Configuració del servidor",
"server_settings_description": "Gestionar la configuració del servidor",
"server_settings_description": "Gestiona la configuració del servidor",
"server_welcome_message": "Missatge de benvinguda",
"server_welcome_message_description": "Missatge que es mostra a la pàgina d'inici de sessió.",
"sidecar_job": "Metadades auxiliars",
"sidecar_job_description": "Descobrir o sincronitzar metadades auxiliars des del sistema de fitxers",
"sidecar_job_description": "Descobreix o sincronitza metadades auxiliars des del sistema de fitxers",
"slideshow_duration_description": "Segons per mostrar cada imatge",
"smart_search_job_description": "Executar aprenentatge automàtic sobre els recursos per donar suport a la cerca intel·ligent",
"smart_search_job_description": "Executa aprenentatge automàtic sobre els elements per donar suport a la cerca intel·ligent",
"storage_template_date_time_description": "La data de creació del recurs s'utilitza per a la informació de la data i l'hora",
"storage_template_date_time_sample": "Temps d'exemple: {date}",
"storage_template_enable_description": "Habilitar el motor de plantilles d'emmagatzematge",
"storage_template_hash_verification_enabled": "Verificació Hash habilitada",
"storage_template_hash_verification_enabled_description": "Activa la verificació de hash. No la desactivis a menys que estiguis segur de les implicacions",
"storage_template_hash_verification_enabled": "Verificació de hash habilitada",
"storage_template_hash_verification_enabled_description": "Activa la verificació de hash. No la desactiveu a menys que estigueu segurs de les implicacions",
"storage_template_migration": "Migració de plantilles d'emmagatzematge",
"storage_template_migration_description": "Aplica la <link>{template}</link> actual als elements pujats prèviament",
"storage_template_migration_info": "Els canvis de plantilla només s'aplicaran a nous elements. Per a aplicar la plantilla rectroactivament a elements pujats prèviament executeu la <link>{job}</link>.",
"storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzemament",
"storage_template_settings": "",
"storage_template_migration_info": "Els canvis de plantilla només s'aplicaran a nous elements. Per aplicar la plantilla rectroactivament a elements pujats prèviament, executeu la <link>{job}</link>.",
"storage_template_migration_job": "Tasca de migració de la plantilla d'emmagatzematge",
"storage_template_settings": "Plantilla d'emmagatzematge",
"storage_template_settings_description": "",
"theme_custom_css_settings": "",
"system_settings": "Configuració del sistema",
"theme_custom_css_settings": "CSS personalitzat",
"theme_custom_css_settings_description": "",
"theme_settings": "",
"theme_settings_description": "",
"thumbnail_generation_job_description": "",
"theme_settings": "Configuració del tema",
"theme_settings_description": "Gestiona la personalització de la interfície web Immich",
"thumbnail_generation_job": "Generar miniatures",
"thumbnail_generation_job_description": "Genera miniatures grans, petites i borroses per a cada element, així com miniatures per a cada persona",
"transcode_policy_description": "",
"transcoding_acceleration_api": "",
"transcoding_acceleration_api": "API d'acceleració",
"transcoding_acceleration_api_description": "",
"transcoding_acceleration_nvenc": "",
"transcoding_acceleration_qsv": "",
"transcoding_acceleration_rkmpp": "",
"transcoding_acceleration_nvenc": "NVENC (requereix GPU d'NVIDIA)",
"transcoding_acceleration_qsv": "Quick Sync (requereix GPU d'Intel de 7a generació o posterior)",
"transcoding_acceleration_rkmpp": "RKMPP (requereix SoC de Rockchip)",
"transcoding_acceleration_vaapi": "VAAPI",
"transcoding_accepted_audio_codecs": "",
"transcoding_accepted_audio_codecs": "Còdecs d'àudio acceptats",
"transcoding_accepted_audio_codecs_description": "",
"transcoding_accepted_video_codecs": "",
"transcoding_accepted_video_codecs": "Còdecs de vídeo acceptats",
"transcoding_accepted_video_codecs_description": "",
"transcoding_advanced_options_description": "",
"transcoding_audio_codec": "",
"transcoding_audio_codec": "Còdec d'àudio",
"transcoding_audio_codec_description": "",
"transcoding_bitrate_description": "",
"transcoding_constant_quality_mode": "",
"transcoding_constant_quality_mode": "Mode de qualitat constant",
"transcoding_constant_quality_mode_description": "",
"transcoding_constant_rate_factor": "",
"transcoding_constant_rate_factor_description": "",
"transcoding_disabled_description": "",
"transcoding_hardware_acceleration": "",
"transcoding_hardware_acceleration": "Acceleració de maquinari",
"transcoding_hardware_acceleration_description": "",
"transcoding_hardware_decoding": "",
"transcoding_hardware_decoding": "Descodificació de maquinari",
"transcoding_hardware_decoding_setting_description": "",
"transcoding_hevc_codec": "",
"transcoding_hevc_codec": "Còdec HEVC",
"transcoding_max_b_frames": "",
"transcoding_max_b_frames_description": "",
"transcoding_max_bitrate": "",
@@ -261,14 +265,14 @@
"transcoding_max_keyframe_interval": "",
"transcoding_max_keyframe_interval_description": "",
"transcoding_optimal_description": "",
"transcoding_preferred_hardware_device": "",
"transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit",
"transcoding_preferred_hardware_device_description": "",
"transcoding_preset_preset": "",
"transcoding_preset_preset_description": "",
"transcoding_reference_frames": "",
"transcoding_reference_frames_description": "",
"transcoding_required_description": "",
"transcoding_settings": "",
"transcoding_settings": "Configuració de transcodificació de vídeo",
"transcoding_settings_description": "",
"transcoding_target_resolution": "",
"transcoding_target_resolution_description": "",
@@ -283,17 +287,21 @@
"transcoding_transcode_policy": "",
"transcoding_two_pass_encoding": "",
"transcoding_two_pass_encoding_setting_description": "",
"transcoding_video_codec": "",
"transcoding_video_codec": "Còdec de vídeo",
"transcoding_video_codec_description": "",
"trash_enabled_description": "",
"trash_number_of_days": "",
"trash_number_of_days": "Nombre de dies",
"trash_number_of_days_description": "",
"trash_settings": "",
"trash_settings_description": "",
"trash_settings": "Configuració de la paperera",
"trash_settings_description": "Gestiona la configuració de la paperera",
"user_delete_delay_settings": "",
"user_delete_delay_settings_description": "",
"user_settings": "",
"user_settings_description": "",
"user_management": "Gestió d'usuaris",
"user_password_has_been_reset": "La contrasenya de l'usuari ha estat restablida:",
"user_restore_description": "Es restaurarà el compte <b>{user}</b> .",
"user_settings": "Configuració d'usuaris",
"user_settings_description": "Gestiona la configuració dels usuaris",
"user_successfully_removed": "L'usuari {email} s'ha eliminat correctament.",
"version_check_enabled_description": "",
"version_check_settings": "",
"version_check_settings_description": "",
@@ -303,6 +311,9 @@
"admin_password": "Contrasenya de l'administrador",
"administration": "Administrador",
"advanced": "Avançat",
"age_months": "{months, plural, one {# mes} other {# mesos}}",
"age_year_months": "Un any i {months, plural, one {# mes} other {# mesos}}",
"age_years": "{years, plural, one {# any} other {# anys}}",
"album_added": "Àlbum afegit",
"album_added_notification_setting_description": "Rep una notificació per correu quan siguis afegit a un àlbum compartit",
"album_cover_updated": "Portada de l'àlbum actualitzada",
@@ -312,13 +323,14 @@
"album_leave_confirmation": "N'esteu segur que voleu sortir de {album}?",
"album_name": "Nom de l'àlbum",
"album_options": "Opcions de l'àlbum",
"album_remove_user": "Suprimir l'usuari?",
"album_remove_user_confirmation": "N'esteu segur que voleu suprimir {user}?",
"album_remove_user": "Eliminar l'usuari?",
"album_remove_user_confirmation": "Esteu segurs que voleu eliminar {user}?",
"album_updated": "Àlbum actualitzat",
"album_updated_setting_description": "",
"album_user_left": "Surt de {album}",
"album_user_removed": "{user} suprimit",
"album_user_removed": "{user} eliminat",
"albums": "Àlbums",
"albums_count": "{count, plural, one {{count, number} àlbum} other {{count, number} àlbums}}",
"all": "Tots",
"all_albums": "Tots els àlbum",
"all_people": "Tota la gent",
@@ -331,12 +343,33 @@
"appears_in": "Apareix a",
"archive": "Arxiu",
"archive_or_unarchive_photo": "Arxivar o desarxivar fotografia",
"archive_size": "Mida de l'arxiu",
"archive_size_description": "Configureu la mida de l'arxiu de les descàrregues (en GiB)",
"archived": "Arxivat",
"asset_offline": "",
"are_these_the_same_person": "Són la mateixa persona?",
"are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
"asset_added_to_album": "Afegit a l'àlbum",
"asset_adding_to_album": "Afegint a l'àlbum...",
"asset_filename_is_offline": "L'element {filename} està fora de línia",
"asset_has_unassigned_faces": "L'element té cares no assignades",
"asset_offline": "Element fora de línia",
"asset_offline_description": "Aquest element està fora de línia. L'Immich no pot accedir a la seva ubicació. Si us plau, assegureu-vos que l'actiu està disponible i després torneu la llibreria.",
"assets": "Elements",
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}",
"assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
"assets_count": "{count, plural, one {Un element} other {# elements}}",
"assets_moved_to_trash_count": "{count, plural, one {Un element mogut} other {# elements moguts}} a la paperera",
"assets_permanently_deleted_count": "{count, plural, one {Un element esborrat} other {# elements esborrats}} permanentment",
"assets_removed_count": "{count, plural, one {Un element eliminat} other {# elements eliminats}}",
"assets_restore_confirmation": "Esteu segurs que voleu restaurar tots els teus actius? Aquesta acció no es pot desfer!",
"assets_restored_count": "{count, plural, one {Un element restaurat} other {# elements restaurats}}",
"assets_trashed_count": "{count, plural, one {Un element enviat} other {# elements enviats}} a la paperera",
"assets_were_part_of_album_count": "{count, plural, one {L'element ja és} other {Els elements ja són}} part de l'àlbum",
"authorized_devices": "Dispositius autoritzats",
"back": "Enrere",
"backward": "Enrere",
"birthdate_saved": "Data de naixement guardada amb èxit",
"birthdate_set_description": "La data de naixement s'utilitza per calcular l'edat d'aquesta persona en el moment d'una foto.",
"blurred_background": "Fons difuminat",
"camera": "Càmera",
"camera_brand": "Marca de la càmera",
@@ -344,6 +377,7 @@
"cancel": "Cancel·la",
"cancel_search": "Cancel·la la cerca",
"cannot_merge_people": "No es pot fusionar gent",
"cannot_undo_this_action": "Aquesta acció no es pot desfer!",
"cannot_update_the_description": "No es pot actualitzar la descripció",
"cant_apply_changes": "No es poden aplicar els canvis",
"cant_get_faces": "No es poden obtenir les cares",
@@ -366,11 +400,13 @@
"close": "Tanca",
"collapse_all": "Redueix-ho tot",
"color_theme": "",
"comment_deleted": "Comentari esborrat",
"comment_options": "Opcions de comentari",
"comments_are_disabled": "",
"comments_and_likes": "Comentaris i agradaments",
"comments_are_disabled": "Els comentaris estan desactivats",
"confirm": "Confirma",
"confirm_admin_password": "Confirmeu la contrasenya d'administrador",
"confirm_delete_shared_link": "N'esteu segur que voleu suprimir aquest enllaç compartit?",
"confirm_delete_shared_link": "Esteu segurs que voleu eliminar aquest enllaç compartit?",
"confirm_password": "Confirma la contrasenya",
"contain": "",
"context": "",
@@ -383,34 +419,39 @@
"copy_link": "Còpia l'enllaç",
"copy_link_to_clipboard": "Còpia l'enllaç al porta-retalls",
"copy_password": "Còpia la contrasenya",
"copy_to_clipboard": "Còpia al porta-retalls",
"copy_to_clipboard": "Copiar al porta-retalls",
"country": "País",
"cover": "",
"covers": "",
"create": "Crea",
"create_album": "Crea un àlbum",
"create_library": "Crea una biblioteca",
"create_album": "Crear un àlbum",
"create_library": "Crea una llibreria",
"create_link": "Crear enllaç",
"create_link_to_share": "Crear enllaç per compartir",
"create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades",
"create_new_person": "Crea una nova persona",
"create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
"create_new_user": "Crea un usuari nou",
"create_user": "Crea un usuari",
"created": "Creat",
"current_device": "Dispositiu actual",
"custom_locale": "",
"custom_locale_description": "",
"custom_locale_description": "Format de dates i números segons la llengua i regió",
"dark": "Fosc",
"date_after": "",
"date_and_time": "Datum a čas",
"date_before": "",
"date_range": "Rozsah data",
"date_after": "Data posterior a",
"date_and_time": "Data i hora",
"date_before": "Data anterior a",
"date_of_birth_saved": "Data de naixement guardada amb èxit",
"date_range": "Interval de dates",
"day": "Dia",
"default_locale": "",
"default_locale_description": "",
"default_locale_description": "Format de dates i números segons la configuració del navegador",
"delete": "Esborra",
"delete_album": "Esborra l'àlbum",
"delete_api_key_prompt": "Esteu segurs que voleu eliminar aquesta clau API?",
"delete_duplicates_confirmation": "Esteu segurs que voleu eliminar aquests duplicats permanentment?",
"delete_key": "",
"delete_library": "Suprimeix biblioteca",
"delete_library": "Suprimeix la llibreria",
"delete_link": "Esborra l'enllaç",
"delete_shared_link": "Odstranit sdílený odkaz",
"delete_user": "Suprimeix l'usuari",
@@ -431,6 +472,7 @@
"download": "Baixa",
"download_settings": "Baixa",
"downloading": "Baixant",
"downloading_asset_filename": "Descarregant l'element {filename}",
"duplicates": "Duplicats",
"duration": "Duració",
"durations": {
@@ -440,6 +482,7 @@
"months": "",
"years": ""
},
"edit": "Editar",
"edit_album": "Edita l'àlbum",
"edit_avatar": "Edita l'avatar",
"edit_date": "Edita la data",
@@ -461,30 +504,61 @@
"empty": "",
"empty_album": "",
"empty_trash": "Buida la paperera",
"enable": "",
"enabled": "",
"enable": "Activar",
"enabled": "Activat",
"end_date": "Data de fi",
"error": "Error",
"error_loading_image": "Error carregant la imatge",
"error_title": "Error - Quelcom ha anat malament",
"errors": {
"cannot_navigate_next_asset": "No es pot navegar a l'element següent",
"cannot_navigate_previous_asset": "No es pot navegar a l'element anterior",
"cant_apply_changes": "No es poden aplicar els canvis",
"cant_change_activity": "No es pot {enabled, select, true {desactivar} other {activar}} aquesta activitat",
"cant_change_metadata_assets_count": "No es poden canviar les metadades {count, plural, one {de l'element} other {dels # elements}}",
"cant_get_faces": "No es poden obtenir les cares",
"cant_get_number_of_comments": "No es pot obtenir el nombre de comentaris",
"cant_search_people": "No es poden cercar persones",
"cant_search_places": "No es poden cercar llocs",
"error_adding_assets_to_album": "Error afegint elements a l'àlbum",
"error_adding_users_to_album": "Error afegint usuaris a l'àlbum",
"error_downloading": "Error descarregant {filename}",
"error_removing_assets_from_album": "Error eliminant els elements de l'àlbum, consulteu la consola per obtenir més detalls",
"error_selecting_all_assets": "Error seleccionant tots els elements",
"exclusion_pattern_already_exists": "Aquest patró dexclusió ja existeix.",
"failed_to_create_album": "No s'ha pogut crear l'àlbum",
"failed_to_create_shared_link": "No s'ha pogut crear l'enllaç compartit",
"failed_to_edit_shared_link": "No s'ha pogut editar l'enllaç compartit",
"failed_to_load_asset": "No s'ha pogut carregar l'element",
"failed_to_load_assets": "No s'han pogut carregar els elements",
"failed_to_stack_assets": "No s'han pogut apilar els elements",
"failed_to_unstack_assets": "No s'han pogut desapilar els elements",
"import_path_already_exists": "Aquest camí d'importació ja existeix.",
"incorrect_email_or_password": "Correu electrònic o contrasenya incorrectes",
"profile_picture_transparent_pixels": "Les fotos de perfil no poden tenir píxels transparents. Per favor, feu zoom in, mogueu la imatge o ambdues.",
"quota_higher_than_disk_size": "Heu establert una quota més gran que la mida de disc",
"unable_to_add_album_users": "No es poden afegir usuaris a l'àlbum",
"unable_to_add_assets_to_shared_link": "No s'han pogut afegir els elements a l'enllaç compartit",
"unable_to_add_comment": "No es pot afegir el comentari",
"unable_to_add_partners": "",
"unable_to_add_exclusion_pattern": "No s'ha pogut afegir el patró dexclusió",
"unable_to_add_import_path": "No s'ha pogut afegir el camí d'importació",
"unable_to_add_partners": "No es poden afegir companys",
"unable_to_add_remove_archive": "No s'ha pogut {archived, select, true {eliminar l'element de} other {afegir l'element a}} l'arxiu",
"unable_to_add_remove_favorites": "No s'ha pogut {favorite, select, true {afegir l'element als} other {eliminar l'element dels}} preferits",
"unable_to_change_album_user_role": "",
"unable_to_change_date": "No es pot canviar la data",
"unable_to_change_location": "No es pot canviar la ubicació",
"unable_to_check_item": "",
"unable_to_check_items": "",
"unable_to_connect_to_server": "No es pot connectar al servidor",
"unable_to_create_admin_account": "",
"unable_to_create_library": "No es pot crear la biblioteca",
"unable_to_create_library": "No es pot crear la llibreria",
"unable_to_create_user": "No es pot crear l'usuari",
"unable_to_delete_album": "No es pot suprimir l'àlbum",
"unable_to_delete_album": "No es pot eliminar l'àlbum",
"unable_to_delete_asset": "",
"unable_to_delete_user": "No es pot suprimir l'usuari",
"unable_to_delete_user": "No es pot eliminar l'usuari",
"unable_to_empty_trash": "No es pot buidar la paperera",
"unable_to_enter_fullscreen": "No es pot entrar al mode de pantalla completa",
"unable_to_enter_fullscreen": "No es pot entrar a la pantalla completa",
"unable_to_exit_fullscreen": "No es pot sortir de la pantalla completa",
"unable_to_hide_person": "No es pot amagar la persona",
"unable_to_load_album": "No es pot carregar l'àlbum",
@@ -496,7 +570,7 @@
"unable_to_remove_album_users": "",
"unable_to_remove_comment": "",
"unable_to_remove_library": "",
"unable_to_remove_partner": "",
"unable_to_remove_partner": "No es pot eliminar company/a",
"unable_to_remove_reaction": "",
"unable_to_remove_user": "",
"unable_to_repair_items": "",
@@ -526,15 +600,16 @@
"every_six_hours": "",
"exit_slideshow": "",
"expand_all": "",
"expire_after": "Caduca després",
"expire_after": "Caduca després de",
"expired": "Caducat",
"explore": "Explora",
"export_as_json": "Exportar com a JSON",
"extension": "Extensió",
"external_libraries": "",
"external_libraries": "Llibreries externes",
"failed_to_get_people": "",
"favorite": "Preferit",
"favorite_or_unfavorite_photo": "",
"favorites": "Preferides",
"favorites": "Preferits",
"feature": "",
"feature_photo_updated": "",
"featurecollection": "",
@@ -554,7 +629,7 @@
"go_to_search": "",
"go_to_share_page": "",
"group_albums_by": "",
"has_quota": "",
"has_quota": "Té quota",
"hide_gallery": "",
"hide_password": "",
"hide_person": "",
@@ -563,11 +638,12 @@
"image": "Imatge",
"img": "",
"immich_logo": "",
"import_from_json": "Importar des de JSON",
"import_path": "",
"in_archive": "",
"include_archived": "Incloure arxivat",
"include_shared_albums": "",
"include_shared_partner_assets": "",
"include_shared_partner_assets": "Incloure elements dels companys",
"individual_share": "",
"info": "",
"interval": {
@@ -579,7 +655,7 @@
"invite_people": "",
"invite_to_album": "Convida a l'àlbum",
"job_settings_description": "",
"jobs": "",
"jobs": "Tasques",
"keep": "Mantenir",
"keyboard_shortcuts": "",
"language": "Idioma",
@@ -603,9 +679,9 @@
"look": "",
"loop_videos": "",
"loop_videos_description": "Habilita la reproducció en bucle del vídeo en els detalls.",
"make": "Fer",
"make": "Fabricant",
"manage_shared_links": "Spravovat sdílené odkazy",
"manage_sharing_with_partners": "",
"manage_sharing_with_partners": "Gestiona la compartició amb els companys",
"manage_the_app_settings": "",
"manage_your_account": "",
"manage_your_api_keys": "",
@@ -614,7 +690,7 @@
"map": "Mapa",
"map_marker_with_image": "",
"map_settings": "Paràmetres de mapa",
"media_type": "",
"media_type": "Tipus de mitjà",
"memories": "Records",
"memories_setting_description": "",
"menu": "Menú",
@@ -623,7 +699,7 @@
"merge_people_successfully": "",
"minimize": "Minimitza",
"minute": "Minut",
"missing": "",
"missing": "Restants",
"model": "",
"month": "Mes",
"more": "Més",
@@ -634,24 +710,26 @@
"never": "Mai",
"new_api_key": "",
"new_password": "Nova contrasenya",
"new_person": "",
"new_user_created": "",
"new_person": "Persona nova",
"new_user_created": "Nou usuari creat",
"newest_first": "",
"next": "Següent",
"next_memory": "",
"no": "No",
"no_albums_message": "",
"no_archived_assets_message": "",
"no_albums_message": "Creeu un àlbum per organitzar les vostres fotos i vídeos",
"no_archived_assets_message": "Arxiveu fotos i vídeos per ocultar-los de Fotos",
"no_assets_message": "",
"no_duplicates_found": "No s'han trobat duplicats.",
"no_exif_info_available": "",
"no_explore_results_message": "",
"no_favorites_message": "",
"no_favorites_message": "Afegiu favorits per trobar les millors fotos i vídeos a l'instant",
"no_libraries_message": "",
"no_name": "",
"no_places": "",
"no_results": "",
"no_shared_albums_message": "",
"no_shared_albums_message": "Creeu un àlbum per compartir fotos i vídeos amb persones a la vostra xarxa",
"not_in_any_album": "",
"note_unlimited_quota": "Nota: Intruduïu 0 per a quota il·limitada",
"notes": "",
"notification_toggle_setting_description": "",
"notifications": "Notificacions",
@@ -661,29 +739,30 @@
"ok": "D'acord",
"oldest_first": "",
"online": "En línia",
"only_favorites": "",
"only_favorites": "Només preferits",
"only_refreshes_modified_files": "",
"open_the_search_filters": "",
"options": "Opcions",
"organize_your_library": "",
"organize_your_library": "Organitzeu la llibreria",
"other": "",
"other_devices": "",
"other_variables": "",
"other_variables": "Altres variables",
"owned": "Propi",
"owner": "Propietari",
"partner": "Company/a",
"partner_can_access": "{partner} hi té accés",
"partner_can_access_assets": "Totes les teves imatges i vídeos excepte les Arxivades i Eliminades",
"partner_can_access_assets": "Totes les vostres fotos i vídeos excepte les arxivades i eliminades",
"partner_can_access_location": "Ubicació en què s'han fet les fotos",
"partner_sharing": "Compartició amb companys",
"partners": "Companys",
"password": "Contrasenya",
"password_does_not_match": "",
"password_required": "",
"password_does_not_match": "La contrasenya no coincideix",
"password_required": "Contrasenya requerida",
"password_reset_success": "",
"past_durations": {
"days": "",
"days": "{days, plural, one {El dia anterior} other {Els # dies anteriors}}",
"hours": "",
"years": ""
"years": "{years, plural, one {L'any passat} other {Els passats # anys}}"
},
"path": "",
"pattern": "Patró",
@@ -692,15 +771,22 @@
"paused": "En pausa",
"pending": "Pendent",
"people": "Persones",
"people_sidebar_description": "",
"people_edits_count": "{count, plural, one {Una persona editada} other {# persones editades}}",
"people_sidebar_description": "Mostrar un enllaç a Persones a la barra lateral",
"perform_library_tasks": "",
"permanent_deletion_warning": "",
"permanent_deletion_warning_setting_description": "",
"permanently_delete": "",
"permanently_deleted_asset": "",
"permanent_deletion_warning": "Avís d'eliminació permanent",
"permanent_deletion_warning_setting_description": "Mostrar un avís quan s'eliminin els elements permanentment",
"permanently_delete": "Eliminar permanentment",
"permanently_delete_assets_count": "Eliminar permanentment {count, plural, one {l'element} other {els elements}}",
"permanently_deleted_asset": "Element eliminat permanentment",
"permanently_deleted_assets_count": "{count, plural, one {S'ha eliminat un element} other {S'han eliminat # elements}} permanentment",
"person": "Persona",
"person_hidden": "{name}{hidden, select, true { (ocultat)} other {}}",
"photos": "Fotos",
"photos_from_previous_years": "",
"pick_a_location": "",
"photos_and_videos": "Fotos i vídeos",
"photos_count": "{count, plural, one {{count, number} foto} other {{count, number} fotos}}",
"photos_from_previous_years": "Fotos d'anys anteriors",
"pick_a_location": "Triar una ubicació",
"place": "Lloc",
"places": "Localitzacions",
"play": "Reprodueix",
@@ -708,77 +794,100 @@
"play_motion_photo": "",
"play_or_pause_video": "",
"point": "",
"port": "",
"port": "Port",
"preset": "",
"preview": "",
"preview": "Previsualització",
"previous": "Anterior",
"previous_memory": "",
"previous_or_next_photo": "",
"primary": "",
"profile_picture_set": "",
"public_album": "Àlbum públic",
"public_share": "",
"range": "",
"raw": "",
"reaction_options": "",
"read_changelog": "",
"reassing_hint": "Assignar els elements seleccionats a una persona existent",
"recent": "Recent",
"recent_searches": "",
"refresh": "",
"refreshed": "",
"recent_searches": "Cerques recents",
"refresh": "Actualitza",
"refresh_metadata": "Actualitza les metadades",
"refresh_thumbnails": "Actualitza la miniatura",
"refreshed": "Actualitzat",
"refreshes_every_file": "",
"remove": "",
"refreshing_metadata": "Actualitzant les metadades",
"regenerating_thumbnails": "Regenerant les miniatures",
"remove": "Eliminar",
"remove_assets_title": "Eliminar els elements?",
"remove_from_album": "Treu de l'àlbum",
"remove_from_favorites": "",
"remove_from_shared_link": "",
"remove_offline_files": "",
"repair": "",
"remove_from_favorites": "Eliminar dels preferits",
"remove_from_shared_link": "Eliminar de l'enllaç compartit",
"remove_offline_files": "Suprimeix fitxers fora de línia",
"remove_user": "Eliminar l'usuari",
"removed_api_key": "Eliminada la clau d'API: {name}",
"removed_from_archive": "Eliminat de l'arxiu",
"removed_from_favorites": "Eliminat dels preferits",
"removed_from_favorites_count": "{count, plural {S'han eliminat # elements}, other {S'ha eliminat un element}} dels preferits",
"repair": "Reparació",
"repair_no_results_message": "",
"replace_with_upload": "",
"replace_with_upload": "Substitueix amb pujada",
"repository": "Repositori",
"require_password": "",
"reset": "",
"reset_password": "",
"reset_people_visibility": "",
"require_user_to_change_password_on_first_login": "Requerir que l'usuari canviï la contrasenya en el primer inici de sessió",
"reset": "Restablir",
"reset_password": "Restablir contrasenya",
"reset_people_visibility": "Restablir la visibilitat de les persones",
"reset_settings_to_default": "",
"resolved_all_duplicates": "Tots els duplicats resolts",
"restore": "Recupera",
"restore_user": "",
"restore_all": "Restaurar-ho tot",
"restore_user": "Restaurar l'usuari",
"restored_asset": "Element restaurat",
"resume": "Reprendre",
"retry_upload": "",
"review_duplicates": "",
"review_duplicates": "Revisar duplicats",
"role": "Rol",
"save": "Desa",
"saved_profile": "",
"saved_settings": "",
"saved_api_key": "Clau d'API guardada",
"saved_profile": "Perfil guardat",
"saved_settings": "Configuració guardada",
"say_something": "Digues quelcom",
"scan_all_libraries": "",
"scan_all_library_files": "",
"scan_new_library_files": "",
"scan_settings": "",
"scan_all_libraries": "Escanejar totes les llibreries",
"scan_all_library_files": "Re-escanejar tots els fitxers de la llibreria",
"scan_new_library_files": "Escanejar nous fitxers de la llibreria",
"scan_settings": "Configuració d'escaneig",
"search": "Cerca",
"search_albums": "",
"search_by_context": "",
"search_camera_make": "",
"search_camera_model": "",
"search_city": "",
"search_country": "",
"search_albums": "Buscar àlbums",
"search_by_context": "Buscar per context",
"search_camera_make": "Buscar per fabricant de càmara...",
"search_camera_model": "Buscar per model de càmera...",
"search_city": "Buscar per ciutat...",
"search_country": "Buscar per país...",
"search_for_existing_person": "",
"search_people": "",
"search_places": "",
"search_state": "",
"search_timezone": "",
"search_type": "",
"search_no_people": "Cap persona",
"search_people": "Buscar persones",
"search_places": "Buscar llocs",
"search_state": "Buscar per regió...",
"search_timezone": "Buscar per fus horari...",
"search_type": "Buscar per tipus",
"search_your_photos": "Cerca les teves fotos",
"searching_locales": "",
"second": "Segon",
"select_album_cover": "",
"see_all_people": "Veure totes les persones",
"select_album_cover": "Seleccionar la portada de l'àlbum",
"select_all": "Selecciona-ho tot",
"select_avatar_color": "Tria color de l'avatar",
"select_face": "Selecciona cara",
"select_featured_photo": "Selecciona foto principal",
"select_from_computer": "Seleccionar des de l'ordinador",
"select_keep_all": "Mantén tota la selecció",
"select_library_owner": "Selecciona el propietari de la bilbioteca",
"select_new_face": "Selecciona nova cara",
"select_photos": "Tria fotografies",
"select_trash_all": "Envia la selecció a la paperera",
"selected": "Seleccionat",
"selected_count": "{count, plural {# seleccionats}, other {Un seleccionat}}",
"send_message": "Envia missatge",
"send_welcome_email": "Envia correu de benvinguda",
"server": "Servidor",
@@ -794,15 +903,18 @@
"settings_saved": "Configuració desada",
"share": "Comparteix",
"shared": "Compartit",
"shared_by": "Compartir per",
"shared_by": "Compartit per",
"shared_by_user": "Compartit per {user}",
"shared_by_you": "Compartit per tu",
"shared_from_partner": "Fotos de {partner}",
"shared_links": "Enllaços compartits",
"shared_photos_and_videos_count": "{assetCount} fotos i vídeos compartits.",
"shared_photos_and_videos_count": "{assetCount, plural, other {# fotos i vídeos compartits.}}",
"shared_with_partner": "Compartit amb {partner}",
"sharing": "Compartint",
"sharing_sidebar_description": "Mostra un enllaç a Compartint a la barra lateral",
"sharing": "Compartit",
"sharing_enter_password": "Introduïu la contrasenya per veure aquesta pàgina.",
"sharing_sidebar_description": "Mostra un enllaç a Compartit a la barra lateral",
"show_album_options": "Mostra les opcions d'àlbum",
"show_all_people": "Veure totes les persones",
"show_and_hide_people": "Mostra i amaga persones",
"show_file_location": "Mostra l'ubicació del fitxer",
"show_gallery": "Mostra la galeria",
@@ -840,10 +952,10 @@
"status": "Estat",
"stop_motion_photo": "Atura foto en moviment",
"stop_photo_sharing": "Deixar de compartir les teves fotos?",
"stop_photo_sharing_description": "{partner} no podrà tornar a accedir a les teves fotos.",
"stop_photo_sharing_description": "{partner} no podrà tornar a accedir a les vostres fotos.",
"stop_sharing_photos_with_user": "Deixa de compartir les fotos amb aquest usuari",
"storage": "Emmagatzematge",
"storage_label": "Etiqueta d'emmagatzematge",
"storage_label": "Etiquetatge d'emmagatzematge",
"storage_usage": "{used} de {available} en ús",
"submit": "Envia",
"suggestions": "Suggeriments",
@@ -854,10 +966,14 @@
"theme": "Tema",
"theme_selection": "Selecció de tema",
"theme_selection_description": "Activa automàticament el tema fosc o clar en funció de les preferències del sistema del navegador",
"they_will_be_merged_together": "Es combinaran",
"time_based_memories": "Records basats en el temps",
"timezone": "Fus horari",
"to_archive": "Arxiva",
"to_change_password": "Canviar la contrasenya",
"to_favorite": "Prefereix",
"to_login": "Iniciar sessió",
"to_trash": "Paperera",
"toggle_settings": "Canvia configuració",
"toggle_theme": "Canvia tema",
"toggle_visibility": "Canvia visibilitat",
@@ -870,13 +986,14 @@
"type": "Tipus",
"unarchive": "Desarxiva",
"unarchived": "Desarxivat",
"unarchived_count": "{count, plural, other {# elements desarxivats}}",
"unfavorite": "Reverteix preferit",
"unhide_person": "Mostra persona",
"unknown": "Desconegut",
"unknown_album": "Àlbum desconegut",
"unknown_year": "Any desconegut",
"unlimited": "Il·limitat",
"unlink_oauth": "Desvincula Oauth",
"unlink_oauth": "Desvincula OAuth",
"unlinked_oauth_account": "Compte Oauth desvinculat",
"unnamed_album": "Àlbum sense nom",
"unnamed_share": "Compartit sense nom",
@@ -889,11 +1006,16 @@
"updated_password": "Contrasenya actualitzada",
"upload": "Puja",
"upload_concurrency": "Concurrència de pujades",
"upload_errors": "Càrrega completada amb {count, plural, one {un error} other {# errors}}, actualitzeu la pàgina per veure els nous elements carregats.",
"upload_status_duplicates": "Duplicats",
"upload_status_errors": "Errors",
"upload_status_uploaded": "Carregat",
"url": "URL",
"usage": "Ús",
"use_custom_date_range": "Fes servir un rang de dates personalitzat",
"user": "Usuari",
"user_id": "ID d'usuari",
"user_role_set": "Establir {user} com a {role}",
"user_usage_detail": "Detall d'ús d'usuari",
"username": "Nom d'usuari",
"users": "Usuaris",
@@ -906,6 +1028,8 @@
"video_hover_setting_description": "Reprodueix la miniatura quan el ratolí plana sobre l'element. Fins i tot quan estigui deshabilitat, la reproducció s'iniciarà planant sobre el botó de reproducció.",
"videos": "Vídeos",
"videos_count": "{count, plural, one {# Video} other {# Vídeos}}",
"view": "Veure",
"view_album": "Veure l'àlbum",
"view_all": "Veure tot",
"view_all_users": "Mostra tot els usuaris",
"view_links": "Mostra enllaços",
@@ -915,10 +1039,12 @@
"viewer": "Visualitzador",
"visibility_changed": "La visibilitat ha canviat per {count, plural, one {# persona} other {# persones}}",
"waiting": "Esperant",
"warning": "Avís",
"week": "Setmana",
"welcome": "Benvingut",
"welcome_to_immich": "Benvingut a immich",
"year": "Any",
"years_ago": "Fa {years, plural, one {un any} other {# anys}}",
"yes": "Sí",
"you_dont_have_any_shared_links": "No tens cap enllaç compartit",
"zoom_image": "Ampliar Imatge"

View File

@@ -127,12 +127,14 @@
"manage_log_settings": "Správa nastavení protokolu",
"map_dark_style": "Tmavý motiv",
"map_enable_description": "Povolit funkce mapy",
"map_gps_settings": "Mapa a GPS",
"map_gps_settings_description": "Správa nastavení mapy a GPS (Reverzní geokódování)",
"map_light_style": "Světlý motiv",
"map_manage_reverse_geocoding_settings": "Správa nastavení <link>Reverzního geokódování</link>",
"map_reverse_geocoding": "Reverzní geokódování",
"map_reverse_geocoding_enable_description": "Povolit reverzní geokódování",
"map_reverse_geocoding_settings": "Reverzní geokódování",
"map_settings": "Mapa a GPS",
"map_settings": "Mapa",
"map_settings_description": "Správa nastavení mapy",
"map_style_description": "URL na style.json motivu",
"metadata_extraction_job": "Extrakce metadat",

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