Compare commits

..

264 Commits

Author SHA1 Message Date
Alex The Bot
c7432834d0 Version v1.108.0 2024-07-10 16:14:02 +00:00
Weblate (bot)
a971fae81f chore(web): update translations (#10961)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
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/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
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/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alexandr Zhytnyk <oper.kh@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Henrik Lievonen <henrik.lievonen@hotmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Lukas Miskovsky <miskovskylukas@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Marc Cizeron <marc@cizeron.me>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: RJS <skudru.rinalds@gmail.com>
Co-authored-by: Rookie Nguyễn <nguyenquocthang2004@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: vytautas <immichtranslation.a03gn@simplelogin.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2024-07-10 11:08:55 -05:00
Alex
a58a2eec53 fix(mobile): crash when navigate between large video files (#11002) 2024-07-10 10:30:27 -05:00
Mert
f43721ec92 fix(ml): armnn not being used (#10929)
* fix armnn not being used, move fallback handling to main, add tests

* formatting
2024-07-10 09:20:43 -05:00
Michel Heusschen
59aa347912 feat(web): render component in notifications (#10990) 2024-07-10 09:05:04 -05:00
Michel Heusschen
1dd1d36120 refactor(web): use callbacks for admin setting events (#10997) 2024-07-10 08:57:18 -05:00
Jason Rasmussen
545b206076 chore(server): remove unused code (#10999)
chore(sesrver): remove unused code
2024-07-10 09:46:52 -04:00
Michel Heusschen
cf77487c00 fix(server): skip smtp validation when there are no changes (#10991)
* fix(server): skip smtp validation when there are no changes

* rename test
2024-07-10 08:37:50 -04:00
renovate[bot]
9d8b755c07 chore(deps): update prom/prometheus docker digest to f20d312 (#10998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 08:37:33 -04:00
Michel Heusschen
bd88b079ea fix(server): avoid server error for invalid email data type (#10978)
* fix(server): avoid server error for invalid email data type

* add e2e test

* fix e2e
2024-07-10 07:58:06 -04:00
renovate[bot]
27b13b82f5 chore(deps): update node (#10996)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 10:44:25 +00:00
renovate[bot]
79c8412660 chore(deps): update typescript-projects (#10975)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 11:40:03 +01:00
renovate[bot]
a078dde241 chore(deps): update base-image to v20240710 (major) (#10995)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-10 11:39:41 +01:00
renovate[bot]
7e4e96c440 chore(deps): update base-image to v20240709 (major) (#10956)
chore(deps): update base-image to v20240709

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 18:41:59 -04:00
Alex
94f129d632 chore(mobile): update photo_manager dep (#10965) 2024-07-09 10:52:58 -05:00
apeman
678111ed3b fix(server): wrong video rotation with QSV and RKMPP decoding (#10905) 2024-07-09 15:18:49 +00:00
renovate[bot]
c1036d6f88 chore(deps): update dependency node to v20.15.1 (#10969)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-09 06:56:25 -04:00
Zack Pollard
e8af0e859e chore: pin specific node version in .nvmrc (#10973) 2024-07-09 10:40:40 +00:00
Michel Heusschen
a0f6d7444a feat(web): improve show & hide people accessibility (#10954) 2024-07-08 22:42:12 -05:00
Simon Thiboutôt
eb89208abb feat(cli): use a queue for duplicate and upload (#10750)
* feat(cli): use a queue for duplicate and upload

Using a queue to process the files makes the file duplicate detection and asset upload more stable and tolerant of network errors. If an error occurs, the whole command will not stop; the task will be retried (3 times) before logging the error and moving to the next step.

The new queue abstraction is using [fastq](https://www.npmjs.com/package/fastq) internally.

* chore(cli): queue.push return promise which resolve with task

* test(cli): add spec for uploadFiles and checkForDuplicates
2024-07-08 22:39:07 -05:00
renovate[bot]
af94f0f979 chore(deps): update typescript-projects (#10962)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-08 22:03:15 -04:00
Weblate (bot)
025a54c462 chore(web): update translations (#10896)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
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/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
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/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Henrik Lievonen <henrik.lievonen@hotmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Joel Calado <joelcalado@gmail.com>
Co-authored-by: Johan Olsson <joholsprivat@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Ján Melíšek <melisek.janko@gmail.com>
Co-authored-by: Lauri Koo <late91@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Majid <abtin.php@gmail.com>
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: Nicholas <nicholasc0212@gmail.com>
Co-authored-by: Petri Hämäläinen <petri.hamalainen@mailbox.org>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: coolstuff99 <aspasmoya@gmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: lapourgagner <poubel125@gmail.com>
Co-authored-by: nazo6 <git@nazo6.dev>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-07-08 19:41:38 -04:00
Jason Rasmussen
334a709cc6 refactor(server): partner search dto (#10902)
* refactor(server): partner search dto

* fix: missed reference

* mobile fix

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-07-08 16:41:39 -04:00
Jason Rasmussen
5f25e2ce82 refactor(server): build resources (#10958) 2024-07-08 14:53:18 -04:00
renovate[bot]
04d0f575b7 chore(deps): update dependency prettier-plugin-organize-imports to v4 (#10930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-07 22:17:06 -04:00
Michel Heusschen
e9683b326a refactor(web): show & hide people (#10933) 2024-07-07 20:33:59 -05:00
Michel Heusschen
cb40db9555 refactor(web): focus trap (#10915) 2024-07-07 20:33:07 -05:00
Ben
39221c8d1f chore(web): translate alt text (#10922)
* chore(web): translate image alt text

* fix: capitalize translations, improve unit test

* fix: unit testing against the actual en.json file

* fix: use derived store to generate alt text
2024-07-07 17:29:56 -05:00
Michel Heusschen
a5467d60ea fix(web): external library disk usage unit (#10935) 2024-07-07 07:21:17 -04:00
Michel Heusschen
d582ec02b1 fix(server): reset-admin-password command (#10939)
* fix(server): reset-admin-password command

* fix immichCli
2024-07-07 07:20:28 -04:00
Jason Rasmussen
59cdbdc492 refactor(server): use .toSorted (#10904) 2024-07-06 10:32:38 -04:00
Jason Rasmussen
01706ccf5c docs: cursed knowledge (#10907) 2024-07-06 00:02:07 -04:00
Jason Rasmussen
6c49a4ba34 fix(server): do not try to upgrade to an older version (#10903) 2024-07-05 17:50:01 -04:00
Jason Rasmussen
e1f25b44d2 refactor(server): stack owner (#10900) 2024-07-05 16:16:53 -04:00
Jordy
f6cafa3290 Update README_nl_NL.md (#10897) 2024-07-05 15:11:05 -04:00
Jason Rasmussen
53d4a5268b refactor(server): remove has asset (#10891) 2024-07-05 15:10:11 -04:00
Jason Rasmussen
cf88f4b6f8 refactor(server): bulk interface (#10889) 2024-07-05 14:58:34 -04:00
Jason Rasmussen
ac8d8d91f7 chore: add az language (#10890) 2024-07-05 13:24:03 -04:00
Weblate (bot)
842291124c chore(web): update translations (#10831)
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/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
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/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/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/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Jaime Branco <jaimembranco@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Jonas Brenig <jonas.brenig@uni-wuerzburg.de>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: Tim Krämer <weblate@tk22.de>
Co-authored-by: Vladimir <vladimir.stoev1015@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: cevirici <cevirici13@users.noreply.hosted.weblate.org>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: gokulvasanthgv <gokulvasanthstudies@gmail.com>
Co-authored-by: r926215 <evdrt01@gmail.com>
Co-authored-by: vytautas <immichtranslation.a03gn@simplelogin.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2024-07-05 12:45:04 -04:00
immich-tofu[bot]
6f5b3c47b0 Added Code of conduct 2024-07-05 15:31:13 +00:00
Alex
b25642b889 fix(mobile): search picker overflow (#10870) 2024-07-05 09:47:33 -05:00
Alex
7bde19d842 chore(mobile): separate build flavors (#10872) 2024-07-05 09:43:11 -05:00
Jason Rasmussen
eb1ba11d60 refactor(server): rename asset stack to stack (#10828) 2024-07-05 09:08:36 -04:00
Michel Heusschen
23b3073687 fix(web): download archive for public user (#10877) 2024-07-05 09:08:14 -04:00
Michel Heusschen
3cd187dced refactor(web): use derived instead of get(t) (#10884) 2024-07-05 09:06:35 -04:00
dependabot[bot]
6791af8c2c chore(deps): bump docker/setup-buildx-action from 3.3.0 to 3.4.0 (#10843)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 11:58:20 +01:00
dependabot[bot]
e566fbb009 chore(deps): bump docker/setup-qemu-action from 3.0.0 to 3.1.0 (#10844)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-05 11:58:10 +01:00
Michel Heusschen
e5c92912fc chore(web): remove unused s function (#10878) 2024-07-05 11:51:52 +01:00
Michel Heusschen
f33d5b0a38 fix(web): duplicate thumbnail cover full width (#10880) 2024-07-05 11:51:28 +01:00
waclaw66
df10618a7e fix(mobile): exposure time display (#10842)
exposure time fix
2024-07-04 23:30:06 -05:00
Michel Heusschen
6030349a6f feat(web): use browser language by default (#10849) 2024-07-04 22:56:54 -05:00
waclaw66
6629bf50ae fix(web): user profile translation (#10851) 2024-07-04 22:55:40 -05:00
Michel Heusschen
e32ce82179 chore: remove unused snowburst one font (#10863) 2024-07-04 17:15:54 -04:00
renovate[bot]
10ea894186 chore(deps): update base-image to v20240702 (major) (#10821)
chore(deps): update base-image to v20240702

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-03 21:35:11 -05:00
Jason Rasmussen
81d12c0586 refactor(server): notification events (#10754) 2024-07-03 21:06:20 -05:00
Mert
0b88bef157 fix(server): face search migration sometimes failing (#10827)
* turn it off and back on

* handle missing smart search embedding column

* handle missing face embedding column

* simplify

* Revert "simplify"

This reverts commit 8322af0baf.

* fix migration
2024-07-03 21:02:43 -05:00
Weblate (bot)
2b8942026c chore(web): update translations (#10795)
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/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/fa/
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/hi/
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/lv/
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/ru/
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/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Andrii Solianyk <asolianik2015@gmail.com>
Co-authored-by: Anrijs J <ajargans@gmail.com>
Co-authored-by: Bartłomiej Ruk <bartek04041993@gmail.com>
Co-authored-by: Ben Tomsen <torben+554gc3ym@g-b.dk>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Ef <weblate.marina324@passmail.net>
Co-authored-by: Filip Bredborg <fbredborg@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Hoi <Hoihoi@users.noreply.hosted.weblate.org>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Matteo D <alex3025game@gmail.com>
Co-authored-by: MattiaPell <mattiapellegrini16@gmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Rajat Patel <rajat@thenextgeek.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Sushil Kumar <sushilz25558@gmail.com>
Co-authored-by: Vladimir <vladimir.stoev1015@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: Yu-Kai \"Steven\" Wang <steven97102@gmail.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: cevirici <cevirici13@users.noreply.hosted.weblate.org>
Co-authored-by: idubnori <i.dub.nori@gmail.com>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: r926215 <evdrt01@gmail.com>
Co-authored-by: vytautas <immichtranslation.a03gn@simplelogin.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-07-03 21:21:02 -04:00
Jason Rasmussen
f5937a5a9b fix(web): delete library (#10822) 2024-07-03 17:09:15 -04:00
Jason Rasmussen
04f0ac1aad chore(web): remove unnused property (#10820) 2024-07-03 16:41:17 -04:00
Tom Vincent
4a481acca6 fix(server): Postgres -> Redis websocket (#10801) 2024-07-03 16:27:29 -04:00
Jason Rasmussen
de62bd3ba5 fix(server): healthcheck (#10811) 2024-07-03 11:27:03 -05:00
dependabot[bot]
ab2ea28ed9 chore(deps): bump docker/build-push-action from 6.2.0 to 6.3.0 (#10805)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-03 11:47:05 +01:00
Alex The Bot
96f29cefeb Version v1.107.2 2024-07-03 03:18:19 +00:00
Alex
6f950ea45d fix(mobile): incorrect translation string (#10794) 2024-07-02 22:13:22 -05:00
Alex
99c45bd4d2 fix(web): slow people page load (#10793) 2024-07-02 22:13:11 -05:00
Weblate (bot)
312030f275 chore(web): update translations (#10753)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/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/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translation: Immich/immich

Co-authored-by: Alex van den Hoogen <alex3305@gmail.com>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Aurora <arci@anche.no>
Co-authored-by: Bartłomiej Ruk <bartek04041993@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Heine Olsen <olsen10051988@gmail.com>
Co-authored-by: Henrik Lievonen <henrik.lievonen@hotmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Justin Ruiter <weblate24@justinruiter.nl>
Co-authored-by: Maciek S <maslanypotwor1@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: MiguelNdeCarvalho <geral@miguelndecarvalho.pt>
Co-authored-by: Nicolò <nicveronese@gmail.com>
Co-authored-by: Ryan Gleeson <gleeson.ryanj@gmail.com>
Co-authored-by: Vincenzo Nunziata <vinciosdev@gmail.com>
Co-authored-by: Ziemowit Zabawa <ziemek.zabawa@outlook.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
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: wariw <wariwpl@gmail.com>
2024-07-02 22:03:20 -05:00
Alex
bed9ccadbc chore(mobile): post release pump (#10775) 2024-07-02 16:41:40 -05:00
renovate[bot]
d55499eba0 chore(deps): update typescript-projects (#10763)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 17:28:24 -04:00
Daniel Dietzler
910b75c6cc docs: fix typo in translations link (#10783) 2024-07-02 16:40:51 -04:00
Jason Rasmussen
6a11464d60 fix(server): do not allow merging a person into themselves (#10776) 2024-07-02 19:56:05 +00:00
Alex The Bot
aa29f5d69c Version v1.107.1 2024-07-02 19:04:29 +00:00
Alex
1ee10ee2d6 feat(mobile): Revert render assets on device by default (#10470) (#10774)
Revert "feat(mobile): render assets on device by default (#10470)"

This reverts commit 32da9d90e4.
2024-07-02 19:01:54 +00:00
Alex
f23401d911 fix(mobile): map crashes on Android (#10773)
Revert "fix(mobile): upgrade maplibre_gl package to fix issue with crash in ios7.4 above simulator (#10182)"

This reverts commit 99c6fdbc1c.
2024-07-02 13:43:52 -05:00
Alex
14d94df1b8 chore(mobile): post release pump (#10759)
* chore(mobile): post release pump

* remove cache report file
2024-07-02 11:20:52 -05:00
Alex The Bot
b47ec2f88f Version v1.107.0 2024-07-02 14:13:10 +00:00
Michel Heusschen
b5c8ca075c fix(web): scroll jank on memories page (#10752) 2024-07-02 11:59:11 +01:00
Harshith Goka
7bfa642fa3 feat(web): add keyboard shortcuts to duplicates utility (#10736)
SHIFT + K: Select keep all
SHIFT + T: Select trash all
SHIFT + C: Confirm selection
2024-07-02 11:32:53 +01:00
Weblate (bot)
9a83038728 chore(web): update translations (#10742)
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Luca De Falco <deffo89@gmail.com>
Co-authored-by: MSDNicrosoft <wang3311835119@hotmail.com>
Co-authored-by: Marcin Czop <marcin@czop.ru>
Co-authored-by: Mario <17320863+myanesp@users.noreply.github.com>
Co-authored-by: Patrick Bellasi <derkling@matbug.net>
Co-authored-by: Ryan Gleeson <gleeson.ryanj@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sitram <adrian.martis@gmail.com>
Co-authored-by: Suryo Wibowo <nutzlichsein+github@gmail.com>
Co-authored-by: boman <boman.d@gmail.com>
Co-authored-by: cevirici <cevirici13@users.noreply.hosted.weblate.org>
Co-authored-by: polar <polar8143@users.noreply.hosted.weblate.org>
Co-authored-by: traptegies <lars.reuss@gmx.de>
2024-07-02 10:22:54 +00:00
renovate[bot]
a1629f0793 chore(deps): update docker/build-push-action action to v6.2.0 (#10745)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 11:11:15 +01:00
renovate[bot]
d4cba57102 fix(deps): update typescript-projects (#10744)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 11:10:56 +01:00
renovate[bot]
2934676594 chore(deps): update node (#10741)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 11:09:15 +01:00
dependabot[bot]
ebea793534 chore(deps): bump docker/build-push-action from 6.1.0 to 6.2.0 (#10655)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 11:08:34 +01:00
renovate[bot]
eeae77422f chore(deps): update terraform cloudflare to v4.36.0 (#10718)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-02 11:08:16 +01:00
Weblate (bot)
850424e960 chore(web): update translations (#10729)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translation: Immich/immich

Co-authored-by: Alessandro Saglia <github.eatery9779@bear-d.me>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Kirill Zhukov <siper13@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Przemek <skweresp@gmail.com>
Co-authored-by: Ryan Gleeson <gleeson.ryanj@gmail.com>
Co-authored-by: cevirici <cevirici13@users.noreply.hosted.weblate.org>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-07-01 19:16:31 -04:00
Alex
58298bd038 docs: blog post July 2024 update (#10737) 2024-07-01 15:57:25 -05:00
Alex
e46af5c26b fix(web): activity status padding regression (#10734) 2024-07-01 15:06:46 -04:00
Zack Pollard
3b37b70626 feat(server): user and server license endpoints (#10682)
* feat: user license endpoints

* feat: server license endpoints

* chore: pr feedback

* chore: add more test cases

* chore: add prod license public keys

* chore: open-api generation
2024-07-01 17:43:16 +00:00
Alex
4193b0dede fix(web): suppress album upload notification (#10717)
* fix(web): suppress album upload notification

* restore translation strings
2024-07-01 13:19:57 -04:00
Michel Heusschen
ac51cad075 feat(web): html tags inside plural and select messages (#10696)
* feat(web): html tags inside plural and select messages

* add component docs
2024-07-01 11:54:13 -05:00
Alex
b54dd4e135 docs: Translations update (#10730)
chore(mobile): translation update
2024-07-01 09:44:15 -05:00
pyorot
f5164b42e0 fix(web): remove black bezels + better integrate ActivityStatus (#10667)
* remove black bezels + better integrate activity status

* remove justify-self-end + mr-4 → mr-3 (closer to desired spacing)

* clean up

* clean up some more

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-07-01 14:05:49 +00:00
Weblate (bot)
783088afbe chore(web): update translations (#10605)
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/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
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/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
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/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Askolds Zusans <askolds.zusans@gmail.com>
Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Daniel <danielwichers@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Ivan Naboichshikov <inaboichshikov@gmail.com>
Co-authored-by: JBP <weblate@1peer1boom.nl>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Kirill Zhukov <siper13@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Londoneye02 <jcdelcaz@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Matteo <matteo.visintini@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Mihai Mura <mihai.mura.dev@gmail.com>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Patrick Williamson <patrickwill@me.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Son Do <son.do@merctrans.vn>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Ultragian <giancarlo.brasil@gmail.com>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: Vegard Skullerud <vegard@skullerud.net>
Co-authored-by: Victor Sueiro <kiwicaja@gmail.com>
Co-authored-by: Vincenzo Nunziata <vinciosdev@gmail.com>
Co-authored-by: YFrendo <yann.frendo@live.fr>
Co-authored-by: Yi Kuo <kuokuoyiyi@gmail.com>
Co-authored-by: Yusuf Mohammed <yousufinternet@gmail.com>
Co-authored-by: alien75 <thomas@imolesi.it>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: dweissmueller <2868emerald@navalcadets.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: nhy42 <paul.zanolin@gmail.com>
Co-authored-by: polar <polar8143@users.noreply.hosted.weblate.org>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-07-01 08:54:33 -05:00
indam
744dfb675b docs: sync Chinese README with the official English version (#10724) 2024-07-01 09:27:35 +01:00
daniel bogachevsky
1d282851e2 fix(web): Sort timezones in assets settings by offset (#10697)
* fixed timezones on web are sorted alphabetically

* swaped order of operations in order to use DataTime.offset property for sorting

* optimization
2024-06-30 22:41:47 -05:00
Dawid Rejowski
d00d33d8a5 chore(doc): small punctuation fix backup-and-restore.md (#10704)
Small punctuation fix
2024-06-30 21:59:18 -05:00
Michel Heusschen
560dbd3c65 fix(web): shared link card (#10702) 2024-06-30 17:34:52 -05:00
Michel Heusschen
c58148af35 feat(web): add more translations (#10700)
* feat(web): add more translations

* formatting
2024-06-30 17:29:10 -05:00
bo0tzz
e54c18367b chore: Lower default duplicate detection distance (#10703) 2024-06-30 11:36:02 -04:00
Alex
8b6d27f1bc fix(server): show partners assets on timeline without permission (#10705)
* fix(server): show partners assets on timeline without permission

* save all

* correct fix
2024-06-29 22:45:59 -05:00
Alex
887acb9d9f chore(mobile): Revert "remove exclude album mechanism for backup (#10552)" (#10686)
Revert "chore(mobile): remove exclude album mechanism for backup (#10552)"

This reverts commit 5f47cf604a.
2024-06-29 11:30:18 -05:00
Michel Heusschen
8f553ddb39 fix(web): i18n race condition in load function (#10693) 2024-06-29 11:29:56 -05:00
Jason Rasmussen
24c1855899 fix: album remove asset bug (#10687)
* fix: album remove asset bug

* trigger GH Action

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-29 00:17:58 -04:00
Ben McCann
6ebae3c84f chore(deps): upgrade @testing-library/svelte (#10690) 2024-06-28 21:31:27 -04:00
martin
e0bb9add91 feat(web): use websocket to update the feature photo (#10683)
feat: use ws to update the feature photo
2024-06-28 14:40:18 -05:00
Pascal Sommer
821570f2fb feat(web): show favorite icon in duplicate asset (#10688)
* show favorite icon in duplicate asset

* remove isSharedLink check

* swap places of favorite icon and view button
2024-06-28 14:37:12 -05:00
Zack Pollard
a2364a12cf refactor: move /server-info endpoints to /server (#10677) 2024-06-28 17:08:19 +01:00
renovate[bot]
e361640e39 chore(deps): update grafana/grafana docker tag to v11.1.0 (#10679)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-28 08:49:51 -04:00
martin
37b5d92110 fix(web): new feature photo (#9443)
* fix: new feature photo

* fix: use updatedAt
2024-06-27 20:16:26 -04:00
Matthew Momjian
325aa1d392 fix(docs): restart DB backup container (#10671)
Update backup-and-restore.md
2024-06-27 20:14:55 -04:00
Jason Rasmussen
72bf9439b0 refactor(server): event emits (#10648)
* refactor(server): event emits

* refactor: change default priority to 0
2024-06-27 15:54:20 -04:00
Jason Rasmussen
7e99394c70 fix(server): live photo relation (#10637)
* fix(server): live photo relation

* handle deletion and unit test

* lint

* chore: clean up and e2e tests

* fix test

* sql

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-27 14:41:49 -05:00
Miguel Domingues
8ff9c37d79 fix(web): match storage_template_migration_job with storage_template_migration (#10662) 2024-06-27 19:33:28 +00:00
Zhenzhen Zhao
0b4153e256 chore(trans): add zh-CN translations for custom proxy headers (#10660)
chore: add zh-CN translations for proxy headers

Signed-off-by: TripleZ <me@triplez.cn>
2024-06-27 13:38:51 -05:00
Michel Heusschen
12b9f3ad91 fix(server): about info version (#10659) 2024-06-27 12:36:25 -05:00
Jason Rasmussen
9fc9465cec feat(web): link router (#10644)
feat: link router
2024-06-27 09:09:28 -04:00
Alex
d8175d8da8 fix(mobile): asset state remain in gallery view after being deleted (#10603)
* fix(mobile): asset doesn't get removed from state renderList

* fix delete last assets

* refactor
2024-06-26 23:15:26 -05:00
Matej Kramny
922430da36 feat(mobile): add additional request headers (#10588)
* add additional request headers

* improve interface

* move headers under advanced settings

* refactor

* refactor

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-26 19:31:55 +00:00
Feng Kaiyu
a3c3619811 fix(cli): fix broken --album on Windows. (#10626)
Extract folder names via system function to avoid the difference between / and \ on Windows.
2024-06-26 11:12:30 -05:00
Pedro Ribeiro
7f5a3e5adb docs: Add new community guide to access Immich with a custom domain (#10638)
add new community guide to access immich with a custom domain

Co-authored-by: ppr88 <ppr88@local>
2024-06-26 10:55:27 -05:00
Jason Rasmussen
63041674c2 fix(server): user delete with stacked assets (#10642) 2024-06-26 09:29:52 -04:00
Jason Rasmussen
8a445cac07 chore: build metadata (#10612)
feat: build metadata
2024-06-26 08:25:09 -04:00
renovate[bot]
15c1cd6449 chore(deps): update dependency @types/node to ^20.14.7 (#10635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 21:48:48 +00:00
renovate[bot]
8198259de8 chore(deps): update dependency typescript to v5.5.2 (#10633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 16:28:11 -04:00
Jason Rasmussen
6decf33226 chore: better auto labels (#10632) 2024-06-25 22:02:24 +02:00
renovate[bot]
df0064c83b chore(deps): pin node.js to 0ccc08f (#10628)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 17:22:16 +01:00
Zack Pollard
c754f2504b chore: bump node docker versions (#10629) 2024-06-25 12:19:51 -04:00
renovate[bot]
0891658668 chore(deps): update base-image to v20240625 (major) (#10620)
chore(deps): update base-image to v20240625

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 12:18:38 -04:00
renovate[bot]
5b909eeaf0 chore(deps): update mambaorg/micromamba:bookworm-slim docker digest to 333f759 (#10631) 2024-06-25 16:18:27 +00:00
Zack Pollard
0484a4e252 chore: add renovate makefile command for testing renovate changes (#10630) 2024-06-25 12:18:02 -04:00
renovate[bot]
bf83fdee49 chore(deps): update terraform cloudflare to v4.35.0 (#10420)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 17:04:15 +01:00
renovate[bot]
9eafbb0524 fix(deps): update machine-learning (#10610) 2024-06-25 12:03:27 -04:00
Mert
6356c28f64 refactor(ml): model sessions (#10559) 2024-06-25 12:00:24 -04:00
Zack Pollard
6538ad8de7 chore: update docker node alpine versions to 3.20 (#10621) 2024-06-25 11:04:02 -04:00
renovate[bot]
9f9e42a96a chore(deps): update dependency prettier-plugin-svelte to v3.2.5 (#10623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 11:03:29 -04:00
renovate[bot]
905d6c1508 chore(deps): update dependency @types/node to ^20.14.6 (#10627)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 11:00:49 -04:00
Zack Pollard
91af793b52 ci: group docker node and npm node updates together (#10625)
also should fix @types/node not being correct versions
2024-06-25 15:49:50 +01:00
Zack Pollard
5912fcc393 ci: use .nvmrc for node-setup node-version in github actions (#10619)
* chore: add node version pinning with .nvmrc and volta for the typescript sdk

* ci: add missing setup-node actions and use .nvmrc for setup-node node-version
2024-06-25 14:01:15 +01:00
renovate[bot]
b5b0c6fe8b chore(deps): pin dependencies (#10618)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 11:22:38 +00:00
Jason Rasmussen
330648ff44 chore(deps): use full semver docker tag for node images (#10613)
* chore: use full semver docker tag for node images

* Update server/Dockerfile

Co-authored-by: bo0tzz <git@bo0tzz.me>

---------

Co-authored-by: bo0tzz <git@bo0tzz.me>
2024-06-25 11:12:27 +00:00
Junghyuk Kwon
54d1dc56a2 chore(docs): update Korean README (#10462)
* chore: update Korean README

* chore: correction

* chore(docs): update README_ko_KR.md

* chore: correction
2024-06-25 11:38:11 +01:00
renovate[bot]
d8e6b17ef9 fix(deps): update typescript-projects (#10616)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-25 11:14:15 +01:00
renovate[bot]
d7a33c8ec2 fix(deps): update typescript-projects (#10611)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-24 23:00:09 -04:00
Jason Rasmussen
0012369c67 chore: merge weblate (#10604) 2024-06-24 15:19:12 -04:00
waclaw66
cb3ac4ff9f chore(web): translation finetuning (#10601)
fixes
2024-06-24 15:05:45 -04:00
Weblate (bot)
4988df3fcb chore(web): update translations (#10593)
* chore(web): update translations

Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: polar <polar8143@users.noreply.hosted.weblate.org>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
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/sr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/
Translation: Immich/immich

* chore: split serbian

---------

Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: polar <polar8143@users.noreply.hosted.weblate.org>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-24 15:04:53 -04:00
Jason Rasmussen
fc6c9a19d9 feat: more languages (#10595)
chore: more languages
2024-06-24 14:15:08 -04:00
Jason Rasmussen
13cc1f0aa6 docs: add private photos to roadmap (#10599)
docs: add locked photos to roadmap
2024-06-24 14:01:44 -04:00
Jason Rasmussen
ba72802888 chore: use immich.app email for security reports (#10594)
chore: use  immich.app email for security reports
2024-06-24 07:25:48 -07:00
RanKKI
04f0e29df6 fix(mobile): inconsistent thumbnail's label (#10589)
* fix(mobile): inconsistent thumbnail with label

* fix: limit person's name width
2024-06-24 07:24:57 -07:00
Jason Rasmussen
c83de5213f docs: add info about translations and weblate (#10591)
docs: update
2024-06-24 07:23:09 -07:00
waclaw66
dd2c7400a6 chore(web): another missing translations (#10274)
* chore(web): another missing translations

* unused removed

* more keys

* lint fix

* test fixed

* dynamic translation fix

* fixes

* people search translation

* params fixed

* keep filter setting fix

* lint fix

* $t fixes

* Update web/src/lib/i18n/en.json

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* another missing

* activity translation

* link sharing translations

* expiration dropdown fix - didn't work localized

* notification title

* device logout

* search results

* reset to default

* unsaved change

* select from computer

* selected

* select-2

* select-3

* unmerge

* pluralize, force icu message

* Update web/src/lib/components/asset-viewer/asset-viewer.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* review fixes

* remove user

* plural fixes

* ffmpeg settings

* fixes

* error title

* plural fixes

* onboarding

* change password

* more more

* console log fix

* another

* api key desc

* map marker

* format fix

* key fix

* asset-utils

* utils

* misc

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-06-24 09:50:01 -04:00
Weblate (bot)
df9e074304 chore(web): update translations (#10304)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
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/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/hr/
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/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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: 2001 Y <yoshiki.tamura2001@gmail.com>
Co-authored-by: Abdullah <abdullahsalameh66@gmail.com>
Co-authored-by: Ahmad Malek <maichael.gt@gmail.com>
Co-authored-by: AlexMa2011 <alexma2011@outlook.com>
Co-authored-by: Alexis <alexisl61@outlook.fr>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: Clément Roblot <clement.roblot@martobre.fr>
Co-authored-by: Coooolfan <coolfan1024@outlook.com>
Co-authored-by: Daddie0 <33762262+GoByeBye@users.noreply.github.com>
Co-authored-by: Dean Cvjetanović <forteee@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Denis Rebaud <denis@rebaud.fr>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Gustavo Ceolin <gustavogiulceolin@hotmail.com>
Co-authored-by: Hadrián Montes <hadrianmontes@gmail.com>
Co-authored-by: Héctor Martínez Juste <hectorzin@hotmail.com>
Co-authored-by: IM Ben <beniiorga@gmail.com>
Co-authored-by: Ignacy Kajdan <ignacy.kajdan@gmail.com>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: J1mooo <programingstafi@gmail.com>
Co-authored-by: JBP <weblate@1peer1boom.nl>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: JinYoung Park <norahc1999@gmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: Julien Deveaux <julien.deveaux@hotmail.com>
Co-authored-by: Julius969 <juliusdjorup@proton.me>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Kovács Gergely <kgerg@duck.com>
Co-authored-by: Kyle Park <mysky3056@gmail.com>
Co-authored-by: Lauritz Tieste <lauritz6000000@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Leo Bottaro <weblate@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Linx <johnsmith_2003@hotmail.com>
Co-authored-by: Logge <hyper.xjo@gmail.com>
Co-authored-by: Maks s <smaks2313@gmail.com>
Co-authored-by: Matjaž T <matjaz@moj-svet.si>
Co-authored-by: Max <Maxime.morasse@hotmail.fr>
Co-authored-by: Maximilian Waidelich <44324946+maxwai@users.noreply.github.com>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: Napat Srichan <napatsrichan2001@gmail.com>
Co-authored-by: Nick Götti <nick.goetti@outlook.com>
Co-authored-by: Patrick <patrickwill@me.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Petri Hämäläinen <petri.hamalainen@mailbox.org>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Polly Julien <julien.polly@laposte.net>
Co-authored-by: Ptsa Daniel <ptsa1987@gmail.com>
Co-authored-by: Roukanken <kuko0411@gmail.com>
Co-authored-by: Ryan Gleeson <gleeson.ryanj@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: Sleeper CH <sleeperch@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Swayerka <admin@crozet.cc>
Co-authored-by: Tyoda <tyoda@pm.me>
Co-authored-by: ZtereoHYPE <57519662+ZtereoHYPE@users.noreply.github.com>
Co-authored-by: aln <imyapear@gmail.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: clementdelestre <clementdelestre@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: ingria <codefuhrer@gmail.com>
Co-authored-by: mxm199 <mxm199@bk.ru>
Co-authored-by: myurar1a <sirometroid1235@outlook.jp>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: wariw <wariwpl@gmail.com>
Co-authored-by: weiwhy <why1573920133@hotmail.com>
Co-authored-by: Àlex Garcia <alexgarciavila@gmail.com>
Co-authored-by: Алексей Меринов <merinov@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Кирилл Москатов <kirillmoskatov@gmail.com>
2024-06-24 12:38:50 +00:00
Alex
5f47cf604a chore(mobile): remove exclude album mechanism for backup (#10552)
* chore(mobile): remove exclude album selection mechanism

* code generator

* code generator
2024-06-22 15:31:27 -07:00
dependabot[bot]
8e2f6f1f41 chore(deps): bump docker/build-push-action from 6.0.1 to 6.1.0 (#10522)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.0.1 to 6.1.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.0.1...v6.1.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-22 11:10:47 -07:00
Alex
32da9d90e4 feat(mobile): render assets on device by default (#10470)
* feat(mobile): render asset on device by default

* remove unused service
2024-06-22 09:13:05 -07:00
Michel Heusschen
6164640575 fix(web): FormatMessage development keys (#10536) 2024-06-22 09:08:56 -07:00
Feng Kaiyu
4cb165304b fix(cli): handle patterns correctly on Windows (#10430)
Modify the handling of patterns in the `crawl` function to correctly
convert the current path to a pattern when it contains backslash on
Windows, in according to fast-glob's docs.
2024-06-21 17:09:02 -07:00
renovate[bot]
1200265425 chore(deps): update docker.io/redis:6.2-alpine docker digest to 328fe6a (#10515)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-21 16:46:39 -04:00
renovate[bot]
0a3aafd439 chore(deps): update redis:6.2-alpine docker digest to 328fe6a (#10516)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-21 16:46:24 -04:00
Michel Heusschen
aaf7c0b6db fix(web): missing translations (#10504) 2024-06-21 13:09:10 -07:00
Michel Heusschen
b3252ffdac feat(web): translations containing html (#10491)
* feat(web): translations containing html

* add tests and more translations

* more translations

* rename FormatTags --> FormatMessage

* update version_announcement_message
2024-06-21 13:08:36 -07:00
Michel Heusschen
1129020159 fix(web): six digit year input (#10517) 2024-06-21 13:05:17 -07:00
erathmus
61a5d67674 feat(web): Adds sort order from album to shared album. (#10528) 2024-06-21 08:14:30 -07:00
Mert
42f3b50422 fix(server): /places entries sometimes not ordered alphabetically (#10514) 2024-06-20 23:48:19 -04:00
Daniel Dietzler
5e9a7b17d9 fix(server): allow library id to be null in metadata search (#10512)
* fix: allow library id to be null in metadata search

* chore: open api
2024-06-20 16:02:05 -07:00
Ben
0fda67543d chore(web): context menu improvements (#10475)
- ability to add custom hover colors
- migrate activity menu to ButtonContextMenu component
- onClick callbacks rather than events for menu options
- remove slots
- configurable menu option colors
- improve menu option layout
2024-06-20 14:15:36 -07:00
Michel Heusschen
5cde52eec9 feat(web): duplicate ui tweaks (#10506) 2024-06-20 14:14:34 -07:00
Matthew Momjian
eff839251c fix(deployment): Postgres healthcheck, add username to pg_isready (#10221) 2024-06-20 14:17:57 -04:00
Mert
a42af06889 fix(ml): limit load retries (#10494) 2024-06-20 14:13:18 -04:00
Mert
79a8ab71ef fix(server): reindex after changing to a model with a different dimension size (#10496)
reindex after truncating
2024-06-19 17:25:02 -04:00
Mert
1191978d50 fix(server): library refresh not checking trashed assets (#10495)
* set `withDeleted`

* update sql
2024-06-19 20:42:55 +00:00
renovate[bot]
7ea0278b32 chore(deps): update dependency eslint-plugin-unicorn to v54 (#10486)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 12:56:35 -04:00
renovate[bot]
4ef033aa55 chore(deps): update base-image to v20240618 (major) (#10457)
chore(deps): update base-image to v20240618

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 12:53:02 -04:00
dependabot[bot]
660afa9fad chore(deps): bump docker/build-push-action from 6.0.0 to 6.0.1 (#10483)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.0.0 to 6.0.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.0.0...v6.0.1)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 12:26:52 -04:00
dependabot[bot]
104048ecd5 chore(deps): bump ws and engine.io-client in /e2e (#10488)
Bumps [ws](https://github.com/websockets/ws) and [engine.io-client](https://github.com/socketio/engine.io-client). These dependencies needed to be updated together.

Updates `ws` from 8.11.0 to 8.17.1
- [Release notes](https://github.com/websockets/ws/releases)
- [Commits](https://github.com/websockets/ws/compare/8.11.0...8.17.1)

Updates `engine.io-client` from 6.5.3 to 6.5.4
- [Release notes](https://github.com/socketio/engine.io-client/releases)
- [Changelog](https://github.com/socketio/engine.io-client/blob/main/CHANGELOG.md)
- [Commits](https://github.com/socketio/engine.io-client/compare/6.5.3...6.5.4)

---
updated-dependencies:
- dependency-name: ws
  dependency-type: indirect
- dependency-name: engine.io-client
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 16:25:55 +00:00
dependabot[bot]
bec77f926e chore(deps): bump braces from 3.0.2 to 3.0.3 in /server (#10487)
Bumps [braces](https://github.com/micromatch/braces) from 3.0.2 to 3.0.3.
- [Changelog](https://github.com/micromatch/braces/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/braces/compare/3.0.2...3.0.3)

---
updated-dependencies:
- dependency-name: braces
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-19 16:23:21 +00:00
renovate[bot]
ba57a1144d chore(deps): update prom/prometheus docker digest to 075b1ba (#10484)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 12:20:58 -04:00
Mert
b3f9641edf feat(web): bulk deduplicate (#10448)
* bulk deduplicate

* notification for keeping all duplicates

* fix notification

* remove unused text

* pr feedback

* wording

* formatting
2024-06-19 12:11:59 -04:00
Mert
86cbc6e125 chore(ml): support python 3.12 (#10481) 2024-06-19 10:51:10 -04:00
Mert
968553a50e fix(server): video thumbnail generation failing with single i-frame (#10477) 2024-06-19 10:50:25 -04:00
Mert
5813dc02d1 fix(server): let thumbnail generation fail on error (#10479) 2024-06-19 10:50:09 -04:00
Mert
58b17a866b feat(web): display original heif images for safari (#10478) 2024-06-19 10:49:59 -04:00
renovate[bot]
c58b0ac66a chore(deps): update typescript-projects (#10445)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-19 07:39:25 -04:00
waclaw66
517a83cfa9 fix(web): comment send button (#10453) 2024-06-18 16:29:46 -07:00
renovate[bot]
7daa761eed chore(deps): update mambaorg/micromamba:bookworm-slim docker digest to b17c9b1 (#10465)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 19:05:26 -04:00
Mert
e58131492d fix(server): consider all I-frames for video thumbnails (#10471)
nointra instead of nokey
2024-06-18 19:02:33 -04:00
renovate[bot]
b21572cb32 chore(deps): update machine-learning (#10446)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-18 00:45:29 -04:00
renovate[bot]
8332efcd04 chore(deps): update dependency exiftool-vendored to v27 (#10447)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-17 21:03:23 -07:00
Ben
b71aa4473b feat(web): keyboard accessible context menus (#10017)
* feat(web,a11y): context menu keyboard navigation

* wip: all context menus visible

* wip: more migrations to the ButtonContextMenu, usability improvements

* wip: migrate Administration, PeopleCard

* wip: refocus the button on click, docs

* fix: more intuitive RightClickContextMenu

- configurable title
- focus management: tab keys, clicks, closing the menu
- automatically closing when an option is selected

* fix: refining the little details

- adjust the aria attributes
- intuitive escape key propagation
- extract context into its own file

* fix: dropdown options not clickable in a <Portal>

* wip: small fixes

- export selectedColor to prevent unexpected styling
- better context function naming

* chore: revert changes to list navigation, to reduce scope of the PR

* fix: remove topBorder prop

* feat: automatically select the first option on enter or space keypress

* fix: use Svelte store instead to handle selecting menu options

- better prop naming for ButtonContextMenu

* feat: hovering the mouse can change the active element

* fix: remove Portal, more predictable open/close behavior

* feat: make selected item visible using a scroll

- also: minor cleanup of the context-menu-navigation Svelte action

* feat: maintain context menu position on resize

* fix: use the whole padding class as better tailwind convention

* fix: options not announcing with screen reader for ButtonContextMenu

* fix: screen reader announcing right click context menu options

* fix: handle focus out scenario

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-17 20:52:38 -07:00
Muhideen Mujeeb Adeoye
99c6fdbc1c fix(mobile): upgrade maplibre_gl package to fix issue with crash in ios7.4 above simulator (#10182)
* fix(mobile): upgrade maplibre_gl package to fix issue with crash in ios7.4 above simulator

* chore: switch from deprecated widget and controller name to new name in latest sdk

* remove todo

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-17 22:27:54 +00:00
aviv926
c1a5ed3526 fix(web): Update prompt (#10237)
* update

* update

* update

* npm run format:fix
2024-06-17 15:24:04 -07:00
dependabot[bot]
9000ce4283 chore(deps): bump docker/build-push-action from 5.4.0 to 6.0.0 (#10433)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.4.0 to 6.0.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.4.0...v6.0.0)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-17 13:11:55 -07:00
Alex
e8994d9ffd fix(web): confirm button is disabled if two dialogs are shown subsequently (#10440) 2024-06-17 11:44:25 -07:00
Stephen Smith
1b67ea2d91 chore(server): update exiftool and migrate off deprecated method signatures (#10367)
* chore(server): update exiftool and migrate off deprecated method signatures

* chore(server): update exiftool-vendored to 27.0.0

* chore(server): switch away from deprecated exiftool method signatures
- options now includes read/writeArgs making the deprecated signatures with
  args array redundant
- switch read call from file,args,options to file,options
- switch write call from file,tags,args to file,tags,options

* chore(server): move largefilesupport flags into exiftool constructor
- options now includes read/writeArgs making it available to be set globally in
  constructor
- switches back to instantiating an instance of exiftool

* chore(server): consolidate exiftool config into constructor along with writeArgs

* chore(server): move exiftool instantiation into MetadataRepository constructor
2024-06-17 10:11:11 -07:00
François-Guillaume Lemesre
38e26fd67c chore: update Unraid Docker-Compose documentation to reflect missing healthcheck start_interval parameter from Docker Engine v24.0.9. (#10406)
* Update Unraid Docker-Compose documentation to reflect missing healthcheck start_interval parameter from Docker Engine v24.0.9.

Unraid v6.12.10 uses Docker Engine v24.0.9, which does not support setting a start_interval parameter, used by the database container. Added info to the documentation to bypass this while retaining the initial health check interval.

* Fixed Markdown formatting.

* Removed info box formatting issue.

Moved the information about Unraid's Docker Engine version to section 4 of the installation instructions, instead of trying to use an info box that broke the formatting.

* fix format

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-17 17:08:31 +00:00
RanKKI
29e4666dfa fix(mobile): asset description is not shown on the sheet when opened for the first time (#10377)
* fix: invalidate asset's description when asset details changed

* refactor(exif-sheet): use description from exif instead

* refactor(asset-description): remove asset_description.provider

* fix(asset-description): set is empty based on exifInfo.description

* chore: rename service to provider
2024-06-17 10:01:02 -07:00
RanKKI
7ce87abc95 fix(mobile): my location button on maps not visible due to bottom padding (#10384)
fix(maps): my location button not visible due to bottom padding
2024-06-17 08:48:58 -07:00
RanKKI
eb987c14c1 fix(mobile): search page (#10385)
* refactor(search): hide people/places if empty

* refactor(search): remove unused stack

* refactor(search): fix dropdown menu's width

* feat(search): show camera make/model vertically on mobile devices

* fix: lint errors
2024-06-17 08:47:04 -07:00
Michel Heusschen
a6e767e46d fix(web): selecting shared link expiration (#10437) 2024-06-17 08:31:11 -07:00
Michel Heusschen
8e373cee8d fix(server): include archived assets in forced thumbnail generation (#10409) 2024-06-16 16:16:02 -04:00
Mert
6b1b5054f8 feat(server): separate face search relation (#10371)
* wip

* various fixes

* new migration

* fix test

* add face search entity, update sql

* update e2e

* set storage to external
2024-06-16 19:25:27 +00:00
RanKKI
0fe152b1ef fix(mobile): translation for title (#10324)
* fix(memory): translation for title

* chore: update memoery translation for dutch

* refactor(translation): avoid incompatibility with i18n website

* fix: lint errors
2024-06-16 15:54:15 +00:00
Mert
e77e87b936 fix(server): orientation handling for person thumbnails (#10382)
fix orientation handling
2024-06-16 08:45:58 -07:00
Michel Heusschen
0b08af7082 fix(web): update avatar color immediately (#10393) 2024-06-16 08:38:32 -07:00
Michel Heusschen
010eb1e0d6 fix(server): include trashed assets in forced thumbnail generation (#10389)
* fix(server): include trashed assets in forced thumbnail generation

* deleted -> trashed
2024-06-16 08:37:51 -07:00
Michel Heusschen
83a851b556 fix(web): play video muted when blocked by browser (#10383) 2024-06-16 08:37:25 -07:00
RanKKI
1cd51cc2de fix(app-bar): remove safe area of the app bar in photos page (#10340)
fix(app-bar): remove safe area of appbar in photos
2024-06-15 13:47:12 -07:00
Michel Heusschen
f3c15c7df8 feat(web): full screen view for duplicates (#10346)
* feat(web): full screen view for duplicates

* styling: make button visibility better

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-15 20:45:20 +00:00
Michel Heusschen
6a5435764e fix(web): allow sending test email when using config file (#10351)
fix(web): send test email when using config file
2024-06-15 12:14:28 -07:00
Michel Heusschen
dfad4f0ff4 fix(web): prevent new uploads from temporarily showing in trash (#10348) 2024-06-15 13:44:18 -04:00
Snowknight26
aea1c46bea feat(web): add cover images to individual shares (#9988)
* feat(web): add cover images to individual shares

* Update wording in share modal

* Use translation function

* Add and use new translations

* Fix formatting

* Update with suggestions

* Update test language

* Update test and language file per suggestions

* Fix formatting

* Remove unused translation
2024-06-14 19:16:48 -04:00
Jason Rasmussen
78f600ebce refactor(server): partner ids (#10321) 2024-06-14 18:29:32 -04:00
Daniel Dietzler
c896fe393f refactor(web): byte unit utils (#10332)
refactor byte unit utils
2024-06-14 17:27:46 +00:00
renovate[bot]
b4b654b53f fix(deps): update dependency exiftool-vendored to v26.2.0 (#10102)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 13:27:12 -04:00
Daniel Dietzler
dddc06c3b2 feat: user preferences for archive download size (#10296)
* feat: user preferences for archive download size

* chore: open api

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-14 11:27:12 -04:00
Matthew Momjian
596412cb8f docs: brief instructions to recover from corruption (#10319)
* brief instructions for corruption

* Update FAQ.mdx
2024-06-14 07:59:33 -05:00
renovate[bot]
e3a314b649 chore(deps): update node.js to eb17a08 (#10098)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 02:34:08 -05:00
renovate[bot]
2bdb4bca9e chore(deps): update dependency flutter to v3.22.2 (#10158)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-14 02:33:27 -05:00
Ben
211451d234 chore(web): standardize settings labels (#10303)
* chore(web): standardize settings labels

- spelling out "max" and "min" in full
- accordions use title case
- labels for settings all use sentence case
- remove the "Enable"/"Enabled"/"ENABLED" titles for toggles, in favor
  of just using the description
- change any gray labels to be immich blue, to match the look and feel
  of the other settings

* chore: update user settings toggle, remove unused "enable" strings
2024-06-14 02:32:41 -05:00
Ronald Cantillo
e1731fe316 fix(doc): literal translation & missing parts (#10295) 2024-06-14 02:31:11 -05:00
renovate[bot]
ee186a40c2 fix(deps): update typescript-projects (#10105)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-13 20:46:26 -04:00
Weblate (bot)
32a0688028 chore(web): update translations (#10285)
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/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translation: Immich/immich

Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: David Anes <david.anes@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Gustavo Ceolin <gustavogiulceolin@hotmail.com>
Co-authored-by: IM Ben <beniiorga@gmail.com>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Julius969 <juliusdjorup@proton.me>
Co-authored-by: Kyle Park <mysky3056@gmail.com>
Co-authored-by: Macgyver <macgyver@users.noreply.hosted.weblate.org>
Co-authored-by: Maks s <smaks2313@gmail.com>
Co-authored-by: Meliox <silent.ftp@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: Napat Srichan <napatsrichan2001@gmail.com>
Co-authored-by: RJS <skudru.rinalds@gmail.com>
Co-authored-by: Samoht11 <thomasa24@gmail.com>
Co-authored-by: Sleeper CH <sleeperch@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: kyu seok Park <tofinders@gmail.com>
Co-authored-by: mxm199 <mxm199@bk.ru>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Владислав Потаенко <vipotaenko02@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-06-14 00:35:49 +00:00
Daniel Dietzler
e5ed7d4af1 chore: update discord links (#10301)
update discord links
2024-06-13 20:27:01 -04:00
William Brockhus
30627fe91e chore: fix typo in jobs-workers.md (#10302) 2024-06-13 19:06:58 -04:00
Jason Rasmussen
77bd162872 fix(server): headers already send (#10289) 2024-06-13 13:30:34 -05:00
Jason Rasmussen
c6ab047167 fix(server): oauth linking error message (#10287) 2024-06-13 11:42:07 -04:00
Alex The Bot
8c2195c820 Version v1.106.4 2024-06-13 15:12:51 +00:00
Zack Pollard
5e99f651ec feat(web): add chinese (traditional), bislama and croatian to our supported languages (#10283)
* feat(web): add chinese (traditional), bislama and croatian to our supported languages

* test: remove language tag tests as it doesn't really test the correctness of tags
2024-06-13 15:00:55 +00:00
Weblate (bot)
0de15121f2 chore(web): update translations (#10224)
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/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/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/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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Adrian <adrian.hundseth@gmail.com>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: Ari <ayhavlin@gmail.com>
Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: Beniamin Iorga <beniiorga@gmail.com>
Co-authored-by: BoBBer446 <eXestend@gmx.de>
Co-authored-by: Daddie0 <33762262+GoByeBye@users.noreply.github.com>
Co-authored-by: David Anes <david.anes@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Erik Mizenak <erikmizenak@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: J1mooo <programingstafi@gmail.com>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Kihoon Kim <kihoon.kim.dev@gmail.com>
Co-authored-by: Kyle Park <mysky3056@gmail.com>
Co-authored-by: Londoneye02 <jcdelcaz@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki M <medolino2009@gmail.com>
Co-authored-by: Nega Duck <negaduck420@gmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Pontus Österlindh <Pompe90@users.noreply.hosted.weblate.org>
Co-authored-by: Ptsa Daniel <ptsa1987@gmail.com>
Co-authored-by: Ryan Gleeson <gleeson.ryanj@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: SisyphusMD <guardian.note2892@fastmail.com>
Co-authored-by: ZHYang <i526842@gmail.com>
Co-authored-by: ZOKOB <remyfrichet@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: buck5060 <buck5060@gmail.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: kyu seok Park <tofinders@gmail.com>
Co-authored-by: mxm199 <mxm199@bk.ru>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: yuuaHP <identity@yuua.dev>
Co-authored-by: Владислав Потаенко <vipotaenko02@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 변준서 <four2mis@gmail.com>
2024-06-13 15:50:05 +01:00
Michel Heusschen
212ba35aef chore(web): translations in page load functions (#10260) 2024-06-13 09:23:52 -05:00
Richard Salame
827ec1b63a chore(doc): update quick-start.mdx (#10276)
Update quick-start.mdx

The following changes are made:

- Changed the headings to capital case.
- Changed a few sentences to sound more clear.
- Removed '?' from the heading as per the standards.
2024-06-13 09:23:02 -05:00
Alex
e2a2c86a31 chore(server): optional originalMimeType in asset response payload (#10272)
* chore(server): optional originalMimeType in asset response payload

* lint

* Update web/src/lib/utils/asset-utils.ts

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>

* fix permission of shared link

* test

* test

* test

* test server

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-13 09:21:47 -05:00
bo0tzz
df31eb1214 chore(docs): Delete unsupported SQL shenanigans (#10278) 2024-06-13 13:58:44 +00:00
Zack Pollard
0d6a4975a3 chore(web): remove unnecessary input.select for lang selector (#10273)
chore: remove unnecessary input.select for lang selector
2024-06-13 12:44:06 +00:00
Zack Pollard
7de2665344 fix(web): more language selector nits (#10271)
* fix: always sort development lang to bottom of list

* fix: clear search query in languages when box is clicked
2024-06-13 12:37:15 +01:00
bo0tzz
058ca28d88 feat(web): Language settings list UX nits (#10261)
* feat(web): Sort language settings list

before: https://bo0.tz/u/xMLnEW.png
after: https://bo0.tz/u/lGLn9h.png

* feat(web): Select combobox text when focused
2024-06-13 06:01:18 -05:00
Min Idzelis
b9593361a4 chore: additional makefile targets (#10243) 2024-06-13 05:58:44 -05:00
Michel Heusschen
a54e01ef2f fix: load original image for gifs (#10252) 2024-06-13 05:57:46 -05:00
Mert
fb641c74be fix(server): use preview image when generating person thumbnail from video (#10240) 2024-06-12 22:16:26 -04:00
Alex
c642150b85 chore(mobile): post release task (#10228) 2024-06-12 14:17:58 -05:00
Alex The Bot
a8a7d29891 Version v1.106.3 2024-06-12 18:26:10 +00:00
Alex
67e98ed313 fix(mobile): video player not updating state (#10220)
* fix(mobile): video player not updating state

* unused code
2024-06-12 12:43:01 -05:00
renovate[bot]
47ef48e3c2 chore(deps): update base-image to v20240611 (major) (#10118)
chore(deps): update base-image to v20240611

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-12 12:51:46 -04:00
waclaw66
376feadb76 fix(web): missing svelte translations (#10199)
* fix(web): missing svelte translations

* fixes

* format fix

* translation keys fix

* "merge" key fix

* Update web/src/lib/components/shared-components/side-bar/more-information-albums.svelte

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* Update web/src/lib/i18n/en.json

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>

* suggestion fix

* trash pluralization

* video+photo count fix

* format fix

* unused removal

* translation key fix

* duplicate key removal

* format fix

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-06-12 17:37:46 +01:00
Jason Rasmussen
3d82005797 fix: no floats (replace with doubles) (#10218)
* fix: no floats (replace with doubles)

* Update server/src/utils/misc.ts

Co-authored-by: Zack Pollard <zackpollard@ymail.com>

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 17:36:24 +01:00
Weblate (bot)
10aa00af21 chore(web): update translations (#10216)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translation: Immich/immich

Co-authored-by: Mario <17320863+myanesp@users.noreply.github.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2024-06-12 17:35:04 +01:00
Zack Pollard
1f8bdcdce7 chore: renovate shouldn't update mobile native dependencies (#10217) 2024-06-12 17:00:54 +01:00
Jason Rasmussen
98ebfc22f8 chore: translations from mobile (#10214) 2024-06-12 15:47:51 +01:00
Weblate (bot)
032b99fe93 chore(web): update translations (#10203)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/it/
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/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alexandr Zhytnyk <oper.kh@gmail.com>
Co-authored-by: Amadeous <am4d3ous@users.noreply.hosted.weblate.org>
Co-authored-by: Beniamin Iorga <beniiorga@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Kim <shnukoms@users.noreply.hosted.weblate.org>
Co-authored-by: Maximilian Waidelich <44324946+maxwai@users.noreply.github.com>
Co-authored-by: Maximilian Waidelich <maximilian.waidelich@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: Yves ANDOLFATTO <register@yves.aleeas.com>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: clementdelestre <clementdelestre@gmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: mgabor <mgabor@users.noreply.hosted.weblate.org>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: wariw <wariwpl@gmail.com>
Co-authored-by: Владислав Потаенко <vipotaenko02@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
2024-06-12 15:38:18 +01:00
Zack Pollard
07156135c2 fix(server): double counting cores when processor name includes the word "processor" (#10211) 2024-06-12 13:49:20 +00:00
Michel Heusschen
9dbf5db72e fix(server): checkExistingAssets (#10192)
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-12 08:48:44 -05:00
Daniel Heppner
52170423be feat(web): select all duplicates (#10189)
* feat(web): select all duplicates

Allows users to select or deselect all duplicate photos when removing duplicates

* styling

* chore(web): add more translations to duplicates page

* color

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 13:01:55 +00:00
Zack Pollard
ae095baad3 fix(server): only run healthchecks when api worker is running on immich-server (#10204)
fix: only run healthchecks when api worker is running on immich-server
2024-06-12 12:44:30 +01:00
Michel Heusschen
f99f289f74 fix(web): small translation issues + remove unused (#10200)
* fix(web): small translation issues + remove unused

* more unused keys

* formatting

* fix(web): incorrectly used translations

* fix and remove unused translations

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-12 12:13:10 +01:00
Alex
476eea44df chore(web): remove thumbnail usage for places card (#10142)
* chore(web): remove thumbnail usage for places

* remove href attribute from Thumbnail

* linting
2024-06-12 11:12:58 +00:00
Jason Rasmussen
e84657192c refactor: config caching (#10168) 2024-06-12 11:07:35 +00:00
Mert
5dda5d93f5 chore(docs): remove microservices from hwa docs (#10188)
remove microservices from hwa docs
2024-06-12 11:57:40 +01:00
Michel Heusschen
6260caf649 fix(web): multi file upload in albums (#10190) 2024-06-12 11:57:11 +01:00
Michel Heusschen
9e5c52b7b7 chore(web): more translations for user settings and admin pages (#10161)
* chore(web): more translations for user settings and admin pages

* JobSettings translations

* feedback

* missed one

* feedback
2024-06-12 11:54:40 +01:00
Weblate (bot)
0e1311e3d3 chore(web): update translations (#10152)
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/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
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/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/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/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: 94tiger <94tiger@naver.com>
Co-authored-by: Adrian <adrian.hundseth@gmail.com>
Co-authored-by: Andrej Kralj <andrej.kralj@gmail.com>
Co-authored-by: AngelaDMerkel <personal@caduffy.com>
Co-authored-by: Anton <ajp_anton@hotmail.com>
Co-authored-by: Beniamin Iorga <beniiorga@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Flowake <weblate.cx6on@passmail.net>
Co-authored-by: Immich <immich@futo.org>
Co-authored-by: Jakub <jakubula.jm@gmail.com>
Co-authored-by: Jan <account@thebraker.net>
Co-authored-by: Jan <jan.widmer.ch@gmail.com>
Co-authored-by: Jason Dean Lessenich <jasonlessenich@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: Julien Deveaux <julien.deveaux@hotmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Kim <shnukoms@users.noreply.hosted.weblate.org>
Co-authored-by: Kyle Park <mysky3056@gmail.com>
Co-authored-by: League2EB <info@league2eb.me>
Co-authored-by: Londoneye02 <jcdelcaz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Marcos Besteiro López (MarcosBL) <marcosbl@gmail.com>
Co-authored-by: MeisterEder286 <walbrun.johann@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miko-Matias Grönvall <matias.gronvall@gmail.com>
Co-authored-by: MozPri <primoz.arh@gmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Ole Morten Didriksen <code@oledid.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Ptsa Daniel <ptsa1987@gmail.com>
Co-authored-by: RWDai <869759838@qq.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: SisyphusMD <guardian.note2892@fastmail.com>
Co-authored-by: Smiehoo <github@pocz.net>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: Tomas Babej <web+weblate@tbabej.com>
Co-authored-by: Tomek <tjomek@gmail.com>
Co-authored-by: VB <Victor2B@protonmail.com>
Co-authored-by: Vojtěch Bargl <bargl.vojtech@gmail.com>
Co-authored-by: YFrendo <yann.frendo@live.fr>
Co-authored-by: Yves ANDOLFATTO <register@yves.aleeas.com>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: biglate <bigtech+weblate@aleeas.com>
Co-authored-by: carcawey <dacarva@gmail.com>
Co-authored-by: clementdelestre <clementdelestre@gmail.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: ferrets <ferrets@live.cn>
Co-authored-by: frauhottelmann <frauhottelmann@gmail.com>
Co-authored-by: gilo <giantlolli@proton.me>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: ingria <codefuhrer@gmail.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: myurar1a <sirometroid1235@outlook.jp>
Co-authored-by: sephrat <florian.dupret@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Кирилл Москатов <kirillmoskatov@gmail.com>
2024-06-12 11:52:33 +01:00
Stephen Smith
216cca4383 fix(server): exiftool largefilesupport only set for the first call (#10167)
* Revert "feat(server): enable exiftool largefilesupport (#9894)"

This reverts commit afa10ebcb2.

* feat(server): enable exiftool largefilesupport by passing options to read
2024-06-12 05:43:38 -05:00
Mert
cdc98de848 fix(server): increase pixel limit for thumbnail generation (#10181)
disable input limit
2024-06-11 22:11:03 -04:00
Mert
126cbeabe8 feat(server): add av1 support for vaapi (#10180)
add av1
2024-06-12 00:24:06 +00:00
Mert
2e0c6f6fff fix: postgres health check reporting any db without checksums as unhealthy (#10178)
handle disabled checksumming
2024-06-12 00:18:24 +00:00
698 changed files with 52966 additions and 25648 deletions

View File

@@ -1,11 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: I have a question or need support
url: https://discord.gg/D8JsnBEuKb
url: https://discord.immich.app
about: We use GitHub for tracking bugs, please check out our Discord channel for freaky fast support.
- name: Feature Request
url: https://github.com/immich-app/immich/discussions/new?category=feature-request
about: Please use our GitHub Discussion for making feature requests.
- name: I'm unsure where to go
url: https://discord.gg/D8JsnBEuKb
url: https://discord.immich.app
about: If you are unsure where to go, then joining our Discord is recommended; Just ask!

36
.github/labeler.yml vendored
View File

@@ -1,23 +1,35 @@
cli:
- changed-files:
- any-glob-to-any-file: cli/**
- changed-files:
- any-glob-to-any-file:
- cli/src/**
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
- changed-files:
- any-glob-to-any-file:
- docs/blob/**
- docs/docs/**
- docs/src/**
- docs/static/**
🖥web:
- changed-files:
- any-glob-to-any-file: web/**
- changed-files:
- any-glob-to-any-file:
- web/src/**
- web/static/**
📱mobile:
- changed-files:
- any-glob-to-any-file: mobile/**
- changed-files:
- any-glob-to-any-file:
- mobile/lib/**
- mobile/test/**
🗄server:
- changed-files:
- any-glob-to-any-file: server/**
- changed-files:
- any-glob-to-any-file:
- server/src/**
- server/test/**
🧠machine-learning:
- changed-files:
- any-glob-to-any-file: machine-learning/**
- changed-files:
- any-glob-to-any-file:
- machine-learning/app/**

View File

@@ -33,7 +33,7 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
- name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/
@@ -56,10 +56,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.4.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v5.4.0
uses: docker/build-push-action@v6.3.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@@ -63,10 +63,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3.1.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.3.0
uses: docker/setup-buildx-action@v3.4.0
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
@@ -115,7 +115,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v5.4.0
uses: docker/build-push-action@v6.3.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
@@ -124,7 +124,11 @@ jobs:
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
build-args: |
DEVICE=${{ matrix.device }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}

View File

@@ -26,6 +26,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './docs/.nvmrc'
- name: Run npm install
run: npm ci

View File

@@ -19,7 +19,7 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: '20.x'
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
- name: Install deps
run: npm ci

View File

@@ -21,6 +21,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './server/.nvmrc'
- name: Run npm install
run: npm ci
@@ -54,7 +59,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version-file: './cli/.nvmrc'
- name: Setup typescript-sdk
run: npm ci && npm run build
@@ -79,6 +84,38 @@ jobs:
run: npm run test:cov
if: ${{ !cancelled() }}
cli-unit-tests-win:
name: CLI (Windows)
runs-on: windows-latest
defaults:
run:
working-directory: ./cli
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './cli/.nvmrc'
- name: Setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: npm ci
# Skip linter & formatter in Windows test.
- name: Run tsc
run: npm run check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: npm run test:cov
if: ${{ !cancelled() }}
web-unit-tests:
name: Web
runs-on: ubuntu-latest
@@ -90,6 +127,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './web/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
@@ -133,7 +175,7 @@ jobs:
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: 20
node-version-file: './e2e/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
@@ -241,6 +283,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './server/.nvmrc'
- name: Install server dependencies
run: npm --prefix=server ci
@@ -291,6 +338,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './server/.nvmrc'
- name: Install server dependencies
run: npm ci

View File

@@ -131,4 +131,4 @@ conduct enforcement ladder](https://github.com/mozilla/diversity).
For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are
available at https://www.contributor-covenant.org/translations.
available at https://www.contributor-covenant.org/translations.

View File

@@ -35,3 +35,51 @@ sql:
attach-server:
docker exec -it docker_immich-server_1 sh
renovate:
LOG_LEVEL=debug npx renovate --platform=local --repository-cache=reset
MODULES = e2e server web cli sdk
audit-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) audit fix
install-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) i
build-cli: build-sdk
build-web: build-sdk
build-%: install-%
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'build' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run build || true
format-%:
npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run | grep 'format:fix' >/dev/null \
&& npm --prefix $(subst sdk,open-api/typescript-sdk,$*) run format:fix || true
lint-%:
npm --prefix $* run lint:fix
check-%:
npm --prefix $* run check
check-web:
npm --prefix web run check:typescript
npm --prefix web run check:svelte
test-%:
npm --prefix $* run test
test-e2e:
docker compose -f ./e2e/docker-compose.yml build
npm --prefix e2e run test
npm --prefix e2e run test:web
build-all: $(foreach M,$(MODULES),build-$M) ;
install-all: $(foreach M,$(MODULES),install-$M) ;
check-all: $(foreach M,$(MODULES),check-$M) ;
lint-all: $(foreach M,$(MODULES),lint-$M) ;
format-all: $(foreach M,$(MODULES),format-$M) ;
audit-all: $(foreach M,$(MODULES),audit-$M) ;
hygiene-all: lint-all format-all check-all sql audit-all;
test-all: $(foreach M,$(MODULES),test-$M) ;
clean:
find . -name "node_modules" -type d -prune -exec rm -rf '{}' +
find . -name "dist" -type d -prune -exec rm -rf '{}' +
find . -name "build" -type d -prune -exec rm -rf '{}' +
find . -name "svelte-kit" -type d -prune -exec rm -rf '{}' +
docker compose -f ./docker/docker-compose.dev.yml rm -v -f || true
docker compose -f ./e2e/docker-compose.yml rm -v -f || true

View File

@@ -1,7 +1,7 @@
<p align="center">
<br/>
<a href="https://opensource.org/license/agpl-v3"><img src="https://img.shields.io/badge/License-AGPL_v3-blue.svg?color=3F51B5&style=for-the-badge&label=License&logoColor=000000&labelColor=ececec" alt="License: AGPLv3"></a>
<a href="https://discord.gg/D8JsnBEuKb">
<a href="https://discord.immich.app">
<img src="https://img.shields.io/discord/979116623879368755.svg?label=Discord&logo=Discord&style=for-the-badge&logoColor=000000&labelColor=ececec" alt="Discord"/>
</a>
<br/>
@@ -19,20 +19,21 @@
<br/>
<p align="center">
<a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_es_ES.md">Español</a>
<a href="readme_i18n/README_fr_FR.md">Français</a>
<a href="readme_i18n/README_it_IT.md">Italiano</a>
<a href="readme_i18n/README_ja_JP.md">日本語</a>
<a href="readme_i18n/README_ko_KR.md">한국어</a>
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
<a href="readme_i18n/README_ca_ES.md">Català</a>
<a href="readme_i18n/README_es_ES.md">Español</a>
<a href="readme_i18n/README_fr_FR.md">Français</a>
<a href="readme_i18n/README_it_IT.md">Italiano</a>
<a href="readme_i18n/README_ja_JP.md">日本語</a>
<a href="readme_i18n/README_ko_KR.md">한국어</a>
<a href="readme_i18n/README_de_DE.md">Deutsch</a>
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>
<a href="readme_i18n/README_ar_JO.md">العربية</a>
</p>
## Disclaimer
@@ -42,45 +43,36 @@
- ⚠️ **Do not use the app as the only way to store your photos and videos.**
- ⚠️ Always follow [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan for your precious photos and videos!
## Content
> [!NOTE]
> You can find the main documentation, including installation guides, at https://immich.app/.
- [Official Documentation](https://immich.app/docs)
- [Roadmap](https://github.com/orgs/immich-app/projects/1)
## Links
- [Documentation](https://immich.app/docs)
- [About](https://immich.app/docs/overview/introduction)
- [Installation](https://immich.app/docs/install/requirements)
- [Roadmap](https://immich.app/roadmap)
- [Demo](#demo)
- [Features](#features)
- [Introduction](https://immich.app/docs/overview/introduction)
- [Installation](https://immich.app/docs/install/requirements)
- [Contribution Guidelines](https://immich.app/docs/overview/support-the-project)
## Documentation
You can find the main documentation, including installation guides, at https://immich.app/.
- [Translations](https://immich.app/docs/developer/translations)
- [Contributing](https://immich.app/docs/overview/support-the-project)
## Demo
You can access the web demo at https://demo.immich.app
Access the demo [here](https://demo.immich.app). The demo is running on a Free-tier Oracle VM in Amsterdam with a 2.4Ghz quad-core ARM64 CPU and 24GB RAM.
For the mobile app, you can use `https://demo.immich.app/api` for the `Server Endpoint URL`
```bash title="Demo Credential"
The credential
email: demo@immich.app
password: demo
```
### Login credentials
```
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## Activities
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
| Email | Password |
| --------------- | -------- |
| demo@immich.app | demo |
## Features
| Features | Mobile | Web |
| :--------------------------------------------- | -------- | ----- |
| :------------------------------------------- | ------ | --- |
| Upload and view videos and photos | Yes | Yes |
| Auto backup when the app is opened | Yes | N/A |
| Prevent duplication of assets | Yes | Yes |
@@ -110,13 +102,19 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Read-only gallery | Yes | Yes |
| Stacked Photos | Yes | Yes |
## Contributors
## Translations
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
Read more about translations [here](https://immich.app/docs/developer/translations).
<a href="https://hosted.weblate.org/engage/immich/">
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
</a>
## Star History
## Repository activity
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Star history
<a href="https://star-history.com/#immich-app/immich&Date">
<picture>
@@ -125,3 +123,9 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture>
</a>
## Contributors
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>

View File

@@ -2,4 +2,4 @@
## Reporting a Vulnerability
Please report security issues to `alex.tran1502@gmail.com`
Please report security issues to `security@immich.app`

View File

@@ -1 +1 @@
20.14
20.15.1

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core
FROM node:20.15.1-alpine3.20@sha256:34b7aa411056c85dbf71d240d26516949b3f72b318d796c26b57caaa1df5639a as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
@@ -16,4 +16,4 @@ RUN npm run build
WORKDIR /import
ENTRYPOINT ["node", "/usr/src/app/dist"]
ENTRYPOINT ["node", "/usr/src/app/dist"]

492
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.2",
"version": "2.2.8",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -18,7 +18,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1",
"@types/node": "^20.14.10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
@@ -28,14 +28,15 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1"
},
"scripts": {
@@ -59,9 +60,10 @@
},
"dependencies": {
"fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.14.0"
"node": "20.15.1"
}
}

View File

@@ -0,0 +1,201 @@
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock';
import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
vi.mock('@immich/sdk');
describe('getAlbumName', () => {
it('should return a non-undefined value', () => {
if (os.platform() === 'win32') {
// This is meaningless for Unix systems.
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
}
expect(getAlbumName('D:/parentfolder/test/Filename.txt', {} as UploadOptionsDto)).toBe('test');
});
it('has higher priority to return `albumName` in `options`', () => {
expect(getAlbumName('/parentfolder/test/Filename.txt', { albumName: 'example' } as UploadOptionsDto)).toBe(
'example',
);
});
});
describe('uploadFiles', () => {
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const testFilePath = path.join(testDir, 'test.png');
const testFileData = 'test';
const baseUrl = 'http://example.com';
const apiKey = 'key';
const retry = 3;
const fetchMocker = createFetchMock(vi);
beforeEach(() => {
// Create a test file
fs.writeFileSync(testFilePath, testFileData);
// Defaults
vi.mocked(defaults).baseUrl = baseUrl;
vi.mocked(defaults).headers = { 'x-api-key': apiKey };
fetchMocker.enableMocks();
fetchMocker.resetMocks();
});
it('returns new assets when upload file is successful', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
};
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
]);
});
it('returns new assets when upload file retry is successful', async () => {
let counter = 0;
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
counter++;
if (counter < retry) {
throw new Error('Network error');
}
return {
status: 200,
body: JSON.stringify({ id: 'fc5621b1-86f6-44a1-9905-403e607df9f5', status: 'created' }),
};
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
]);
});
it('returns new assets when upload file retry is failed', async () => {
fetchMocker.doMockIf(new RegExp(`${baseUrl}/assets$`), () => {
throw new Error('Network error');
});
await expect(uploadFiles([testFilePath], { concurrency: 1 })).resolves.toEqual([]);
});
});
describe('checkForDuplicates', () => {
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
const testFilePath = path.join(testDir, 'test.png');
const testFileData = 'test';
const testFileChecksum = 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3'; // SHA1
const retry = 3;
beforeEach(() => {
// Create a test file
fs.writeFileSync(testFilePath, testFileData);
});
it('checks duplicates', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await checkForDuplicates([testFilePath], { concurrency: 1 });
expect(checkBulkUpload).toHaveBeenCalledWith({
assetBulkUploadCheckDto: {
assets: [
{
checksum: testFileChecksum,
id: testFilePath,
},
],
},
});
});
it('returns duplicates when check duplicates is rejected', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Reject,
id: testFilePath,
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
reason: Reason.Duplicate,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [
{
filepath: testFilePath,
id: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
},
],
newFiles: [],
});
});
it('returns new assets when check duplicates is accepted', async () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [testFilePath],
});
});
it('returns results when check duplicates retry is successful', async () => {
let mocked = vi.mocked(checkBulkUpload);
for (let i = 1; i < retry; i++) {
mocked = mocked.mockRejectedValueOnce(new Error('Network error'));
}
mocked.mockResolvedValue({
results: [
{
action: Action.Accept,
id: testFilePath,
},
],
});
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [testFilePath],
});
});
it('returns results when check duplicates retry is failed', async () => {
vi.mocked(checkBulkUpload).mockRejectedValue(new Error('Network error'));
await expect(checkForDuplicates([testFilePath], { concurrency: 1 })).resolves.toEqual({
duplicates: [],
newFiles: [],
});
});
});

View File

@@ -15,8 +15,8 @@ import { Presets, SingleBar } from 'cli-progress';
import { chunk } from 'lodash-es';
import { Stats, createReadStream } from 'node:fs';
import { stat, unlink } from 'node:fs/promises';
import os from 'node:os';
import path, { basename } from 'node:path';
import { Queue } from 'src/queue';
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
const s = (count: number) => (count === 1 ? '' : 's');
@@ -25,7 +25,7 @@ const s = (count: number) => (count === 1 ? '' : 's');
type AssetBulkUploadCheckResults = Array<AssetBulkUploadCheckResult & { id: string }>;
type Asset = { id: string; filepath: string };
interface UploadOptionsDto {
export interface UploadOptionsDto {
recursive?: boolean;
ignore?: string;
dryRun?: boolean;
@@ -84,7 +84,7 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
return files;
};
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
if (skipHash) {
console.log('Skipping hash check, assuming all files are new');
return { newFiles: files, duplicates: [] };
@@ -100,32 +100,50 @@ const checkForDuplicates = async (files: string[], { concurrency, skipHash }: Up
const newFiles: string[] = [];
const duplicates: Asset[] = [];
try {
// TODO refactor into a queue
for (const items of chunk(files, concurrency)) {
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })));
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) {
const queue = new Queue<string[], AssetBulkUploadCheckResults>(
async (filepaths: string[]) => {
const dto = await Promise.all(
filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
);
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
const results = response.results as AssetBulkUploadCheckResults;
for (const { id: filepath, assetId, action } of results) {
if (action === Action.Accept) {
newFiles.push(filepath);
} else {
// rejects are always duplicates
duplicates.push({ id: assetId as string, filepath });
}
progressBar.increment();
}
}
} finally {
progressBar.stop();
progressBar.increment(filepaths.length);
return results;
},
{ concurrency, retry: 3 },
);
for (const items of chunk(files, concurrency)) {
await queue.push(items);
}
await queue.drained();
progressBar.stop();
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
// Report failures
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
if (failedTasks.length > 0) {
console.log(`Failed to verify ${failedTasks.length} file${s(failedTasks.length)}:`);
for (const task of failedTasks) {
console.log(`- ${task.data} - ${task.error}`);
}
}
return { newFiles, duplicates };
};
const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.');
return [];
@@ -159,37 +177,52 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
const newAssets: Asset[] = [];
try {
for (const items of chunk(files, concurrency)) {
await Promise.all(
items.map(async (filepath) => {
const stats = statsMap.get(filepath) as Stats;
const response = await uploadFile(filepath, stats);
const queue = new Queue<string, AssetMediaResponseDto>(
async (filepath: string) => {
const stats = statsMap.get(filepath);
if (!stats) {
throw new Error(`Stats not found for ${filepath}`);
}
newAssets.push({ id: response.id, filepath });
const response = await uploadFile(filepath, stats);
newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) {
duplicateCount++;
duplicateSize += stats.size ?? 0;
} else {
successCount++;
successSize += stats.size ?? 0;
}
if (response.status === AssetMediaStatus.Duplicate) {
duplicateCount++;
duplicateSize += stats.size ?? 0;
} else {
successCount++;
successSize += stats.size ?? 0;
}
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
return response;
},
{ concurrency, retry: 3 },
);
return response;
}),
);
}
} finally {
uploadProgress.stop();
for (const filepath of files) {
await queue.push(filepath);
}
await queue.drained();
uploadProgress.stop();
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
if (duplicateCount > 0) {
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`);
}
// Report failures
const failedTasks = queue.tasks.filter((task) => task.status === 'failed');
if (failedTasks.length > 0) {
console.log(`Failed to upload ${failedTasks.length} asset${s(failedTasks.length)}:`);
for (const task of failedTasks) {
console.log(`- ${task.data} - ${task.error}`);
}
}
return newAssets;
};
@@ -346,7 +379,9 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
}
};
const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
const folderName = os.platform() === 'win32' ? filepath.split('\\').at(-2) : filepath.split('/').at(-2);
return options.albumName ?? folderName;
// `filepath` valid format:
// - Windows: `D:\\test\\Filename.txt` or `D:/test/Filename.txt`
// - Unix: `/test/Filename.txt`
export const getAlbumName = (filepath: string, options: UploadOptionsDto) => {
return options.albumName ?? path.basename(path.dirname(filepath));
};

131
cli/src/queue.ts Normal file
View File

@@ -0,0 +1,131 @@
import * as fastq from 'fastq';
import { uniqueId } from 'lodash-es';
export type Task<T, R> = {
readonly id: string;
status: 'idle' | 'processing' | 'succeeded' | 'failed';
data: T;
error: unknown | undefined;
count: number;
// TODO: Could be useful to adding progress property.
// TODO: Could be useful to adding start_at/end_at/duration properties.
result: undefined | R;
};
export type QueueOptions = {
verbose?: boolean;
concurrency?: number;
retry?: number;
// TODO: Could be useful to adding timeout property for retry.
};
export type ComputedQueueOptions = Required<QueueOptions>;
export const defaultQueueOptions = {
concurrency: 1,
retry: 0,
verbose: false,
};
/**
* An in-memory queue that processes tasks in parallel with a given concurrency.
* @see {@link https://www.npmjs.com/package/fastq}
* @template T - The type of the worker task data.
* @template R - The type of the worker output data.
*/
export class Queue<T, R> {
private readonly queue: fastq.queueAsPromised<string, Task<T, R>>;
private readonly store = new Map<string, Task<T, R>>();
readonly options: ComputedQueueOptions;
readonly worker: (data: T) => Promise<R>;
/**
* Create a new queue.
* @param worker - The worker function that processes the task.
* @param options - The queue options.
*/
constructor(worker: (data: T) => Promise<R>, options?: QueueOptions) {
this.options = { ...defaultQueueOptions, ...options };
this.worker = worker;
this.store = new Map<string, Task<T, R>>();
this.queue = this.buildQueue();
}
get tasks(): Task<T, R>[] {
const tasks: Task<T, R>[] = [];
for (const task of this.store.values()) {
tasks.push(task);
}
return tasks;
}
getTask(id: string): Task<T, R> {
const task = this.store.get(id);
if (!task) {
throw new Error(`Task with id ${id} not found`);
}
return task;
}
/**
* Wait for the queue to be empty.
* @returns Promise<void> - The returned Promise will be resolved when all tasks in the queue have been processed by a worker.
* This promise could be ignored as it will not lead to a `unhandledRejection`.
*/
async drained(): Promise<void> {
await this.queue.drain();
}
/**
* Add a task at the end of the queue.
* @see {@link https://www.npmjs.com/package/fastq}
* @param data
* @returns Promise<void> - A Promise that will be fulfilled (rejected) when the task is completed successfully (unsuccessfully).
* This promise could be ignored as it will not lead to a `unhandledRejection`.
*/
async push(data: T): Promise<Task<T, R>> {
const id = uniqueId();
const task: Task<T, R> = { id, status: 'idle', error: undefined, count: 0, data, result: undefined };
this.store.set(id, task);
return this.queue.push(id);
}
// TODO: Support more function delegation to fastq.
private buildQueue(): fastq.queueAsPromised<string, Task<T, R>> {
return fastq.promise((id: string) => {
const task = this.getTask(id);
return this.work(task);
}, this.options.concurrency);
}
private async work(task: Task<T, R>): Promise<Task<T, R>> {
task.count += 1;
task.error = undefined;
task.status = 'processing';
if (this.options.verbose) {
console.log('[task] processing:', task);
}
try {
task.result = await this.worker(task.data);
task.status = 'succeeded';
if (this.options.verbose) {
console.log('[task] succeeded:', task);
}
return task;
} catch (error) {
task.error = error;
task.status = 'failed';
if (this.options.verbose) {
console.log('[task] failed:', task);
}
if (this.options.retry > 0 && task.count < this.options.retry) {
if (this.options.verbose) {
console.log('[task] retry:', task);
}
return this.work(task);
}
return task;
}
}
}

View File

@@ -1,4 +1,5 @@
import mockfs from 'mock-fs';
import { readFileSync } from 'node:fs';
import { CrawlOptions, crawl } from 'src/utils';
interface Test {
@@ -9,6 +10,10 @@ interface Test {
const cwd = process.cwd();
const readContent = (path: string) => {
return readFileSync(path).toString();
};
const extensions = [
'.jpg',
'.jpeg',
@@ -256,7 +261,8 @@ const tests: Test[] = [
{
test: 'should support ignoring absolute paths',
options: {
pathsToCrawl: ['/'],
// Currently, fast-glob has some caveat when dealing with `/`.
pathsToCrawl: ['/*s'],
recursive: true,
exclusionPattern: '/images/**',
},
@@ -276,14 +282,16 @@ describe('crawl', () => {
describe('crawl', () => {
for (const { test, options, files } of tests) {
it(test, async () => {
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, ''])));
// The file contents is the same as the path.
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file])));
const actual = await crawl({ ...options, extensions });
const expected = Object.entries(files)
.filter((entry) => entry[1])
.map(([file]) => file);
expect(actual.sort()).toEqual(expected.sort());
// Compare file's content instead of path since a file can be represent in multiple ways.
expect(actual.map((path) => readContent(path)).sort()).toEqual(expected.sort());
});
}
});

View File

@@ -1,8 +1,9 @@
import { getMyUser, init, isHttpError } from '@immich/sdk';
import { glob } from 'fast-glob';
import { convertPathToPattern, glob } from 'fast-glob';
import { createHash } from 'node:crypto';
import { createReadStream } from 'node:fs';
import { readFile, stat, writeFile } from 'node:fs/promises';
import { platform } from 'node:os';
import { join, resolve } from 'node:path';
import yaml from 'yaml';
@@ -106,6 +107,11 @@ export interface CrawlOptions {
exclusionPattern?: string;
extensions: string[];
}
const convertPathToPatternOnWin = (path: string) => {
return platform() === 'win32' ? convertPathToPattern(path) : path;
};
export const crawl = async (options: CrawlOptions): Promise<string[]> => {
const { extensions: extensionsWithPeriod, recursive, pathsToCrawl, exclusionPattern, includeHidden } = options;
const extensions = extensionsWithPeriod.map((extension) => extension.replace('.', ''));
@@ -124,11 +130,11 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
if (stats.isFile() || stats.isSymbolicLink()) {
crawledFiles.push(absolutePath);
} else {
patterns.push(absolutePath);
patterns.push(convertPathToPatternOnWin(absolutePath));
}
} catch (error: any) {
if (error.code === 'ENOENT') {
patterns.push(currentPath);
patterns.push(convertPathToPatternOnWin(currentPath));
} else {
throw error;
}

View File

@@ -2,6 +2,7 @@ import { defineConfig } from 'vite';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
resolve: { alias: { src: '/src' } },
build: {
rollupOptions: {
input: 'src/index.ts',

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
version = "4.36.0"
constraints = "4.36.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"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",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3",
]
}

View File

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

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
version = "4.36.0"
constraints = "4.36.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"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",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3",
]
}

View File

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

View File

@@ -26,6 +26,16 @@ services:
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
environment:
IMMICH_REPOSITORY: immich-app/immich
IMMICH_REPOSITORY_URL: https://github.com/immich-app/immich
IMMICH_SOURCE_REF: local
IMMICH_SOURCE_COMMIT: af2efbdbbddc27cd06142f22253ccbbbbeec1f55
IMMICH_SOURCE_URL: https://github.com/immich-app/immich/commit/af2efbdbbddc27cd06142f22253ccbbbbeec1f55
IMMICH_BUILD: '9654404849'
IMMICH_BUILD_URL: https://github.com/immich-app/immich/actions/runs/9654404849
IMMICH_BUILD_IMAGE: development
IMMICH_BUILD_IMAGE_URL: https://github.com/immich-app/immich/pkgs/container/immich-server
ulimits:
nofile:
soft: 1048576
@@ -84,7 +94,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
healthcheck:
test: redis-cli ping || exit 1
@@ -103,11 +113,26 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
command:
[
'postgres',
'-c',
'shared_preload_libraries=vectors.so',
'-c',
'search_path="$$user", public, vectors',
'-c',
'logging_collector=on',
'-c',
'max_wal_size=2GB',
'-c',
'shared_buffers=512MB',
'-c',
'wal_compression=on',
]
# set IMMICH_METRICS=true in .env to enable metrics
# immich-prometheus:

View File

@@ -41,7 +41,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -61,7 +61,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
@@ -73,7 +73,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:5c435642ca4d8427ca26f4901c11114023004709037880cd7860d5b7176aa731
image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -85,7 +85,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.0.0-ubuntu@sha256:dcd3ae78713958a862732c3608d32c03f0c279c35a2032d74b80b12c5cdc47b8
image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -43,7 +43,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
image: docker.io/redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -59,7 +59,7 @@ services:
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m

View File

@@ -43,7 +43,7 @@ if [ -n "${quota:-}" ] && [ -n "${period:-}" ]; then
cpus=1
fi
else
cpus=$(grep -c processor /proc/cpuinfo)
cpus=$(grep -c ^processor /proc/cpuinfo)
fi
echo "$cpus"

View File

@@ -1 +1 @@
20.14
20.15.1

View File

@@ -94,7 +94,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Cheer!

View File

@@ -142,7 +142,7 @@ Thank you, and I am asking for your support for the project. I hope to be a full
- Bitcoin: 3QVAb9dCHutquVejeNXitPqZX26Yg5kxb7
- Give a project a star - the contributors love gazing at the stars and seeing their creations shining in the sky.
Join our friendly [Discord](https://discord.gg/D8JsnBEuKb) to talk and discuss Immich, tech, or anything
Join our friendly [Discord](https://discord.immich.app) to talk and discuss Immich, tech, or anything
Cheer!

View File

@@ -0,0 +1,77 @@
---
title: Immich Update - July 2024
authors: [alextran]
tags: [update, v1.106.0]
---
Hello everybody! Alex from Immich here and I am back with another development progress update for the project.
Summer has returned once again, and the night sky is filled with stars, thank you for **38_000 shining stars** you have sent to our [GitHub repo](https://github.com/immich-app/immich)! Since the last announcement several core contributors have started full time. Everything is going great with development, PRs get merged with _brrrrrrr_ rate, conversation exchange between team members is on a new high, we met and are working with the great engineers at FUTO. The spirit is high and we have a lot of things brewing that we think you will like.
Let's go over some of the updates we had since the last post.
### Container consolidation
Reduced the number of total containers from 5 to 4 by making the microservices thread get spawned directly in the server container. Woohoo, remember when Immich had 7 containers?
### Email notifications
![smtp](https://github.com/immich-app/immich/assets/27055614/949cba85-d3f1-4cd3-b246-a6f5fb5d3ae8)
We added email notifications to the app with SMTP settings that you can configure for the following events
- A new account is created for you.
- You are added to a shared album.
- New media is added to an album.
### Versioned docs
You can now jump back into the past or take a peek at the unreleased version of the documentation by selecting the version on the website.
![version-doc](https://github.com/immich-app/immich/assets/27055614/6d22898a-5093-41ad-b416-4573d7ce6e03)
### Similarity deduplication
With more machine learning and CLIP magic, we now have similarity deduplication built into the application where it will search for closely similar images and let you decide what to do with them; i.e keep or trash.
![similarity-deduplication](https://github.com/immich-app/immich/assets/27055614/3cac8478-fbf7-47ea-acb6-0146901dc67e)
### Permanent URL for asset on the web
The detail view for an asset now has a permanent URL so you can easily share them with your loved ones.
### Web app translations
We now have a public Weblate project which the community can use to translate the webapp to their native languages. We are planning to port the mobile app translation to this platform as well. If you would like to contribute, you can take a look [here](https://hosted.weblate.org/projects/immich/immich/). We're already close to 50% translations -- we really appreciate everyone contributing to that!
![web-translation](https://github.com/immich-app/immich/assets/27055614/363df2ed-656c-4584-bd82-0708a693c5bc)
### Read-only/Editor mode on shared album
As the owner of the album, you can choose if the shared user can edit the album or to only view the content of the album without any modification.
![read-only-album](https://github.com/immich-app/immich/assets/27055614/c6f66375-b869-495a-9a86-3e87b316d109)
### Better video thumbnails
Immich now tries to find a descriptive video thumbnail instead of simply using the first frame. No more black images for thumbnails!
### Public Roadmap
We now have a [public roadmap](https://immich.app/roadmap), giving you a high-level overview of things the team is working on. The first goal of this roadmap is to bring Immich to a stable release, which is expected sometime later this year. Some of the highlights include
- Auto stacking - Auto stacking of burst photos
- Basic editor - Basic photo editing capabilities
- Workflows - Automate tasks with workflows
- Fine grained access controls - Granular access controls for users and api keys
- Better background backups - Rework background backups to be more reliable
- Private/locked photos - Private assets with extra protections
Beyond the items in the roadmap, we have _many many_ more ideas for Immich. The team and I hope that you are enjoying the application, find it helpful in your life and we have nothing but the intention of building out great software for you all!
Have an amazing Summer or Winter for those in the southern hemisphere! :D
Until next time,
Cheers!
Alex

View File

@@ -133,40 +133,6 @@ For example, say you have existing transcodes with the policy "Videos higher tha
No. Our design principle is that the original assets should always be untouched.
### How can I move all data (photos, persons, albums, libraries) from one user to another?
This is not officially supported but can be accomplished with some database updates. You can do this on the command line (in the PostgreSQL container using the `psql` command), or you can add, for example, an [Adminer](https://www.adminer.org/) container to the `docker-compose.yml` file so that you can use a web interface.
<details>
<summary>Steps</summary>
1. **MAKE A BACKUP** - See [backup and restore](/docs/administration/backup-and-restore.md).
2. Find the ID of both the 'source' and the 'destination' user (it's the id column in the `users` table)
3. Four tables need to be updated:
```sql
BEGIN;
-- reassign albums
UPDATE albums SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
-- reassign people
UPDATE person SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
-- reassign assets
UPDATE assets SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>'
AND CHECKSUM NOT IN (SELECT CHECKSUM FROM assets WHERE "ownerId" = '<destinationId>');
-- reassign external libraries
UPDATE libraries SET "ownerId" = '<destinationId>' WHERE "ownerId" = '<sourceId>';
COMMIT;
```
4. There might be left-over assets in the 'source' user's library if they are skipped by the last query because of duplicate checksums. These are probably duplicates anyway, and can probably be removed.
</details>
---
## Albums
@@ -442,4 +408,11 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details>
If corruption is detected, you should immediately make a backup before performing any other work in the database.
To do so, you may need to set the `zero_damaged_pages=on` flag for the database server to allow `pg_dumpall` to succeed.
After taking a backup, the recommended next step is to restore the database from a healthy backup before corruption was detected.
The damaged database dump can be used to manually recover any changes made since the last backup, if needed.
The causes of possible corruption are many, but can include unexpected poweroffs or unmounts, use of a network share for Postgres data, or a poor storage medium such an SD card or failing HDD/SSD.
[huggingface]: https://huggingface.co/immich-app

View File

@@ -76,6 +76,7 @@ services:
backup:
container_name: immich_db_dumper
image: prodrigestivill/postgres-backup-local:14
restart: always
env_file:
- .env
environment:
@@ -191,6 +192,6 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
</Tabs>
:::danger
Do not touch the files inside these folders under any circumstances except taking a backup, changing or removing an asset can cause untracked and missing files.
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
:::

View File

@@ -27,7 +27,7 @@ Copy the entire `immich-server` block as a new service and make the following ch
+ container_name: immich_microservices
```
Once you have two copies of the immich-server service, make the following chnages to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
Once you have two copies of the immich-server service, make the following changes to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
```diff
services:

View File

@@ -0,0 +1,21 @@
# Translations
:::tip
You can request a new language [here](https://hosted.weblate.org/new-lang/immich/immich/).
:::
## Weblate
[Weblate](https://weblate.org/) is a "libre software web-based continuous localization system". Immich localization efforts are managed on their [hosted platform](https://hosted.weblate.org/projects/immich/immich/).
## International message format
Plurals, numbers, dates and other locale specific message formats can be handled by using the [ICU message format](https://unicode-org.github.io/icu/userguide/format_parse/messages/). Internally, this is handled by the [intl-messageformat](https://www.npmjs.com/package/intl-messageformat) library. Their [documentation](https://formatjs.io/docs/intl-messageformat/) includes common, editable examples via a "live editor" feature, which can be useful to test and debug message formats.
## Progress
Immich currently supports the following languages:
<a href="https://hosted.weblate.org/engage/immich/">
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
</a>

View File

@@ -1,7 +1,7 @@
# Troubleshooting
:::tip
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.gg/D8JsnBEuKb) server, where we have a dedicated channel for `#contributing`.
A great option to get assistance with troubleshooting is to join our [Discord](https://discord.immich.app) server, where we have a dedicated channel for `#contributing`.
:::
## Known Issues

View File

@@ -60,17 +60,17 @@ For RKMPP to work:
#### Basic Setup
1. If you do not already have it, download the latest [`hwaccel.transcoding.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
2. In the `docker-compose.yml` under `immich-microservices`, uncomment the `extends` section and change `cpu` to the appropriate backend.
2. In the `docker-compose.yml` under `immich-server`, uncomment the `extends` section and change `cpu` to the appropriate backend.
- For VAAPI on WSL2, be sure to use `vaapi-wsl` rather than `vaapi`
3. Redeploy the `immich-microservices` container with these updated settings.
3. Redeploy the `immich-server` container with these updated settings.
4. In the Admin page under `Video transcoding settings`, change the hardware acceleration setting to the appropriate option and save.
5. (Optional) If using a compatible backend, you may enable hardware decoding for optimal performance.
#### Single Compose File
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-microservices` service directly.
Some platforms, including Unraid and Portainer, do not support multiple Compose files as of writing. As an alternative, you can "inline" the relevant contents of the [`hwaccel.transcoding.yml`][hw-file] file into the `immich-server` service directly.
For example, the `qsv` section in this file is:
@@ -79,21 +79,22 @@ devices:
- /dev/dri:/dev/dri
```
You can add this to the `immich-microservices` service instead of extending from `hwaccel.transcoding.yml`:
You can add this to the `immich-server` service instead of extending from `hwaccel.transcoding.yml`:
```yaml
immich-microservices:
container_name: immich_microservices
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
# Note the lack of an `extends` section
devices:
- /dev/dri:/dev/dri
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
- .env
ports:
- 2283:3001
depends_on:
- redis
- database

View File

@@ -45,8 +45,6 @@ Regardless of filesystem, it is not recommended to use a network share for your
| `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 |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | 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 |

View File

@@ -27,7 +27,7 @@ For more information about setting up the community image see [here](https://git
:::info
- Guide was written using Unraid v6.12.10
- Guide was written using Unraid v6.12.10.
- Requires you to have installed the plugin: [Docker Compose Manager](https://forums.unraid.net/topic/114415-plugin-docker-compose-manager/)
- An Unraid share created for your images
- There has been a [report](https://forums.unraid.net/topic/130006-errortraps-traps-node27707-trap-invalid-opcode-ip14fcfc8d03c0-sp7fff32889dd8-more/#comment-1189395) of this not working if your Unraid server doesn't support AVX _(e.g. using a T610)_
@@ -46,7 +46,8 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
3. Select the cog ⚙️ next to Immich then click "**Edit Stack**"
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default.
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
<details >
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
<ul>
@@ -70,6 +71,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
</ul>
</details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:

View File

@@ -13,4 +13,4 @@ Running into an issue or have a question? Try the following:
[github-issues]: https://github.com/immich-app/immich/issues
[github-releases]: https://github.com/immich-app/immich/releases
[discord-link]: https://discord.com/invite/D8JsnBEuKb
[discord-link]: https://discord.immich.app

View File

@@ -5,21 +5,21 @@ sidebar_position: 3
# Quick Start
Here is a quick, no-choices path to install Immich and take it for a test drive.
Once you've tried it, perhaps you'll use one of the many other ways
Once you've tried it, you might use one of the many other ways
to install and use it.
## Requirements
Check the [requirements page](/docs/install/requirements) to get started.
## Install and launch via Docker Compose
## Install and Launch via Docker Compose
Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions
to install the server.
- Where random passwords are required, `pwgen` is a handy utility.
- `UPLOAD_LOCATION` should be set to some new directory on the server
with free space.
with enough free space.
- You may ignore "Step 4 - Upgrading".
## Try the Web UI
@@ -48,26 +48,26 @@ import MobileAppLogin from '/docs/partials/_mobile-app-login.md';
In the mobile app, you should see the photo you uploaded from the web UI.
### Transfer Photos from your Mobile Device
### Transfer Photos from Your Mobile Device
import MobileAppBackup from '/docs/partials/_mobile-app-backup.md';
<MobileAppBackup />
Depending on how many photos are on your mobile device, this backup may
The backup time differs depending on how many photos are on your mobile device. Large uploads may
take quite a while.
You can select the Jobs tab to see Immich processing your photos.
You can select the **Jobs** tab to see Immich processing your photos.
<img src={require('/docs/guides/img/jobs-tab.png').default} title="Jobs tab" />
## Set up your backups
## Set up Your Backups
You may want to back up the content of your Immich instance
along with other parts of your server; be sure to read about
[database backup](/docs/administration/backup-and-restore).
## Where to go from here?
## Where to Go From Here
You may decide you'd like to install the server a different way;
the Install category on the left menu provides many options.

View File

@@ -4,11 +4,17 @@ sidebar_position: 5
# Support The Project
## Contributing
## Report issues
1. Testing - Using Immich and reporting bugs is a great way to help support the project. Found a bug? [Open an issue on GitHub][github-issue].
1. Translations - The Immich mobile app has been translated into [17 languages][github-langs] so far! To contribute with translations, email me at alex.tran1502@gmail.com or send me a message on discord.
1. Development - If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section.
By far the easiest way to help make Immich better it to use it and report issues and bugs. Found a bug? [Open an issue on GitHub][github-issue].
## Translations
Support the project by localizing on [Weblate](https://hosted.weblate.org/projects/immich/immich/). For more information, see the [Translations](/docs/developer/translations) section.
## Development
If you are a programmer or developer, take a look at Immich's [technology stack](/docs/developer/architecture.mdx) and consider fixing bugs or building new features. The team and I are always looking for new contributors. For information about how to contribute as a developer, see the [Developer](/docs/developer/architecture.mdx) section.
[github-issue]: https://github.com/immich-app/immich/issues/new/choose
[github-langs]: https://github.com/immich-app/immich/tree/main/mobile/assets/i18n

View File

@@ -124,7 +124,7 @@ const config = {
position: 'right',
},
{
href: 'https://discord.gg/D8JsnBEuKb',
href: 'https://discord.immich.app',
label: 'Discord',
position: 'right',
},
@@ -151,7 +151,7 @@ const config = {
items: [
{
label: 'Discord',
href: 'https://discord.com/invite/D8JsnBEuKb',
href: 'https://discord.immich.app',
},
{
label: 'Reddit',

379
docs/package-lock.json generated
View File

@@ -2155,9 +2155,10 @@
}
},
"node_modules/@docusaurus/core": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.3.2.tgz",
"integrity": "sha512-PzKMydKI3IU1LmeZQDi+ut5RSuilbXnA8QdowGeJEgU8EJjmx3rBHNT1LxQxOVqNEwpWi/csLwd9bn7rUjggPA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz",
"integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.3",
"@babel/generator": "^7.23.3",
@@ -2169,12 +2170,12 @@
"@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8",
"@docusaurus/cssnano-preset": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/cssnano-preset": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2240,9 +2241,10 @@
}
},
"node_modules/@docusaurus/cssnano-preset": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.3.2.tgz",
"integrity": "sha512-+5+epLk/Rp4vFML4zmyTATNc3Is+buMAL6dNjrMWahdJCJlMWMPd/8YfU+2PA57t8mlSbhLJ7vAZVy54cd1vRQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz",
"integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==",
"license": "MIT",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
"postcss": "^8.4.38",
@@ -2254,9 +2256,10 @@
}
},
"node_modules/@docusaurus/logger": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.3.2.tgz",
"integrity": "sha512-Ldu38GJ4P8g4guN7d7pyCOJ7qQugG7RVyaxrK8OnxuTlaImvQw33aDRwaX2eNmX8YK6v+//Z502F4sOZbHHCHQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz",
"integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"tslib": "^2.6.0"
@@ -2266,13 +2269,14 @@
}
},
"node_modules/@docusaurus/mdx-loader": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.3.2.tgz",
"integrity": "sha512-AFRxj/aOk3/mfYDPxE3wTbrjeayVRvNSZP7mgMuUlrb2UlPRbSVAFX1k2RbgAJrnTSwMgb92m2BhJgYRfptN3g==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz",
"integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@@ -2304,11 +2308,12 @@
}
},
"node_modules/@docusaurus/module-type-aliases": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.3.2.tgz",
"integrity": "sha512-b/XB0TBJah5yKb4LYuJT4buFvL0MGAb0+vJDrJtlYMguRtsEBkf2nWl5xP7h4Dlw6ol0hsHrCYzJ50kNIOEclw==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz",
"integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==",
"license": "MIT",
"dependencies": {
"@docusaurus/types": "3.3.2",
"@docusaurus/types": "3.4.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2322,17 +2327,18 @@
}
},
"node_modules/@docusaurus/plugin-content-blog": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.3.2.tgz",
"integrity": "sha512-fJU+dmqp231LnwDJv+BHVWft8pcUS2xVPZdeYH6/ibH1s2wQ/sLcmUrGWyIv/Gq9Ptj8XWjRPMghlxghuPPoxg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz",
"integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
@@ -2353,18 +2359,19 @@
}
},
"node_modules/@docusaurus/plugin-content-docs": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.3.2.tgz",
"integrity": "sha512-Dm1ri2VlGATTN3VGk1ZRqdRXWa1UlFubjaEL6JaxaK7IIFqN/Esjpl+Xw10R33loHcRww/H76VdEeYayaL76eg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz",
"integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
@@ -2383,15 +2390,16 @@
}
},
"node_modules/@docusaurus/plugin-content-pages": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.3.2.tgz",
"integrity": "sha512-EKc9fQn5H2+OcGER8x1aR+7URtAGWySUgULfqE/M14+rIisdrBstuEZ4lUPDRrSIexOVClML82h2fDS+GSb8Ew==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz",
"integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
@@ -2405,13 +2413,14 @@
}
},
"node_modules/@docusaurus/plugin-debug": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.3.2.tgz",
"integrity": "sha512-oBIBmwtaB+YS0XlmZ3gCO+cMbsGvIYuAKkAopoCh0arVjtlyPbejzPrHuCoRHB9G7abjNZw7zoONOR8+8LM5+Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz",
"integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0"
@@ -2425,13 +2434,14 @@
}
},
"node_modules/@docusaurus/plugin-google-analytics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.3.2.tgz",
"integrity": "sha512-jXhrEIhYPSClMBK6/IA8qf1/FBoxqGXZvg7EuBax9HaK9+kL3L0TJIlatd8jQJOMtds8mKw806TOCc3rtEad1A==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz",
"integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"tslib": "^2.6.0"
},
"engines": {
@@ -2443,13 +2453,14 @@
}
},
"node_modules/@docusaurus/plugin-google-gtag": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.3.2.tgz",
"integrity": "sha512-vcrKOHGbIDjVnNMrfbNpRQR1x6Jvcrb48kVzpBAOsKbj9rXZm/idjVAXRaewwobHdOrJkfWS/UJoxzK8wyLRBQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz",
"integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},
@@ -2462,13 +2473,14 @@
}
},
"node_modules/@docusaurus/plugin-google-tag-manager": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.3.2.tgz",
"integrity": "sha512-ldkR58Fdeks0vC+HQ+L+bGFSJsotQsipXD+iKXQFvkOfmPIV6QbHRd7IIcm5b6UtwOiK33PylNS++gjyLUmaGw==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz",
"integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"tslib": "^2.6.0"
},
"engines": {
@@ -2480,16 +2492,17 @@
}
},
"node_modules/@docusaurus/plugin-sitemap": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.3.2.tgz",
"integrity": "sha512-/ZI1+bwZBhAgC30inBsHe3qY9LOZS+79fRGkNdTcGHRMcdAp6Vw2pCd1gzlxd/xU+HXsNP6cLmTOrggmRp3Ujg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz",
"integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"
@@ -2503,23 +2516,24 @@
}
},
"node_modules/@docusaurus/preset-classic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.3.2.tgz",
"integrity": "sha512-1SDS7YIUN1Pg3BmD6TOTjhB7RSBHJRpgIRKx9TpxqyDrJ92sqtZhomDc6UYoMMLQNF2wHFZZVGFjxJhw2VpL+Q==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz",
"integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/plugin-debug": "3.3.2",
"@docusaurus/plugin-google-analytics": "3.3.2",
"@docusaurus/plugin-google-gtag": "3.3.2",
"@docusaurus/plugin-google-tag-manager": "3.3.2",
"@docusaurus/plugin-sitemap": "3.3.2",
"@docusaurus/theme-classic": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-search-algolia": "3.3.2",
"@docusaurus/types": "3.3.2"
"@docusaurus/core": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/plugin-debug": "3.4.0",
"@docusaurus/plugin-google-analytics": "3.4.0",
"@docusaurus/plugin-google-gtag": "3.4.0",
"@docusaurus/plugin-google-tag-manager": "3.4.0",
"@docusaurus/plugin-sitemap": "3.4.0",
"@docusaurus/theme-classic": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-search-algolia": "3.4.0",
"@docusaurus/types": "3.4.0"
},
"engines": {
"node": ">=18.0"
@@ -2530,22 +2544,23 @@
}
},
"node_modules/@docusaurus/theme-classic": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.3.2.tgz",
"integrity": "sha512-gepHFcsluIkPb4Im9ukkiO4lXrai671wzS3cKQkY9BXQgdVwsdPf/KS0Vs4Xlb0F10fTz+T3gNjkxNEgSN9M0A==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz",
"integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.3.2",
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/types": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
@@ -2569,17 +2584,18 @@
}
},
"node_modules/@docusaurus/theme-common": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.3.2.tgz",
"integrity": "sha512-kXqSaL/sQqo4uAMQ4fHnvRZrH45Xz2OdJ3ABXDS7YVGPSDTBC8cLebFrRR4YF9EowUHto1UC/EIklJZQMG/usA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz",
"integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==",
"license": "MIT",
"dependencies": {
"@docusaurus/mdx-loader": "3.3.2",
"@docusaurus/module-type-aliases": "3.3.2",
"@docusaurus/plugin-content-blog": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/plugin-content-pages": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2598,18 +2614,19 @@
}
},
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.3.2.tgz",
"integrity": "sha512-qLkfCl29VNBnF1MWiL9IyOQaHxUvicZp69hISyq/xMsNvFKHFOaOfk9xezYod2Q9xx3xxUh9t/QPigIei2tX4w==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
"integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==",
"license": "MIT",
"dependencies": {
"@docsearch/react": "^3.5.2",
"@docusaurus/core": "3.3.2",
"@docusaurus/logger": "3.3.2",
"@docusaurus/plugin-content-docs": "3.3.2",
"@docusaurus/theme-common": "3.3.2",
"@docusaurus/theme-translations": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-validation": "3.3.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0",
@@ -2628,9 +2645,10 @@
}
},
"node_modules/@docusaurus/theme-translations": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.3.2.tgz",
"integrity": "sha512-bPuiUG7Z8sNpGuTdGnmKl/oIPeTwKr0AXLGu9KaP6+UFfRZiyWbWE87ti97RrevB2ffojEdvchNujparR3jEZQ==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz",
"integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==",
"license": "MIT",
"dependencies": {
"fs-extra": "^11.1.1",
"tslib": "^2.6.0"
@@ -2640,9 +2658,10 @@
}
},
"node_modules/@docusaurus/types": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.3.2.tgz",
"integrity": "sha512-5p201S7AZhliRxTU7uMKtSsoC8mgPA9bs9b5NQg1IRdRxJfflursXNVsgc3PcMqiUTul/v1s3k3rXXFlRE890w==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz",
"integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==",
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
"@types/history": "^4.7.11",
@@ -2660,12 +2679,13 @@
}
},
"node_modules/@docusaurus/utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.3.2.tgz",
"integrity": "sha512-f4YMnBVymtkSxONv4Y8js3Gez9IgHX+Lcg6YRMOjVbq8sgCcdYK1lf6SObAuz5qB/mxiSK7tW0M9aaiIaUSUJg==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz",
"integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0",
@@ -2682,6 +2702,7 @@
"shelljs": "^0.8.5",
"tslib": "^2.6.0",
"url-loader": "^4.1.1",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
@@ -2697,9 +2718,10 @@
}
},
"node_modules/@docusaurus/utils-common": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.3.2.tgz",
"integrity": "sha512-QWFTLEkPYsejJsLStgtmetMFIA3pM8EPexcZ4WZ7b++gO5jGVH7zsipREnCHzk6+eDgeaXfkR6UPaTt86bp8Og==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz",
"integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.0"
},
@@ -2716,15 +2738,18 @@
}
},
"node_modules/@docusaurus/utils-validation": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.3.2.tgz",
"integrity": "sha512-itDgFs5+cbW9REuC7NdXals4V6++KifgVMzoGOOOSIifBQw+8ULhy86u5e1lnptVL0sv8oAjq2alO7I40GR7pA==",
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz",
"integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.3.2",
"@docusaurus/utils": "3.3.2",
"@docusaurus/utils-common": "3.3.2",
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"fs-extra": "^11.2.0",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
"lodash": "^4.17.21",
"tslib": "^2.6.0"
},
"engines": {
@@ -12615,9 +12640,10 @@
}
},
"node_modules/picocolors": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ=="
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"license": "ISC"
},
"node_modules/picomatch": {
"version": "2.3.1",
@@ -12728,9 +12754,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.38",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"funding": [
{
"type": "opencollective",
@@ -12745,9 +12771,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"source-map-js": "^1.2.0"
},
"engines": {
@@ -13573,10 +13600,11 @@
}
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -15986,9 +16014,10 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
"integrity": "sha512-U7sxQk/n397Bmx4JHbJx/iSOOv5G+II3f1kpLpY2QeUv5DcPdcTsYLlusZfq1NthHS1c1cZoyFmmkex1rzke0A==",
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
"arg": "^5.0.2",
@@ -16025,6 +16054,7 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
@@ -16346,9 +16376,10 @@
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"

View File

@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
"node": "20.14.0"
"node": "20.15.1"
}
}

View File

@@ -38,6 +38,11 @@ const guides: CommunityGuidesProps[] = [
description: 'Import your Google Photos files into Immich and add your albums',
url: 'https://github.com/immich-app/immich/discussions/1340',
},
{
title: 'Access Immich with custom domain',
description: 'Access your local Immich installation over the internet using your own domain',
url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -8,7 +8,6 @@
@tailwind utilities;
@import url('https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Snowburst+One&display=swap');
html,
button {
@@ -48,7 +47,3 @@ img {
div[class^='announcementBar_'] {
min-height: 2rem;
}
.navbar__brand .navbar__title {
@apply font-immich-title text-2xl font-normal text-immich-primary dark:text-immich-dark-primary;
}

View File

@@ -0,0 +1,77 @@
import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item as TimelineItem, Timeline } from '../components/timeline';
const withLanguage = (date: Date) => (language: string) => date.toLocaleDateString(language);
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [
{
icon: mdiLeadPencil,
iconColor: 'gold',
title: 'PostgreSQL NOTIFY is cursed',
description:
'PostgreSQL does everything in a transaction, including NOTIFY. This means using the socket.io postgres-adapter writes to WAL every 5 seconds.',
link: { url: 'https://github.com/immich-app/immich/pull/10801', text: '#10801' },
date: new Date(2024, 6, 3),
},
{
icon: mdiWeb,
iconColor: 'lightskyblue',
title: 'npm scripts are cursed',
description:
'npm scripts make a http call to the npm registry each time they run, which means they are a terrible way to execute a health check.',
link: { url: 'https://github.com/immich-app/immich/issues/10796', text: '#10796' },
date: new Date(2024, 6, 3),
},
{
icon: mdiSpeedometerSlow,
iconColor: 'brown',
title: '50 extra packages are cursed',
description:
'There is a user in the JavaScript community who goes around adding "backwards compatibility" to projects. They do this by adding 50 extra package dependencies to your project, which are maintained by them.',
link: { url: 'https://github.com/immich-app/immich/pull/10690', text: '#10690' },
date: new Date(2024, 5, 28),
},
{
icon: mdiLockOutline,
iconColor: 'gold',
title: 'Long passwords are cursed',
description:
'The bcrypt implementation only uses the first 72 bytes of a string. Any characters after that are ignored.',
// link: GHSA-4p64-9f7h-3432
date: new Date(2024, 5, 25),
},
{
icon: mdiCalendarToday,
iconColor: 'greenyellow',
title: 'JavaScript Date objects are cursed',
description: 'JavaScript date objects are 1 indexed for years and days, but 0 indexed for months.',
link: { url: 'https://github.com/immich-app/immich/pulls/6787', text: '#6787' },
date: new Date(2024, 0, 31),
},
];
export default function CursedKnowledgePage(): JSX.Element {
return (
<Layout title="Cursed Knowledge" description="Things we wish we didn't know">
<section className="my-8">
<h1 className="md:text-6xl text-center mb-10 text-immich-primary dark:text-immich-dark-primary px-2">
Cursed Knowledge
</h1>
<p className="text-center text-xl px-2">
Cursed knowledge we have learned as a result of building Immich that we wish we never knew.
</p>
<div className="flex justify-around mt-8 w-full max-w-full">
<Timeline
items={items
.sort((a, b) => b.date.getTime() - a.date.getTime())
.map((item) => ({ ...item, getDateLabel: withLanguage(item.date) }))}
/>
</div>
</section>
</Layout>
);
}

View File

@@ -36,7 +36,7 @@ function HomepageHeader() {
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-dark-primary dark:bg-immich-primary rounded-full hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://discord.gg/D8JsnBEuKb"
to="https://discord.immich.app"
>
Discord
</Link>

View File

@@ -14,6 +14,7 @@ import {
mdiCheckboxMarked,
mdiCloudUploadOutline,
mdiCollage,
mdiContentDuplicate,
mdiDevices,
mdiEmailOutline,
mdiExpansionCard,
@@ -28,12 +29,14 @@ import {
mdiForum,
mdiHandshakeOutline,
mdiHeart,
mdiHistory,
mdiImage,
mdiImageAlbum,
mdiImageEdit,
mdiImageMultipleOutline,
mdiImageSearch,
mdiKeyboardSettingsOutline,
mdiLockOutline,
mdiMagnify,
mdiMagnifyScan,
mdiMap,
@@ -63,14 +66,13 @@ import {
mdiVectorCombine,
mdiVideo,
mdiWeb,
mdiContentDuplicate,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
'v1.106.0': new Date(2024, 5, 11),
'v1.106.1': new Date(2024, 5, 11),
'v1.104.0': new Date(2024, 4, 13),
'v1.103.0': new Date(2024, 3, 29),
'v1.102.0': new Date(2024, 3, 15),
@@ -159,6 +161,14 @@ const withRelease = ({
};
const roadmap: Item[] = [
{
done: false,
icon: mdiLockOutline,
iconColor: 'sandybrown',
title: 'Private/locked photos',
description: 'Private assets with extra protections',
getDateLabel: () => 'Planned for 2024',
},
{
done: false,
icon: mdiRocketLaunch,
@@ -199,14 +209,6 @@ const roadmap: Item[] = [
description: 'Granular access controls for users and api keys',
getDateLabel: () => 'Planned for 2024',
},
{
done: false,
icon: mdiWeb,
iconColor: 'royalblue',
title: 'Web translations',
description: 'Translate the web application to multiple languages',
getDateLabel: () => 'Planned for 2024',
},
{
done: false,
icon: mdiCameraBurst,
@@ -218,18 +220,31 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
withRelease({
icon: mdiHistory,
title: 'Versioned documentation',
description: 'View documentation as it was at the time of past releases',
release: 'v1.106.1',
}),
withRelease({
icon: mdiWeb,
iconColor: 'royalblue',
title: 'Web translations',
description: 'Translate the web application to multiple languages',
release: 'v1.106.1',
}),
withRelease({
icon: mdiContentDuplicate,
title: 'Similar image detection',
description: 'Detect duplicate assets that arent exactly identical',
release: 'v1.106.0',
release: 'v1.106.1',
}),
withRelease({
icon: mdiVectorCombine,
title: 'Container consolidation',
description:
'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
release: 'v1.106.0',
release: 'v1.106.1',
}),
withRelease({
icon: mdiPencil,

View File

@@ -1,4 +1,28 @@
[
{
"label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app"
},
{
"label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app"
},
{
"label": "v1.107.1",
"url": "https://v1.107.1.archive.immich.app"
},
{
"label": "v1.107.0",
"url": "https://v1.107.0.archive.immich.app"
},
{
"label": "v1.106.4",
"url": "https://v1.106.4.archive.immich.app"
},
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"

View File

@@ -21,9 +21,6 @@ module.exports = {
'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#212121',
},
fontFamily: {
'immich-title': ['Snowburst One', 'cursive'],
},
},
},
plugins: [],

View File

@@ -1 +1 @@
20.14
20.15.1

View File

@@ -10,6 +10,11 @@ services:
build:
context: ../
dockerfile: server/Dockerfile
args:
- BUILD_ID=1234567890
- BUILD_IMAGE=e2e
- BUILD_SOURCE_REF=e2e
- BUILD_SOURCE_COMMIT=e2eeeeeeeeeeeeeeeeee
environment:
- DB_HOSTNAME=database
- DB_USERNAME=postgres
@@ -17,6 +22,7 @@ services:
- DB_DATABASE_NAME=immich
- IMMICH_MACHINE_LEARNING_ENABLED=false
- IMMICH_METRICS=true
- IMMICH_ENV=testing
volumes:
- upload:/usr/src/app/upload
- ./test-assets:/test-assets
@@ -27,7 +33,7 @@ services:
- 2283:3001
redis:
image: redis:6.2-alpine@sha256:d6c2911ac51b289db208767581a5d154544f2b2fe4914ea5056443f62dc6e900
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

245
e2e/package-lock.json generated
View File

@@ -1,19 +1,19 @@
{
"name": "immich-e2e",
"version": "1.106.2",
"version": "1.108.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.106.2",
"version": "1.108.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/node": "^20.14.10",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",
@@ -23,13 +23,13 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"eslint-plugin-unicorn": "^54.0.0",
"exiftool-vendored": "^27.0.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@@ -39,11 +39,12 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.2",
"version": "2.2.8",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21"
},
"bin": {
@@ -55,7 +56,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.3.1",
"@types/node": "^20.14.10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
@@ -65,14 +66,15 @@
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1"
},
"engines": {
@@ -81,14 +83,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.106.2",
"version": "1.108.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.11.0",
"@types/node": "^20.14.10",
"typescript": "^5.3.3"
}
},
@@ -971,18 +973,19 @@
}
},
"node_modules/@playwright/test": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.44.1.tgz",
"integrity": "sha512-1hZ4TNvD5z9VuhNJ/walIjvMVvYkZKf71axoF/uiAqpntQJXpG64dlXhoDXE3OczPuTuvjf/M5KWFg5VAVUS3Q==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz",
"integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.44.1"
"playwright": "1.45.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
@@ -1230,9 +1233,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"version": "20.14.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
"integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1344,17 +1347,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.15.0.tgz",
"integrity": "sha512-uiNHpyjZtFrLwLDpHnzaDlP3Tt6sGMqTCiqmxaN4n4RP0EfYZDODJyddiFDF44Hjwxr5xAcaYxVKm9QKQFJFLA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.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",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1378,16 +1381,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"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==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@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",
"debug": "^4.3.4"
},
"engines": {
@@ -1407,14 +1410,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1428,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.15.0.tgz",
"integrity": "sha512-SkgriaeV6PDvpA6253PDVep0qCqgbO1IOBiycjnXsszNTVQe5flN5wR5jiczoEoDEnAqYFSFFc9al9BSGVltkg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/typescript-estree": "7.15.0",
"@typescript-eslint/utils": "7.15.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1453,9 +1456,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"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==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1467,14 +1470,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"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==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/visitor-keys": "7.15.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1506,9 +1509,9 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.4",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.4.tgz",
"integrity": "sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==",
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -1522,16 +1525,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"version": "7.15.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.15.0.tgz",
"integrity": "sha512-hfDMDqaqOqsUVGiEPSMLR/AjTSCsmJwjpKkYQRo1FNbmW4tBwBspYDwO9eh7sKSTwMQgBw9/T4DHudPaqshRWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
"@typescript-eslint/scope-manager": "7.15.0",
"@typescript-eslint/types": "7.15.0",
"@typescript-eslint/typescript-estree": "7.15.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1548,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"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==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/types": "7.15.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1671,10 +1674,11 @@
"dev": true
},
"node_modules/acorn": {
"version": "8.11.3",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
"integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz",
"integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==",
"dev": true,
"license": "MIT",
"bin": {
"acorn": "bin/acorn"
},
@@ -2276,15 +2280,15 @@
"dev": true
},
"node_modules/engine.io-client": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz",
"integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==",
"version": "6.5.4",
"resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz",
"integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==",
"dev": true,
"dependencies": {
"@socket.io/component-emitter": "~3.1.0",
"debug": "~4.3.1",
"engine.io-parser": "~5.2.1",
"ws": "~8.11.0",
"ws": "~8.17.1",
"xmlhttprequest-ssl": "~2.0.0"
}
},
@@ -2484,10 +2488,11 @@
}
},
"node_modules/eslint-plugin-unicorn": {
"version": "53.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-53.0.0.tgz",
"integrity": "sha512-kuTcNo9IwwUCfyHGwQFOK/HjJAYzbODHN3wP0PgqbW+jbXqpNWxNVpVhj2tO9SixBwuAdmal8rVcWKBxwFnGuw==",
"version": "54.0.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz",
"integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.24.5",
"@eslint-community/eslint-utils": "^4.4.0",
@@ -2517,10 +2522,11 @@
}
},
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.0.2.tgz",
"integrity": "sha512-wV19ZEGEMAC1eHgrS7UQPqsdEiCIbTKTasEfcXAigzoXICcqZSjBZEHlZwNVvKg6UBCjSlos84XiLqsRJnIcIg==",
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz",
"integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
@@ -2544,6 +2550,7 @@
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz",
"integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
@@ -2552,12 +2559,13 @@
}
},
"node_modules/eslint-plugin-unicorn/node_modules/espree": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.0.1.tgz",
"integrity": "sha512-MWkrWZbJsL2UwnjxTX3gG8FneachS/Mwg7tdGXce011sJd5b0JG54vat5KHnfSBODZ3Wvzd2WnjxyzsRoVv+ww==",
"version": "10.1.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz",
"integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.11.3",
"acorn": "^8.12.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.0.0"
},
@@ -2573,6 +2581,7 @@
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
@@ -2700,9 +2709,9 @@
}
},
"node_modules/exiftool-vendored": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"version": "27.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-27.0.0.tgz",
"integrity": "sha512-/jHX8Jjadj0YJzpqnuBo1Yy2ln2hnRbBIc+3jcVOLQ6qhHEKsLRlfJ145Ghn7k/EcnfpDzVX3V8AUCTC8juTow==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -4127,10 +4136,11 @@
}
},
"node_modules/pg": {
"version": "8.11.5",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.11.5.tgz",
"integrity": "sha512-jqgNHSKL5cbDjFlHyYsCXmQDrfIX/3RsNwYqpd4N0Kt8niLuNoRNH+aazv6cOd43gPh9Y4DjQCtb+X0MH0Hvnw==",
"version": "8.12.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.12.0.tgz",
"integrity": "sha512-A+LHUSnwnxrnL/tZ+OLfqR1SxLN3c/pgDztZ47Rpbsd4jUytsTtwQo/TLPRzPJMp/1pbhYVhH9cuSZLAajNfjQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.6.4",
"pg-pool": "^3.6.2",
@@ -4255,33 +4265,35 @@
}
},
"node_modules/playwright": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.44.1.tgz",
"integrity": "sha512-qr/0UJ5CFAtloI3avF95Y0L1xQo6r3LQArLIg/z/PoGJ6xa+EwzrwO5lpNr/09STxdHuUoP2mvuELJS+hLdtgg==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz",
"integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.44.1"
"playwright-core": "1.45.1"
},
"bin": {
"playwright": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
},
"optionalDependencies": {
"fsevents": "2.3.2"
}
},
"node_modules/playwright-core": {
"version": "1.44.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.44.1.tgz",
"integrity": "sha512-wh0JWtYTrhv1+OSsLPgFzGzt67Y7BE/ZS3jEqgGBlp2ppp1ZDj8c+9IARNW4dwf1poq5MgHreEM2KV/GuR4cFA==",
"version": "1.45.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz",
"integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
},
"engines": {
"node": ">=16"
"node": ">=18"
}
},
"node_modules/pluralize": {
@@ -4385,10 +4397,11 @@
}
},
"node_modules/prettier": {
"version": "3.2.5",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz",
"integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==",
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
@@ -4412,21 +4425,22 @@
}
},
"node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==",
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4",
"@volar/vue-typescript": "^1.0.4",
"@vue/language-plugin-pug": "^2.0.24",
"prettier": ">=2.0",
"typescript": ">=2.9"
"typescript": ">=2.9",
"vue-tsc": "^2.0.24"
},
"peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": {
"@vue/language-plugin-pug": {
"optional": true
},
"@volar/vue-typescript": {
"vue-tsc": {
"optional": true
}
}
@@ -5260,10 +5274,11 @@
}
},
"node_modules/typescript": {
"version": "5.4.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz",
"integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==",
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -5572,16 +5587,16 @@
"dev": true
},
"node_modules/ws": {
"version": "8.11.0",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz",
"integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==",
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"dev": true,
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": "^5.0.2"
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.106.2",
"version": "1.108.0",
"description": "",
"main": "index.js",
"type": "module",
@@ -23,7 +23,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/node": "^20.14.10",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",
@@ -33,13 +33,13 @@
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^53.0.0",
"exiftool-vendored": "^26.0.0",
"eslint-plugin-unicorn": "^54.0.0",
"exiftool-vendored": "^27.0.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"prettier-plugin-organize-imports": "^4.0.0",
"socket.io-client": "^4.7.4",
"supertest": "^7.0.0",
"typescript": "^5.3.3",
@@ -47,6 +47,6 @@
"vitest": "^1.3.0"
},
"volta": {
"node": "20.14.0"
"node": "20.15.1"
}
}

View File

@@ -88,7 +88,7 @@ describe('/albums', () => {
});
await addAssetsToAlbum(
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id] } },
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
{ headers: asBearerAuth(user1.accessToken) },
);
@@ -261,7 +261,7 @@ describe('/albums', () => {
.get(`/albums?assetId=${user1Asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toHaveLength(2);
});
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
@@ -509,7 +509,17 @@ describe('/albums', () => {
expect(body).toEqual(errorDto.unauthorized);
});
it('should not be able to remove foreign asset from own album', async () => {
it('should require authorization', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user1Albums[1].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
.send({ ids: [user1Asset1.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should be able to remove foreign asset from owned album', async () => {
const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user2.accessToken}`)
@@ -519,8 +529,7 @@ describe('/albums', () => {
expect(body).toEqual([
expect.objectContaining({
id: user1Asset1.id,
success: false,
error: 'no_permission',
success: true,
}),
]);
});
@@ -555,10 +564,10 @@ describe('/albums', () => {
const { status, body } = await request(app)
.delete(`/albums/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [user1Asset1.id] });
.send({ ids: [user1Asset2.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
expect(body).toEqual([expect.objectContaining({ id: user1Asset2.id, success: true })]);
});
it('should not be able to remove assets from album as a viewer', async () => {

View File

@@ -588,6 +588,58 @@ describe('/asset', () => {
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(true);
});
it('should clean up live photos', async () => {
const { id: motionId } = await utils.createAsset(admin.accessToken, {
assetData: { filename: 'test.mp4', bytes: makeRandomImage() },
});
const { id: photoId } = await utils.createAsset(admin.accessToken, { livePhotoVideoId: motionId });
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: photoId });
await utils.waitForWebsocketEvent({ event: 'assetHidden', id: motionId });
const asset = await utils.getAssetInfo(admin.accessToken, photoId);
expect(asset.livePhotoVideoId).toBe(motionId);
const { status } = await request(app)
.delete('/assets')
.send({ ids: [photoId], force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: photoId });
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: motionId });
});
it('should not delete a shared motion asset', async () => {
const { id: motionId } = await utils.createAsset(admin.accessToken, {
assetData: { filename: 'test.mp4', bytes: makeRandomImage() },
});
const { id: asset1 } = await utils.createAsset(admin.accessToken, { livePhotoVideoId: motionId });
const { id: asset2 } = await utils.createAsset(admin.accessToken, { livePhotoVideoId: motionId });
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset1 });
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset2 });
await utils.waitForWebsocketEvent({ event: 'assetHidden', id: motionId });
const asset = await utils.getAssetInfo(admin.accessToken, asset1);
expect(asset.livePhotoVideoId).toBe(motionId);
const { status } = await request(app)
.delete('/assets')
.send({ ids: [asset1], force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: asset1 });
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
await expect(utils.getAssetInfo(admin.accessToken, motionId)).resolves.toMatchObject({ id: motionId });
await expect(utils.getAssetInfo(admin.accessToken, asset2)).resolves.toMatchObject({
id: asset2,
livePhotoVideoId: motionId,
});
});
});
describe('GET /assets/:id/thumbnail', () => {
@@ -1148,4 +1200,29 @@ describe('/asset', () => {
expect(video.checksum).toStrictEqual(checksum);
});
});
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['invalid', 'INVALID'],
});
expect(response.existingIds).toHaveLength(0);
});
it('returns the IDs of existing assets', async () => {
await utils.createAsset(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetId: 'test-asset-0',
});
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['test-asset-0'],
});
expect(response.existingIds).toEqual(['test-asset-0']);
});
});
});

View File

@@ -100,6 +100,12 @@ describe('/auth/*', () => {
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should reject an invalid email', async () => {
const { status, body } = await request(app).post('/auth/login').send({ email: [], password });
expect(status).toBe(400);
expect(body).toEqual(errorDto.invalidEmail);
});
}
it('should accept a correct password', async () => {

View File

@@ -230,4 +230,21 @@ describe('/people', () => {
expect(body).toMatchObject({ birthDate: null });
});
});
describe('POST /people/:id/merge', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/people/${uuidDto.notFound}/merge`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not supporting merging a person into themselves', async () => {
const { status, body } = await request(app)
.post(`/people/${visiblePerson.id}/merge`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [visiblePerson.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot merge a person into themselves'));
});
});
});

View File

@@ -339,6 +339,13 @@ describe('/search', () => {
should: 'should search by model',
deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }),
},
{
should: 'should allow searching the upload library (libraryId: null)',
deferred: () => ({
dto: { libraryId: null, size: 1 },
assets: [assetLast],
}),
},
];
for (const { should, deferred } of searchTests) {

View File

@@ -15,6 +15,40 @@ describe('/server-info', () => {
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('GET /server-info/about', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server-info/about');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return about information', async () => {
const { status, body } = await request(app)
.get('/server-info/about')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
version: expect.any(String),
versionUrl: expect.any(String),
repository: 'immich-app/immich',
repositoryUrl: 'https://github.com/immich-app/immich',
build: '1234567890',
buildUrl: 'https://github.com/immich-app/immich/actions/runs/1234567890',
buildImage: 'e2e',
buildImageUrl: 'https://github.com/immich-app/immich/pkgs/container/immich-server',
sourceRef: 'e2e',
sourceCommit: 'e2eeeeeeeeeeeeeeeeee',
sourceUrl: 'https://github.com/immich-app/immich/commit/e2eeeeeeeeeeeeeeeeee',
nodejs: expect.any(String),
ffmpeg: expect.any(String),
imagemagick: expect.any(String),
libvips: expect.any(String),
exiftool: expect.any(String),
licensed: false,
});
});
});
describe('GET /server-info/storage', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server-info/storage');

View File

@@ -0,0 +1,307 @@
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
const serverLicense = {
licenseKey: 'IMSV-6ECZ-91TE-WZRM-Q7AQ-MBN4-UW48-2CPT-71X9',
activationKey:
'4kJUNUWMq13J14zqPFm1NodRcI6MV6DeOGvQNIgrM8Sc9nv669wyEVvFw1Nz4Kb1W7zLWblOtXEQzpRRqC4r4fKjewJxfbpeo9sEsqAVIfl4Ero-Vp1Dg21-sVdDGZEAy2oeTCXAyCT5d1JqrqR6N1qTAm4xOx9ujXQRFYhjRG8uwudw7_Q49pF18Tj5OEv9qCqElxztoNck4i6O_azsmsoOQrLIENIWPh3EynBN3ESpYERdCgXO8MlWeuG14_V1HbNjnJPZDuvYg__YfMzoOEtfm1sCqEaJ2Ww-BaX7yGfuCL4XsuZlCQQNHjfscy_WywVfIZPKCiW8QR74i0cSzQ',
};
describe('/server', () => {
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('GET /server/about', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/about');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return about information', async () => {
const { status, body } = await request(app)
.get('/server/about')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
version: expect.any(String),
versionUrl: expect.any(String),
repository: 'immich-app/immich',
repositoryUrl: 'https://github.com/immich-app/immich',
build: '1234567890',
buildUrl: 'https://github.com/immich-app/immich/actions/runs/1234567890',
buildImage: 'e2e',
buildImageUrl: 'https://github.com/immich-app/immich/pkgs/container/immich-server',
sourceRef: 'e2e',
sourceCommit: 'e2eeeeeeeeeeeeeeeeee',
sourceUrl: 'https://github.com/immich-app/immich/commit/e2eeeeeeeeeeeeeeeeee',
nodejs: expect.any(String),
ffmpeg: expect.any(String),
imagemagick: expect.any(String),
libvips: expect.any(String),
exiftool: expect.any(String),
licensed: false,
});
});
});
describe('GET /server/storage', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/storage');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return the disk information', async () => {
const { status, body } = await request(app)
.get('/server/storage')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
diskAvailable: expect.any(String),
diskAvailableRaw: expect.any(Number),
diskSize: expect.any(String),
diskSizeRaw: expect.any(Number),
diskUsagePercentage: expect.any(Number),
diskUse: expect.any(String),
diskUseRaw: expect.any(Number),
});
});
});
describe('GET /server/ping', () => {
it('should respond with pong', async () => {
const { status, body } = await request(app).get('/server/ping');
expect(status).toBe(200);
expect(body).toEqual({ res: 'pong' });
});
});
describe('GET /server/version', () => {
it('should respond with the server version', async () => {
const { status, body } = await request(app).get('/server/version');
expect(status).toBe(200);
expect(body).toEqual({
major: expect.any(Number),
minor: expect.any(Number),
patch: expect.any(Number),
});
});
});
describe('GET /server/features', () => {
it('should respond with the server features', async () => {
const { status, body } = await request(app).get('/server/features');
expect(status).toBe(200);
expect(body).toEqual({
smartSearch: false,
configFile: false,
duplicateDetection: false,
facialRecognition: false,
map: true,
reverseGeocoding: true,
oauth: false,
oauthAutoLaunch: false,
passwordLogin: true,
search: true,
sidecar: true,
trash: true,
email: false,
});
});
});
describe('GET /server/config', () => {
it('should respond with the server configuration', async () => {
const { status, body } = await request(app).get('/server/config');
expect(status).toBe(200);
expect(body).toEqual({
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
trashDays: 30,
userDeleteDelay: 7,
isInitialized: true,
externalDomain: '',
isOnboarded: false,
});
});
});
describe('GET /server/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/statistics')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should return the server stats', async () => {
const { status, body } = await request(app)
.get('/server/statistics')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
photos: 0,
usage: 0,
usageByUser: [
{
quotaSizeInBytes: null,
photos: 0,
usage: 0,
userName: 'Immich Admin',
userId: admin.userId,
videos: 0,
},
{
quotaSizeInBytes: null,
photos: 0,
usage: 0,
userName: 'User 1',
userId: nonAdmin.userId,
videos: 0,
},
],
videos: 0,
});
});
});
describe('GET /server/media-types', () => {
it('should return accepted media types', async () => {
const { status, body } = await request(app).get('/server/media-types');
expect(status).toBe(200);
expect(body).toEqual({
sidecar: ['.xmp'],
image: expect.any(Array),
video: expect.any(Array),
});
});
});
describe('GET /server/theme', () => {
it('should respond with the server theme', async () => {
const { status, body } = await request(app).get('/server/theme');
expect(status).toBe(200);
expect(body).toEqual({
customCss: '',
});
});
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.get('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should return the server license', async () => {
await request(app).put('/server/license').set('Authorization', `Bearer ${admin.accessToken}`).send(serverLicense);
const { status, body } = await request(app)
.get('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...serverLicense,
activatedAt: expect.any(String),
});
});
});
describe('DELETE /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.delete('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should delete the server license', async () => {
await request(app)
.delete('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(serverLicense);
const { status } = await request(app).get('/server/license').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
});
});
describe('PUT /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/server/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should only work for admins', async () => {
const { status, body } = await request(app)
.put('/server/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should set the server license', async () => {
const { status, body } = await request(app)
.put('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(serverLicense);
expect(status).toBe(200);
expect(body).toEqual({ ...serverLicense, activatedAt: expect.any(String) });
const { body: licenseBody } = await request(app)
.get('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(licenseBody).toEqual({ ...serverLicense, activatedAt: expect.any(String) });
});
it('should reject license not starting with IMSV-', async () => {
const { status, body } = await request(app)
.put('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ licenseKey: 'IMCL-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
it('should reject license with invalid activation key', async () => {
const { status, body } = await request(app)
.put('/server/license')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ licenseKey: serverLicense.licenseKey, activationKey: `invalid${serverLicense.activationKey}` });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
});
});

View File

@@ -5,6 +5,7 @@ import {
getUserAdmin,
getUserPreferencesAdmin,
login,
updateAssets,
} from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
@@ -20,18 +21,16 @@ describe('/admin/users', () => {
let nonAdmin: LoginResponseDto;
let deletedUser: LoginResponseDto;
let userToDelete: LoginResponseDto;
let userToHardDelete: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, nonAdmin, deletedUser, userToDelete, userToHardDelete] = await Promise.all([
[websocket, nonAdmin, deletedUser, userToDelete] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
await deleteUserAdmin(
@@ -64,13 +63,12 @@ describe('/admin/users', () => {
.get(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
]),
);
});
@@ -81,13 +79,12 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ email: admin.userEmail }),
expect.objectContaining({ email: nonAdmin.userEmail }),
expect.objectContaining({ email: userToDelete.userEmail }),
expect.objectContaining({ email: userToHardDelete.userEmail }),
expect.objectContaining({ email: deletedUser.userEmail }),
]),
);
@@ -250,18 +247,23 @@ describe('/admin/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
expect(body).toMatchObject({ avatar: { color: 'orange' } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
expect(after).toMatchObject({ avatar: { color: 'orange' } });
});
it('should update download archive size', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ download: { archiveSize: 1_234_567 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
});
});
@@ -294,19 +296,49 @@ describe('/admin/users', () => {
});
it('should hard delete a user', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));
const { status, body } = await request(app)
.delete(`/admin/users/${userToHardDelete.userId}`)
.delete(`/admin/users/${user.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: userToHardDelete.userId,
id: user.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: userToHardDelete.userId, timeout: 5000 });
await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
});
it('should hard delete a user with stacked assets', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('hard-delete-1'));
const [asset1, asset2] = await Promise.all([
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: asset1.id, ids: [asset2.id] } },
{ headers: asBearerAuth(user.accessToken) },
);
const { status, body } = await request(app)
.delete(`/admin/users/${user.userId}`)
.send({ force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: user.userId,
updatedAt: expect.any(String),
deletedAt: expect.any(String),
});
await utils.waitForWebsocketEvent({ event: 'userDelete', id: user.userId, timeout: 5000 });
});
});

View File

@@ -5,6 +5,12 @@ import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
const userLicense = {
licenseKey: 'IMCL-FF69-TUK1-RWZU-V9Q8-QGQS-S5GC-X4R2-UFK4',
activationKey:
'KuX8KsktrBSiXpQMAH0zLgA5SpijXVr_PDkzLdWUlAogCTMBZ0I3KCHXK0eE9EEd7harxup8_EHMeqAWeHo5VQzol6LGECpFv585U9asXD4Zc-UXt3mhJr2uhazqipBIBwJA2YhmUCDy8hiyiGsukDQNu9Rg9C77UeoKuZBWVjWUBWG0mc1iRqfvF0faVM20w53czAzlhaMxzVGc3Oimbd7xi_CAMSujF_2y8QpA3X2fOVkQkzdcH9lV0COejl7IyH27zQQ9HrlrXv3Lai5Hw67kNkaSjmunVBxC5PS0TpKoc9SfBJMaAGWnaDbjhjYUrm-8nIDQnoeEAidDXVAdPw',
};
describe('/users', () => {
let admin: LoginResponseDto;
let deletedUser: LoginResponseDto;
@@ -72,6 +78,24 @@ describe('/users', () => {
quotaUsageInBytes: 0,
});
});
it('should get my user with license info', async () => {
const { status: licenseStatus } = await request(app)
.put(`/users/me/license`)
.send(userLicense)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(licenseStatus).toBe(200);
const { status, body } = await request(app)
.get(`/users/me`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: nonAdmin.userId,
email: nonAdmin.userEmail,
quotaUsageInBytes: 0,
license: userLicense,
});
});
});
describe('PUT /users/me', () => {
@@ -173,6 +197,45 @@ describe('/users', () => {
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
it('should update avatar color', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ avatar: { color: 'blue' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ avatar: { color: 'blue' } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ avatar: { color: 'blue' } });
});
it('should require an integer for download archive size', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567.89 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number']));
});
it('should update download archive size', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ download: { archiveSize: 4 * 2 ** 30 } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { archiveSize: 1_234_567 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { archiveSize: 1_234_567 } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
});
});
describe('GET /users/:id', () => {
@@ -197,4 +260,81 @@ describe('/users', () => {
});
});
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/users/me/license');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return the user license', async () => {
await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send(userLicense);
const { status, body } = await request(app)
.get('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...userLicense,
activatedAt: expect.any(String),
});
});
});
describe('PUT /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(app).put(`/users/me/license`);
expect(status).toEqual(401);
});
it('should set the user license', async () => {
const { status, body } = await request(app)
.put(`/users/me/license`)
.send(userLicense)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ ...userLicense, activatedAt: expect.any(String) });
expect(status).toBe(200);
expect(body).toEqual({ ...userLicense, activatedAt: expect.any(String) });
const { body: licenseBody } = await request(app)
.get('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(licenseBody).toEqual({ ...userLicense, activatedAt: expect.any(String) });
});
it('should reject license not starting with IMCL-', async () => {
const { status, body } = await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ licenseKey: 'IMSV-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD-ABCD', activationKey: 'activationKey' });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
it('should reject license with invalid activation key', async () => {
const { status, body } = await request(app)
.put('/users/me/license')
.set('Authorization', `Bearer ${nonAdmin.accessToken}`)
.send({ licenseKey: userLicense.licenseKey, activationKey: `invalid${userLicense.activationKey}` });
expect(status).toBe(400);
expect(body.message).toBe('Invalid license key');
});
});
describe('DELETE /users/me/license', () => {
it('should require authentication', async () => {
const { status } = await request(app).put(`/users/me/license`);
expect(status).toEqual(401);
});
it('should delete the user license', async () => {
const { status } = await request(app)
.delete(`/users/me/license`)
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
});
});
});

View File

@@ -9,11 +9,30 @@ describe(`immich-admin`, () => {
describe('list-users', () => {
it('should list the admin user', async () => {
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']);
const { stdout, stderr, exitCode } = await immichAdmin(['list-users']).promise;
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain("email: 'admin@immich.cloud'");
expect(stdout).toContain("name: 'Immich Admin'");
});
});
describe('reset-admin-password', () => {
it('should reset admin password', async () => {
const { child, promise } = immichAdmin(['reset-admin-password']);
let data = '';
child.stdout.on('data', (chunk) => {
data += chunk;
if (data.includes('Please choose a new password (optional)')) {
child.stdin.end('\n');
}
});
const { stderr, stdout, exitCode } = await promise;
expect(exitCode).toBe(0);
expect(stderr).toBe('');
expect(stdout).toContain('The admin password has been updated to:');
});
});
});

View File

@@ -61,6 +61,12 @@ export const errorDto = {
message: 'The server already has an admin',
correlationId: expect.any(String),
},
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
correlationId: expect.any(String),
},
};
export const signupResponseDto = {
@@ -81,6 +87,7 @@ export const signupResponseDto = {
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',
license: null,
},
};

View File

@@ -3,6 +3,7 @@ import {
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetResponseDto,
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
MetadataSearchDto,
@@ -10,6 +11,7 @@ import {
SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto,
checkExistingAssets,
createAlbum,
createApiKey,
createLibrary,
@@ -45,7 +47,7 @@ import { makeRandomImage } from 'src/generators';
import request from 'supertest';
type CommandResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete';
type EventType = 'assetUpload' | 'assetUpdate' | 'assetDelete' | 'userDelete' | 'assetHidden';
type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: number };
type AdminSetupOptions = { onboarding?: boolean };
type AssetData = { bytes?: Buffer; filename: string };
@@ -62,13 +64,13 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) =>
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]);
executeCommand('node', ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args]).promise;
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void;
const deferred = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const promise = new Promise<CommandResponse>((resolve) => (_resolve = resolve));
const child = spawn(command, args, { stdio: 'pipe' });
let stdout = '';
@@ -84,12 +86,13 @@ const executeCommand = (command: string, args: string[]) => {
});
});
return deferred;
return { promise, child };
};
let client: pg.Client | null = null;
const events: Record<EventType, Set<string>> = {
assetHidden: new Set<string>(),
assetUpload: new Set<string>(),
assetUpdate: new Set<string>(),
assetDelete: new Set<string>(),
@@ -149,10 +152,6 @@ export const utils = {
const sql: string[] = [];
if (tables.includes('asset_stack')) {
sql.push('UPDATE "assets" SET "stackId" = NULL;');
}
for (const table of tables) {
if (table === 'system_metadata') {
// prevent reverse geocoder from being re-initialized
@@ -201,6 +200,7 @@ export const utils = {
.on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'assetUpload', id: data.id }))
.on('on_asset_update', (data: AssetResponseDto) => onEvent({ event: 'assetUpdate', id: data.id }))
.on('on_asset_hidden', (assetId: string) => onEvent({ event: 'assetHidden', id: assetId }))
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'assetDelete', id: assetId }))
.on('on_user_delete', (userId: string) => onEvent({ event: 'userDelete', id: userId }))
.connect();
@@ -374,6 +374,9 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},
@@ -393,14 +396,7 @@ export const utils = {
return;
}
const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`;
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
assetId,
personId,
embedding,
]);
await client.query('INSERT INTO asset_faces ("assetId", "personId") VALUES ($1, $2)', [assetId, personId]);
},
setPersonThumbnail: async (personId: string) => {

View File

@@ -51,6 +51,13 @@ test.describe('Shared Links', () => {
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
});
test('download all from shared link', async ({ page }) => {
await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
await page.getByRole('button', { name: 'Download' }).click();
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
});
test('enter password for a shared link', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`);
await page.getByPlaceholder('Password').fill('test-password');

View File

@@ -1,6 +1,6 @@
ARG DEVICE=cpu
FROM python:3.11-bookworm@sha256:96de1ea4821d73fd2c1853d1fdc3cf794ccfe2fae4c3f08579e846de51760a61 as builder-cpu
FROM python:3.11-bookworm@sha256:7bec1574675e7fd9e3a540a03cd7d6811c59ca261bd300cd665369d8f435298a as builder-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as builder-openvino
USER root
@@ -36,7 +36,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
FROM python:3.11-slim-bookworm@sha256:fc39d2e68b554c3f0a5cb8a776280c0b3d73b4c04b83dbade835e2a171ca27ef as prod-cpu
FROM python:3.11-slim-bookworm@sha256:17ec9dc2367aa748559d0212f34665ec4df801129de32db705ea34654b5bc77a as prod-cpu
FROM openvino/ubuntu22_runtime:2023.3.0@sha256:176646df619032ea6c10faf842867119c393e7497b7f88b5e307e932a0fd5aa8 as prod-openvino
USER root

View File

@@ -52,8 +52,6 @@ class Ann(metaclass=_Singleton):
def __init__(self, log_level: int = 3, tuning_level: int = 1, tuning_file: str | None = None) -> None:
if not is_available:
raise RuntimeError("libann is not available!")
if tuning_file and not exists(tuning_file):
raise ValueError("tuning_file must point to an existing (possibly empty) file!")
if tuning_level == 0 and tuning_file is None:
raise ValueError("tuning_level == 0 reads existing tuning information and requires a tuning_file")
if tuning_level < 0 or tuning_level > 3:
@@ -67,6 +65,12 @@ class Ann(metaclass=_Singleton):
self.input_shapes: dict[int, tuple[tuple[int], ...]] = {}
self.ann: int | None = None
self.new()
if self.tuning_file is not None:
# make sure tuning file exists (without clearing contents)
# once filled, the tuning file reduces the cost/time of the first
# inference after model load by 10s of seconds
open(self.tuning_file, "a").close()
def new(self) -> None:
if self.ann is None:
@@ -95,17 +99,19 @@ class Ann(metaclass=_Singleton):
model_path: str,
fast_math: bool = True,
fp16: bool = False,
save_cached_network: bool = False,
cached_network_path: str | None = None,
) -> int:
if not model_path.endswith((".armnn", ".tflite", ".onnx")):
raise ValueError("model_path must be a file with extension .armnn, .tflite or .onnx")
if not exists(model_path):
raise ValueError("model_path must point to an existing file!")
save_cached_network = False
if cached_network_path is not None and not exists(cached_network_path):
raise ValueError("cached_network_path must point to an existing (possibly empty) file!")
if save_cached_network and cached_network_path is None:
raise ValueError("save_cached_network is True, cached_network_path must be specified!")
save_cached_network = True
# create empty model cache file
open(cached_network_path, "a").close()
net_id: int = libann.load(
self.ann,
model_path.encode(),

View File

@@ -8,6 +8,8 @@ from fastapi.testclient import TestClient
from numpy.typing import NDArray
from PIL import Image
from app.config import log
from .main import app
@@ -96,12 +98,83 @@ def clip_tokenizer_cfg() -> dict[str, Any]:
@pytest.fixture(scope="function")
def providers(request: pytest.FixtureRequest) -> Iterator[dict[str, Any]]:
def providers(request: pytest.FixtureRequest) -> Iterator[mock.Mock]:
marker = request.node.get_closest_marker("providers")
if marker is None:
raise ValueError("Missing marker 'providers'")
providers = marker.args[0]
with mock.patch("app.models.base.ort.get_available_providers") as mocked:
with mock.patch("app.sessions.ort.ort.get_available_providers") as mocked:
mocked.return_value = providers
yield providers
@pytest.fixture(scope="function")
def ort_pybind() -> Iterator[mock.Mock]:
with mock.patch("app.sessions.ort.ort.capi._pybind_state") as mocked:
yield mocked
@pytest.fixture(scope="function")
def ov_device_ids(request: pytest.FixtureRequest, ort_pybind: mock.Mock) -> Iterator[mock.Mock]:
marker = request.node.get_closest_marker("ov_device_ids")
if marker is None:
raise ValueError("Missing marker 'ov_device_ids'")
ort_pybind.get_available_openvino_device_ids.return_value = marker.args[0]
return ort_pybind
@pytest.fixture(scope="function")
def ort_session() -> Iterator[mock.Mock]:
with mock.patch("app.sessions.ort.ort.InferenceSession") as mocked:
yield mocked
@pytest.fixture(scope="function")
def ann_session() -> Iterator[mock.Mock]:
with mock.patch("app.sessions.ann.Ann") as mocked:
yield mocked
@pytest.fixture(scope="function")
def rmtree() -> Iterator[mock.Mock]:
with mock.patch("app.models.base.rmtree", autospec=True) as mocked:
mocked.avoids_symlink_attacks = True
yield mocked
@pytest.fixture(scope="function")
def path() -> Iterator[mock.Mock]:
path = mock.MagicMock()
path.exists.return_value = True
path.is_dir.return_value = True
path.is_file.return_value = True
path.with_suffix.return_value = path
path.return_value = path
with mock.patch("app.models.base.Path", return_value=path) as mocked:
yield mocked
@pytest.fixture(scope="function")
def info() -> Iterator[mock.Mock]:
with mock.patch.object(log, "info") as mocked:
yield mocked
@pytest.fixture(scope="function")
def warning() -> Iterator[mock.Mock]:
with mock.patch.object(log, "warning") as mocked:
yield mocked
@pytest.fixture(scope="function")
def exception() -> Iterator[mock.Mock]:
with mock.patch.object(log, "exception") as mocked:
yield mocked
@pytest.fixture(scope="function")
def snapshot_download() -> Iterator[mock.Mock]:
with mock.patch("app.models.base.snapshot_download") as mocked:
yield mocked

View File

@@ -29,6 +29,7 @@ from .schemas import (
InferenceEntry,
InferenceResponse,
MessageResponse,
ModelFormat,
ModelIdentity,
ModelTask,
ModelType,
@@ -192,23 +193,28 @@ async def load(model: InferenceModel) -> InferenceModel:
return model
def _load(model: InferenceModel) -> InferenceModel:
if model.load_attempts > 1:
raise HTTPException(500, f"Failed to load model '{model.model_name}'")
with lock:
model.load()
try:
model.load()
except FileNotFoundError as e:
if model.model_format == ModelFormat.ONNX:
raise e
log.exception(e)
log.warning(
f"{model.model_format.upper()} is available, but model '{model.model_name}' does not support it."
)
model.model_format = ModelFormat.ONNX
model.load()
return model
try:
await run(_load, model)
return model
return await run(_load, model)
except (OSError, InvalidProtobuf, BadZipFile, NoSuchFile):
log.warning(
(
f"Failed to load {model.model_type.replace('_', ' ')} model '{model.model_name}'."
"Clearing cache and retrying."
)
)
log.warning(f"Failed to load {model.model_type.replace('_', ' ')} model '{model.model_name}'. Clearing cache.")
model.clear_cache()
await run(_load, model)
return model
return await run(_load, model)
async def idle_shutdown_task() -> None:

View File

@@ -5,15 +5,14 @@ from pathlib import Path
from shutil import rmtree
from typing import Any, ClassVar
import onnxruntime as ort
from huggingface_hub import snapshot_download
import ann.ann
from app.models.constants import SUPPORTED_PROVIDERS
from app.sessions.ort import OrtSession
from ..config import clean_name, log, settings
from ..schemas import ModelFormat, ModelIdentity, ModelSession, ModelTask, ModelType
from .ann import AnnSession
from ..sessions.ann import AnnSession
class InferenceModel(ABC):
@@ -24,19 +23,17 @@ class InferenceModel(ABC):
self,
model_name: str,
cache_dir: Path | str | None = None,
providers: list[str] | None = None,
provider_options: list[dict[str, Any]] | None = None,
sess_options: ort.SessionOptions | None = None,
preferred_format: ModelFormat | None = None,
model_format: ModelFormat | None = None,
session: ModelSession | None = None,
**model_kwargs: Any,
) -> None:
self.loaded = False
self.loaded = session is not None
self.load_attempts = 0
self.model_name = clean_name(model_name)
self.cache_dir = Path(cache_dir) if cache_dir is not None else self.cache_dir_default
self.providers = providers if providers is not None else self.providers_default
self.provider_options = provider_options if provider_options is not None else self.provider_options_default
self.sess_options = sess_options if sess_options is not None else self.sess_options_default
self.preferred_format = preferred_format if preferred_format is not None else self.preferred_format_default
self.cache_dir = Path(cache_dir) if cache_dir is not None else self._cache_dir_default
self.model_format = model_format if model_format is not None else self._model_format_default
if session is not None:
self.session = session
def download(self) -> None:
if not self.cached:
@@ -48,9 +45,11 @@ class InferenceModel(ABC):
def load(self) -> None:
if self.loaded:
return
self.load_attempts += 1
self.download()
log.info(f"Loading {self.model_type.replace('-', ' ')} model '{self.model_name}' to memory")
attempt = f"Attempt #{self.load_attempts} to load" if self.load_attempts > 1 else "Loading"
log.info(f"{attempt} {self.model_type.replace('-', ' ')} model '{self.model_name}' to memory")
self.session = self._load()
self.loaded = True
@@ -67,7 +66,7 @@ class InferenceModel(ABC):
pass
def _download(self) -> None:
ignore_patterns = [] if self.preferred_format == ModelFormat.ARMNN else ["*.armnn"]
ignore_patterns = [] if self.model_format == ModelFormat.ARMNN else ["*.armnn"]
snapshot_download(
f"immich-app/{clean_name(self.model_name)}",
cache_dir=self.cache_dir,
@@ -103,25 +102,13 @@ class InferenceModel(ABC):
def _make_session(self, model_path: Path) -> ModelSession:
if not model_path.is_file():
onnx_path = model_path.with_suffix(".onnx")
if not onnx_path.is_file():
raise ValueError(f"Model path '{model_path}' does not exist")
log.warning(
f"Could not find model path '{model_path}'. " f"Falling back to ONNX model path '{onnx_path}' instead.",
)
model_path = onnx_path
raise FileNotFoundError(f"Model file not found: {model_path}")
match model_path.suffix:
case ".armnn":
session = AnnSession(model_path)
session: ModelSession = AnnSession(model_path)
case ".onnx":
session = ort.InferenceSession(
model_path.as_posix(),
sess_options=self.sess_options,
providers=self.providers,
provider_options=self.provider_options,
)
session = OrtSession(model_path)
case _:
raise ValueError(f"Unsupported model file type: {model_path.suffix}")
return session
@@ -132,7 +119,7 @@ class InferenceModel(ABC):
@property
def model_path(self) -> Path:
return self.model_dir / f"model.{self.preferred_format}"
return self.model_dir / f"model.{self.model_format}"
@property
def model_task(self) -> ModelTask:
@@ -151,7 +138,7 @@ class InferenceModel(ABC):
self._cache_dir = cache_dir
@property
def cache_dir_default(self) -> Path:
def _cache_dir_default(self) -> Path:
return settings.cache_folder / self.model_task.value / self.model_name
@property
@@ -159,95 +146,14 @@ class InferenceModel(ABC):
return self.model_path.is_file()
@property
def providers(self) -> list[str]:
return self._providers
def model_format(self) -> ModelFormat:
return self._model_format
@providers.setter
def providers(self, providers: list[str]) -> None:
log.info(
(f"Setting '{self.model_name}' execution providers to {providers}, " "in descending order of preference"),
)
self._providers = providers
@model_format.setter
def model_format(self, model_format: ModelFormat) -> None:
log.debug(f"Setting model format to {model_format}")
self._model_format = model_format
@property
def providers_default(self) -> list[str]:
available_providers = set(ort.get_available_providers())
log.debug(f"Available ORT providers: {available_providers}")
if (openvino := "OpenVINOExecutionProvider") in available_providers:
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
log.debug(f"Available OpenVINO devices: {device_ids}")
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
if not gpu_devices:
log.warning("No GPU device found in OpenVINO. Falling back to CPU.")
available_providers.remove(openvino)
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
@property
def provider_options(self) -> list[dict[str, Any]]:
return self._provider_options
@provider_options.setter
def provider_options(self, provider_options: list[dict[str, Any]]) -> None:
log.debug(f"Setting execution provider options to {provider_options}")
self._provider_options = provider_options
@property
def provider_options_default(self) -> list[dict[str, Any]]:
options = []
for provider in self.providers:
match provider:
case "CPUExecutionProvider" | "CUDAExecutionProvider":
option = {"arena_extend_strategy": "kSameAsRequested"}
case "OpenVINOExecutionProvider":
option = {"device_type": "GPU_FP32", "cache_dir": (self.cache_dir / "openvino").as_posix()}
case _:
option = {}
options.append(option)
return options
@property
def sess_options(self) -> ort.SessionOptions:
return self._sess_options
@sess_options.setter
def sess_options(self, sess_options: ort.SessionOptions) -> None:
log.debug(f"Setting execution_mode to {sess_options.execution_mode.name}")
log.debug(f"Setting inter_op_num_threads to {sess_options.inter_op_num_threads}")
log.debug(f"Setting intra_op_num_threads to {sess_options.intra_op_num_threads}")
self._sess_options = sess_options
@property
def sess_options_default(self) -> ort.SessionOptions:
sess_options = ort.SessionOptions()
sess_options.enable_cpu_mem_arena = False
# avoid thread contention between models
if settings.model_inter_op_threads > 0:
sess_options.inter_op_num_threads = settings.model_inter_op_threads
# these defaults work well for CPU, but bottleneck GPU
elif settings.model_inter_op_threads == 0 and self.providers == ["CPUExecutionProvider"]:
sess_options.inter_op_num_threads = 1
if settings.model_intra_op_threads > 0:
sess_options.intra_op_num_threads = settings.model_intra_op_threads
elif settings.model_intra_op_threads == 0 and self.providers == ["CPUExecutionProvider"]:
sess_options.intra_op_num_threads = 2
if sess_options.inter_op_num_threads > 1:
sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
return sess_options
@property
def preferred_format(self) -> ModelFormat:
return self._preferred_format
@preferred_format.setter
def preferred_format(self, preferred_format: ModelFormat) -> None:
log.debug(f"Setting preferred format to {preferred_format}")
self._preferred_format = preferred_format
@property
def preferred_format_default(self) -> ModelFormat:
def _model_format_default(self) -> ModelFormat:
return ModelFormat.ARMNN if ann.ann.is_available and settings.ann else ModelFormat.ONNX

View File

@@ -22,11 +22,12 @@ class BaseCLIPTextualEncoder(InferenceModel):
return res
def _load(self) -> ModelSession:
session = super()._load()
log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'")
self.tokenizer = self._load_tokenizer()
log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'")
return super()._load()
return session
@abstractmethod
def _load_tokenizer(self) -> Tokenizer:

View File

@@ -1,4 +1,3 @@
from pathlib import Path
from typing import Any
import numpy as np
@@ -14,15 +13,9 @@ class FaceDetector(InferenceModel):
depends = []
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None:
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(model_name, cache_dir, **model_kwargs)
super().__init__(model_name, **model_kwargs)
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)

View File

@@ -3,37 +3,32 @@ from typing import Any
import numpy as np
import onnx
import onnxruntime as ort
from insightface.model_zoo import ArcFaceONNX
from insightface.utils.face_align import norm_crop
from numpy.typing import NDArray
from onnx.tools.update_model_dims import update_inputs_outputs_dims
from PIL import Image
from app.config import clean_name, log
from app.config import log
from app.models.base import InferenceModel
from app.models.transforms import decode_cv2
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelSession, ModelTask, ModelType
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
from app.sessions import has_batch_axis
class FaceRecognizer(InferenceModel):
depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)]
identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
def __init__(
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None:
super().__init__(model_name, **model_kwargs)
self.min_score = model_kwargs.pop("minScore", min_score)
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
self.batch = self.model_format == ModelFormat.ONNX
def _load(self) -> ModelSession:
session = self._make_session(self.model_path)
if not self._has_batch_dim(session):
self._add_batch_dim(self.model_path)
if self.batch and not has_batch_axis(session):
self._add_batch_axis(self.model_path)
session = self._make_session(self.model_path)
self.model = ArcFaceONNX(
self.model_path.with_suffix(".onnx").as_posix(),
@@ -47,9 +42,20 @@ class FaceRecognizer(InferenceModel):
if faces["boxes"].shape[0] == 0:
return []
inputs = decode_cv2(inputs)
embeddings: NDArray[np.float32] = self.model.get_feat(self._crop(inputs, faces))
cropped_faces = self._crop(inputs, faces)
embeddings = self._predict_batch(cropped_faces) if self.batch else self._predict_single(cropped_faces)
return self.postprocess(faces, embeddings)
def _predict_batch(self, cropped_faces: list[NDArray[np.uint8]]) -> NDArray[np.float32]:
embeddings: NDArray[np.float32] = self.model.get_feat(cropped_faces)
return embeddings
def _predict_single(self, cropped_faces: list[NDArray[np.uint8]]) -> NDArray[np.float32]:
embeddings: list[NDArray[np.float32]] = []
for face in cropped_faces:
embeddings.append(self.model.get_feat(face))
return np.concatenate(embeddings, axis=0)
def postprocess(self, faces: FaceDetectionOutput, embeddings: NDArray[np.float32]) -> FacialRecognitionOutput:
return [
{
@@ -63,11 +69,8 @@ class FaceRecognizer(InferenceModel):
def _crop(self, image: NDArray[np.uint8], faces: FaceDetectionOutput) -> list[NDArray[np.uint8]]:
return [norm_crop(image, landmark) for landmark in faces["landmarks"]]
def _has_batch_dim(self, session: ort.InferenceSession) -> bool:
return not isinstance(session, ort.InferenceSession) or session.get_inputs()[0].shape[0] == "batch"
def _add_batch_dim(self, model_path: Path) -> None:
log.debug(f"Adding batch dimension to model {model_path}")
def _add_batch_axis(self, model_path: Path) -> None:
log.debug(f"Adding batch axis to model {model_path}")
proto = onnx.load(model_path)
static_input_dims = [shape.dim_value for shape in proto.graph.input[0].type.tensor_type.shape.dim[1:]]
static_output_dims = [shape.dim_value for shape in proto.graph.output[0].type.tensor_type.shape.dim[1:]]

View File

@@ -54,6 +54,14 @@ class ModelSource(StrEnum):
ModelIdentity = tuple[ModelType, ModelTask]
class SessionNode(Protocol):
@property
def name(self) -> str | None: ...
@property
def shape(self) -> tuple[int, ...]: ...
class ModelSession(Protocol):
def run(
self,
@@ -62,6 +70,10 @@ class ModelSession(Protocol):
run_options: Any = None,
) -> list[npt.NDArray[np.float32]]: ...
def get_inputs(self) -> list[SessionNode]: ...
def get_outputs(self) -> list[SessionNode]: ...
class HasProfiling(Protocol):
profiling: dict[str, float]

View File

@@ -0,0 +1,5 @@
from app.schemas import ModelSession
def has_batch_axis(session: ModelSession) -> bool:
return not isinstance(session.get_inputs()[0].shape[0], int) or session.get_inputs()[0].shape[0] < 0

View File

@@ -7,6 +7,7 @@ import numpy as np
from numpy.typing import NDArray
from ann.ann import Ann
from app.schemas import SessionNode
from ..config import log, settings
@@ -16,27 +17,15 @@ class AnnSession:
Wrapper for ANN to be drop-in replacement for ONNX session.
"""
def __init__(self, model_path: Path):
tuning_file = Path(settings.cache_folder) / "gpu-tuning.ann"
with tuning_file.open(mode="a"):
# make sure tuning file exists (without clearing contents)
# once filled, the tuning file reduces the cost/time of the first
# inference after model load by 10s of seconds
pass
self.ann = Ann(tuning_level=3, tuning_file=tuning_file.as_posix())
log.info("Loading ANN model %s ...", model_path)
cache_file = model_path.with_suffix(".anncache")
save = False
if not cache_file.is_file():
save = True
with cache_file.open(mode="a"):
# create empty model cache file
pass
def __init__(self, model_path: Path, cache_dir: Path = settings.cache_folder) -> None:
self.model_path = model_path
self.cache_dir = cache_dir
self.ann = Ann(tuning_level=3, tuning_file=(cache_dir / "gpu-tuning.ann").as_posix())
log.info("Loading ANN model %s ...", model_path)
self.model = self.ann.load(
model_path.as_posix(),
save_cached_network=save,
cached_network_path=cache_file.as_posix(),
cached_network_path=model_path.with_suffix(".anncache").as_posix(),
)
log.info("Loaded ANN model with ID %d", self.model)
@@ -45,11 +34,11 @@ class AnnSession:
log.info("Unloaded ANN model %d", self.model)
self.ann.destroy()
def get_inputs(self) -> list[AnnNode]:
def get_inputs(self) -> list[SessionNode]:
shapes = self.ann.input_shapes[self.model]
return [AnnNode(None, s) for s in shapes]
def get_outputs(self) -> list[AnnNode]:
def get_outputs(self) -> list[SessionNode]:
shapes = self.ann.output_shapes[self.model]
return [AnnNode(None, s) for s in shapes]

View File

@@ -0,0 +1,129 @@
from __future__ import annotations
from pathlib import Path
from typing import Any
import numpy as np
import onnxruntime as ort
from numpy.typing import NDArray
from app.models.constants import SUPPORTED_PROVIDERS
from app.schemas import SessionNode
from ..config import log, settings
class OrtSession:
def __init__(
self,
model_path: Path | str,
providers: list[str] | None = None,
provider_options: list[dict[str, Any]] | None = None,
sess_options: ort.SessionOptions | None = None,
):
self.model_path = Path(model_path)
self.providers = providers if providers is not None else self._providers_default
self.provider_options = provider_options if provider_options is not None else self._provider_options_default
self.sess_options = sess_options if sess_options is not None else self._sess_options_default
self.session = ort.InferenceSession(
self.model_path.as_posix(),
providers=self.providers,
provider_options=self.provider_options,
sess_options=self.sess_options,
)
def get_inputs(self) -> list[SessionNode]:
inputs: list[SessionNode] = self.session.get_inputs()
return inputs
def get_outputs(self) -> list[SessionNode]:
outputs: list[SessionNode] = self.session.get_outputs()
return outputs
def run(
self,
output_names: list[str] | None,
input_feed: dict[str, NDArray[np.float32]] | dict[str, NDArray[np.int32]],
run_options: Any = None,
) -> list[NDArray[np.float32]]:
outputs: list[NDArray[np.float32]] = self.session.run(output_names, input_feed, run_options)
return outputs
@property
def providers(self) -> list[str]:
return self._providers
@providers.setter
def providers(self, providers: list[str]) -> None:
log.info(f"Setting execution providers to {providers}, in descending order of preference")
self._providers = providers
@property
def _providers_default(self) -> list[str]:
available_providers = set(ort.get_available_providers())
log.debug(f"Available ORT providers: {available_providers}")
if (openvino := "OpenVINOExecutionProvider") in available_providers:
device_ids: list[str] = ort.capi._pybind_state.get_available_openvino_device_ids()
log.debug(f"Available OpenVINO devices: {device_ids}")
gpu_devices = [device_id for device_id in device_ids if device_id.startswith("GPU")]
if not gpu_devices:
log.warning("No GPU device found in OpenVINO. Falling back to CPU.")
available_providers.remove(openvino)
return [provider for provider in SUPPORTED_PROVIDERS if provider in available_providers]
@property
def provider_options(self) -> list[dict[str, Any]]:
return self._provider_options
@provider_options.setter
def provider_options(self, provider_options: list[dict[str, Any]]) -> None:
log.debug(f"Setting execution provider options to {provider_options}")
self._provider_options = provider_options
@property
def _provider_options_default(self) -> list[dict[str, Any]]:
options = []
for provider in self.providers:
match provider:
case "CPUExecutionProvider" | "CUDAExecutionProvider":
option = {"arena_extend_strategy": "kSameAsRequested"}
case "OpenVINOExecutionProvider":
option = {"device_type": "GPU_FP32", "cache_dir": (self.model_path.parent / "openvino").as_posix()}
case _:
option = {}
options.append(option)
return options
@property
def sess_options(self) -> ort.SessionOptions:
return self._sess_options
@sess_options.setter
def sess_options(self, sess_options: ort.SessionOptions) -> None:
log.debug(f"Setting execution_mode to {sess_options.execution_mode.name}")
log.debug(f"Setting inter_op_num_threads to {sess_options.inter_op_num_threads}")
log.debug(f"Setting intra_op_num_threads to {sess_options.intra_op_num_threads}")
self._sess_options = sess_options
@property
def _sess_options_default(self) -> ort.SessionOptions:
sess_options = ort.SessionOptions()
sess_options.enable_cpu_mem_arena = False
# avoid thread contention between models
if settings.model_inter_op_threads > 0:
sess_options.inter_op_num_threads = settings.model_inter_op_threads
# these defaults work well for CPU, but bottleneck GPU
elif settings.model_inter_op_threads == 0 and self.providers == ["CPUExecutionProvider"]:
sess_options.inter_op_num_threads = 1
if settings.model_intra_op_threads > 0:
sess_options.intra_op_num_threads = settings.model_intra_op_threads
elif settings.model_intra_op_threads == 0 and self.providers == ["CPUExecutionProvider"]:
sess_options.intra_op_num_threads = 2
if sess_options.inter_op_num_threads > 1:
sess_options.execution_mode = ort.ExecutionMode.ORT_PARALLEL
return sess_options

View File

@@ -11,6 +11,7 @@ import cv2
import numpy as np
import onnxruntime as ort
import pytest
from fastapi import HTTPException
from fastapi.testclient import TestClient
from PIL import Image
from pytest import MonkeyPatch
@@ -21,129 +22,16 @@ from app.models.clip.textual import MClipTextualEncoder, OpenClipTextualEncoder
from app.models.clip.visual import OpenClipVisualEncoder
from app.models.facial_recognition.detection import FaceDetector
from app.models.facial_recognition.recognition import FaceRecognizer
from app.sessions.ann import AnnSession
from app.sessions.ort import OrtSession
from .config import Settings, log, settings
from .config import Settings, settings
from .models.base import InferenceModel
from .models.cache import ModelCache
from .schemas import ModelFormat, ModelTask, ModelType
class TestBase:
CPU_EP = ["CPUExecutionProvider"]
CUDA_EP = ["CUDAExecutionProvider", "CPUExecutionProvider"]
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
@pytest.mark.providers(CPU_EP)
def test_sets_cpu_provider(self, providers: list[str]) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP)
def test_sets_cuda_provider_if_available(self, providers: list[str]) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP
@pytest.mark.providers(OV_EP)
def test_sets_openvino_provider_if_available(self, providers: list[str], mocker: MockerFixture) -> None:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.OV_EP
@pytest.mark.providers(OV_EP)
def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], mocker: MockerFixture) -> None:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["CPU"]
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP
@pytest.mark.providers(TRT_EP)
def test_ignores_unsupported_providers(self, providers: list[str]) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.providers == self.CUDA_EP
def test_sets_provider_kwarg(self) -> None:
providers = ["CUDAExecutionProvider"]
encoder = OpenClipTextualEncoder("ViT-B-32__openai", providers=providers)
assert encoder.providers == providers
def test_sets_default_provider_options(self, mocker: MockerFixture) -> None:
mocked = mocker.patch("app.models.base.ort.capi._pybind_state")
mocked.get_available_openvino_device_ids.return_value = ["GPU.0", "CPU"]
encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.provider_options == [
{"device_type": "GPU_FP32", "cache_dir": (encoder.cache_dir / "openvino").as_posix()},
{"arena_extend_strategy": "kSameAsRequested"},
]
def test_sets_provider_options_kwarg(self) -> None:
encoder = OpenClipTextualEncoder(
"ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[],
)
assert encoder.provider_options == []
def test_sets_default_sess_options(self) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL
assert encoder.sess_options.inter_op_num_threads == 1
assert encoder.sess_options.intra_op_num_threads == 2
assert encoder.sess_options.enable_cpu_mem_arena is False
def test_sets_default_sess_options_does_not_set_threads_if_non_cpu_and_default_threads(self) -> None:
encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.sess_options.inter_op_num_threads == 0
assert encoder.sess_options.intra_op_num_threads == 0
def test_sets_default_sess_options_sets_threads_if_non_cpu_and_set_threads(self, mocker: MockerFixture) -> None:
mock_settings = mocker.patch("app.models.base.settings", autospec=True)
mock_settings.model_inter_op_threads = 2
mock_settings.model_intra_op_threads = 4
encoder = OpenClipTextualEncoder(
"ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"]
)
assert encoder.sess_options.inter_op_num_threads == 2
assert encoder.sess_options.intra_op_num_threads == 4
def test_sets_sess_options_kwarg(self) -> None:
sess_options = ort.SessionOptions()
encoder = OpenClipTextualEncoder(
"ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[],
sess_options=sess_options,
)
assert sess_options is encoder.sess_options
def test_sets_default_cache_dir(self) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
@@ -155,29 +43,30 @@ class TestBase:
assert encoder.cache_dir == cache_dir
def test_sets_default_preferred_format(self, mocker: MockerFixture) -> None:
def test_sets_default_model_format(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", False)
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
assert encoder.preferred_format == ModelFormat.ONNX
assert encoder.model_format == ModelFormat.ONNX
def test_sets_default_preferred_format_to_armnn_if_available(self, mocker: MockerFixture) -> None:
def test_sets_default_model_format_to_armnn_if_available(self, path: mock.Mock, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", True)
path.suffix = ".armnn"
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
assert encoder.preferred_format == ModelFormat.ARMNN
assert encoder.model_format == ModelFormat.ARMNN
def test_sets_preferred_format_kwarg(self, mocker: MockerFixture) -> None:
def test_sets_model_format_kwarg(self, mocker: MockerFixture) -> None:
mocker.patch.object(settings, "ann", False)
mocker.patch("ann.ann.is_available", False)
encoder = OpenClipTextualEncoder("ViT-B-32__openai", preferred_format=ModelFormat.ARMNN)
encoder = OpenClipTextualEncoder("ViT-B-32__openai", model_format=ModelFormat.ARMNN)
assert encoder.preferred_format == ModelFormat.ARMNN
assert encoder.model_format == ModelFormat.ARMNN
def test_casts_cache_dir_string_to_path(self) -> None:
cache_dir = "/test_cache"
@@ -185,120 +74,53 @@ class TestBase:
assert encoder.cache_dir == Path(cache_dir)
def test_clear_cache(self, mocker: MockerFixture) -> None:
mock_rmtree = mocker.patch("app.models.base.rmtree", autospec=True)
mock_rmtree.avoids_symlink_attacks = True
mock_cache_dir = mocker.Mock()
mock_cache_dir.exists.return_value = True
mock_cache_dir.is_dir.return_value = True
mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
info = mocker.spy(log, "info")
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
def test_clear_cache(self, rmtree: mock.Mock, path: mock.Mock, info: mock.Mock) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
encoder.clear_cache()
mock_rmtree.assert_called_once_with(encoder.cache_dir)
rmtree.assert_called_once_with(encoder.cache_dir)
info.assert_called_with(f"Cleared cache directory for model '{encoder.model_name}'.")
def test_clear_cache_warns_if_path_does_not_exist(self, mocker: MockerFixture) -> None:
mock_rmtree = mocker.patch("app.models.base.rmtree", autospec=True)
mock_rmtree.avoids_symlink_attacks = True
mock_cache_dir = mocker.Mock()
mock_cache_dir.exists.return_value = False
mock_cache_dir.is_dir.return_value = True
mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
warning = mocker.spy(log, "warning")
def test_clear_cache_warns_if_path_does_not_exist(
self, rmtree: mock.Mock, path: mock.Mock, warning: mock.Mock
) -> None:
path.return_value.exists.return_value = False
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
encoder.clear_cache()
mock_rmtree.assert_not_called()
rmtree.assert_not_called()
warning.assert_called_once()
def test_clear_cache_raises_exception_if_vulnerable_to_symlink_attack(self, mocker: MockerFixture) -> None:
mock_rmtree = mocker.patch("app.models.base.rmtree", autospec=True)
mock_rmtree.avoids_symlink_attacks = False
mock_cache_dir = mocker.Mock()
mock_cache_dir.exists.return_value = True
mock_cache_dir.is_dir.return_value = True
mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
def test_clear_cache_raises_exception_if_vulnerable_to_symlink_attack(
self, rmtree: mock.Mock, path: mock.Mock
) -> None:
rmtree.avoids_symlink_attacks = False
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
with pytest.raises(RuntimeError):
encoder.clear_cache()
mock_rmtree.assert_not_called()
rmtree.assert_not_called()
def test_clear_cache_replaces_file_with_dir_if_path_is_file(self, mocker: MockerFixture) -> None:
mock_rmtree = mocker.patch("app.models.base.rmtree", autospec=True)
mock_rmtree.avoids_symlink_attacks = True
mock_cache_dir = mocker.Mock()
mock_cache_dir.exists.return_value = True
mock_cache_dir.is_dir.return_value = False
mocker.patch("app.models.base.Path", return_value=mock_cache_dir)
warning = mocker.spy(log, "warning")
def test_clear_cache_replaces_file_with_dir_if_path_is_file(
self, rmtree: mock.Mock, path: mock.Mock, warning: mock.Mock
) -> None:
path.return_value.is_dir.return_value = False
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=mock_cache_dir)
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
encoder.clear_cache()
mock_rmtree.assert_not_called()
mock_cache_dir.unlink.assert_called_once()
mock_cache_dir.mkdir.assert_called_once()
rmtree.assert_not_called()
path.return_value.unlink.assert_called_once()
path.return_value.mkdir.assert_called_once()
warning.assert_called_once()
def test_make_session_return_ann_if_available(self, mocker: MockerFixture) -> None:
mock_model_path = mocker.Mock()
mock_model_path.is_file.return_value = True
mock_model_path.suffix = ".armnn"
mock_model_path.with_suffix.return_value = mock_model_path
mock_ann = mocker.patch("app.models.base.AnnSession")
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
encoder._make_session(mock_model_path)
mock_ann.assert_called_once()
def test_make_session_return_ort_if_available_and_ann_is_not(self, mocker: MockerFixture) -> None:
mock_armnn_path = mocker.Mock()
mock_armnn_path.is_file.return_value = False
mock_armnn_path.suffix = ".armnn"
mock_onnx_path = mocker.Mock()
mock_onnx_path.is_file.return_value = True
mock_onnx_path.suffix = ".onnx"
mock_armnn_path.with_suffix.return_value = mock_onnx_path
mock_ann = mocker.patch("app.models.base.AnnSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
encoder._make_session(mock_armnn_path)
mock_ort.assert_called_once()
mock_ann.assert_not_called()
def test_make_session_raises_exception_if_path_does_not_exist(self, mocker: MockerFixture) -> None:
mock_model_path = mocker.Mock()
mock_model_path.is_file.return_value = False
mock_model_path.suffix = ".onnx"
mock_model_path.with_suffix.return_value = mock_model_path
mock_ann = mocker.patch("app.models.base.AnnSession")
mock_ort = mocker.patch("app.models.base.ort.InferenceSession")
encoder = OpenClipTextualEncoder("ViT-B-32__openai")
with pytest.raises(ValueError):
encoder._make_session(mock_model_path)
mock_ann.assert_not_called()
mock_ort.assert_not_called()
def test_download(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
def test_download(self, snapshot_download: mock.Mock) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="/path/to/cache")
encoder.download()
mock_snapshot_download.assert_called_once_with(
snapshot_download.assert_called_once_with(
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
@@ -306,13 +128,11 @@ class TestBase:
ignore_patterns=["*.armnn"],
)
def test_download_downloads_armnn_if_preferred_format(self, mocker: MockerFixture) -> None:
mock_snapshot_download = mocker.patch("app.models.base.snapshot_download")
encoder = OpenClipTextualEncoder("ViT-B-32__openai", preferred_format=ModelFormat.ARMNN)
def test_download_downloads_armnn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai", model_format=ModelFormat.ARMNN)
encoder.download()
mock_snapshot_download.assert_called_once_with(
snapshot_download.assert_called_once_with(
"immich-app/ViT-B-32__openai",
cache_dir=encoder.cache_dir,
local_dir=encoder.cache_dir,
@@ -320,6 +140,180 @@ class TestBase:
ignore_patterns=[],
)
def test_throws_exception_if_model_path_does_not_exist(
self, snapshot_download: mock.Mock, ort_session: mock.Mock, path: mock.Mock
) -> None:
path.return_value.__truediv__.return_value.__truediv__.return_value.is_file.return_value = False
encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir=path)
with pytest.raises(FileNotFoundError):
encoder.load()
snapshot_download.assert_called_once()
ort_session.assert_not_called()
@pytest.mark.usefixtures("ort_session")
class TestOrtSession:
CPU_EP = ["CPUExecutionProvider"]
CUDA_EP = ["CUDAExecutionProvider", "CPUExecutionProvider"]
OV_EP = ["OpenVINOExecutionProvider", "CPUExecutionProvider"]
CUDA_EP_OUT_OF_ORDER = ["CPUExecutionProvider", "CUDAExecutionProvider"]
TRT_EP = ["TensorrtExecutionProvider", "CUDAExecutionProvider", "CPUExecutionProvider"]
@pytest.mark.providers(CPU_EP)
def test_sets_cpu_provider(self, providers: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP)
def test_sets_cuda_provider_if_available(self, providers: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.CUDA_EP
@pytest.mark.ov_device_ids(["GPU.0", "CPU"])
@pytest.mark.providers(OV_EP)
def test_sets_openvino_provider_if_available(self, providers: list[str], ov_device_ids: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.OV_EP
@pytest.mark.ov_device_ids(["CPU"])
@pytest.mark.providers(OV_EP)
def test_avoids_openvino_if_gpu_not_available(self, providers: list[str], ov_device_ids: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.CPU_EP
@pytest.mark.providers(CUDA_EP_OUT_OF_ORDER)
def test_sets_providers_in_correct_order(self, providers: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.CUDA_EP
@pytest.mark.providers(TRT_EP)
def test_ignores_unsupported_providers(self, providers: list[str]) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.providers == self.CUDA_EP
def test_sets_provider_kwarg(self) -> None:
providers = ["CUDAExecutionProvider"]
session = OrtSession("ViT-B-32__openai", providers=providers)
assert session.providers == providers
@pytest.mark.ov_device_ids(["GPU.0", "CPU"])
def test_sets_default_provider_options(self, ov_device_ids: list[str]) -> None:
model_path = "/cache/ViT-B-32__openai/model.onnx"
session = OrtSession(model_path, providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"])
assert session.provider_options == [
{"device_type": "GPU_FP32", "cache_dir": "/cache/ViT-B-32__openai/openvino"},
{"arena_extend_strategy": "kSameAsRequested"},
]
def test_sets_provider_options_kwarg(self) -> None:
session = OrtSession(
"ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[],
)
assert session.provider_options == []
def test_sets_default_sess_options(self) -> None:
session = OrtSession("ViT-B-32__openai")
assert session.sess_options.execution_mode == ort.ExecutionMode.ORT_SEQUENTIAL
assert session.sess_options.inter_op_num_threads == 1
assert session.sess_options.intra_op_num_threads == 2
assert session.sess_options.enable_cpu_mem_arena is False
def test_sets_default_sess_options_does_not_set_threads_if_non_cpu_and_default_threads(self) -> None:
session = OrtSession("ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
assert session.sess_options.inter_op_num_threads == 0
assert session.sess_options.intra_op_num_threads == 0
def test_sets_default_sess_options_sets_threads_if_non_cpu_and_set_threads(self, mocker: MockerFixture) -> None:
mock_settings = mocker.patch("app.sessions.ort.settings", autospec=True)
mock_settings.model_inter_op_threads = 2
mock_settings.model_intra_op_threads = 4
session = OrtSession("ViT-B-32__openai", providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
assert session.sess_options.inter_op_num_threads == 2
assert session.sess_options.intra_op_num_threads == 4
def test_sets_sess_options_kwarg(self) -> None:
sess_options = ort.SessionOptions()
session = OrtSession(
"ViT-B-32__openai",
providers=["OpenVINOExecutionProvider", "CPUExecutionProvider"],
provider_options=[],
sess_options=sess_options,
)
assert sess_options is session.sess_options
class TestAnnSession:
def test_creates_ann_session(self, ann_session: mock.Mock, info: mock.Mock) -> None:
model_path = mock.MagicMock(spec=Path)
cache_dir = mock.MagicMock(spec=Path)
AnnSession(model_path, cache_dir)
ann_session.assert_called_once_with(tuning_level=3, tuning_file=(cache_dir / "gpu-tuning.ann").as_posix())
ann_session.return_value.load.assert_called_once_with(
model_path.as_posix(), cached_network_path=model_path.with_suffix(".anncache").as_posix()
)
info.assert_has_calls(
[
mock.call("Loading ANN model %s ...", model_path),
mock.call("Loaded ANN model with ID %d", ann_session.return_value.load.return_value),
]
)
def test_get_inputs(self, ann_session: mock.Mock) -> None:
ann_session.return_value.load.return_value = 123
ann_session.return_value.input_shapes = {123: [(1, 3, 224, 224)]}
session = AnnSession(Path("ViT-B-32__openai"))
inputs = session.get_inputs()
assert len(inputs) == 1
assert inputs[0].name is None
assert inputs[0].shape == (1, 3, 224, 224)
def test_get_outputs(self, ann_session: mock.Mock) -> None:
ann_session.return_value.load.return_value = 123
ann_session.return_value.output_shapes = {123: [(1, 3, 224, 224)]}
session = AnnSession(Path("ViT-B-32__openai"))
outputs = session.get_outputs()
assert len(outputs) == 1
assert outputs[0].name is None
assert outputs[0].shape == (1, 3, 224, 224)
def test_run(self, ann_session: mock.Mock, mocker: MockerFixture) -> None:
ann_session.return_value.load.return_value = 123
np_spy = mocker.spy(np, "ascontiguousarray")
session = AnnSession(Path("ViT-B-32__openai"))
[input1, input2] = [np.random.rand(1, 3, 224, 224).astype(np.float32) for _ in range(2)]
input_feed = {"input.1": input1, "input.2": input2}
session.run(None, input_feed)
ann_session.return_value.execute.assert_called_once_with(123, [input1, input2])
np_spy.call_count == 2
np_spy.assert_has_calls([mock.call(input1), mock.call(input2)])
class TestCLIP:
embedding = np.random.rand(512).astype(np.float32)
@@ -486,6 +480,88 @@ class TestFaceRecognition:
assert isinstance(call_args[0][0], np.ndarray)
assert call_args[0][0].shape == (112, 112, 3)
def test_recognition_adds_batch_axis_for_ort(
self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
) -> None:
onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch(
"app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
)
mocker.patch("app.models.base.InferenceModel.download")
mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX")
ort_session.return_value.get_inputs.return_value = [SimpleNamespace(name="input.1", shape=(1, 3, 224, 224))]
ort_session.return_value.get_outputs.return_value = [SimpleNamespace(name="output.1", shape=(1, 800))]
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
proto = mock.Mock()
input_dims = mock.Mock()
input_dims.name = "input.1"
input_dims.type.tensor_type.shape.dim = [SimpleNamespace(dim_value=size) for size in [1, 3, 224, 224]]
proto.graph.input = [input_dims]
output_dims = mock.Mock()
output_dims.name = "output.1"
output_dims.type.tensor_type.shape.dim = [SimpleNamespace(dim_value=size) for size in [1, 800]]
proto.graph.output = [output_dims]
onnx.load.return_value = proto
face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
face_recognizer.load()
assert face_recognizer.batch is True
update_dims.assert_called_once_with(proto, {"input.1": ["batch", 3, 224, 224]}, {"output.1": ["batch", 800]})
onnx.save.assert_called_once_with(update_dims.return_value, face_recognizer.model_path)
def test_recognition_does_not_add_batch_axis_if_exists(
self, ort_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
) -> None:
onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch(
"app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
)
mocker.patch("app.models.base.InferenceModel.download")
mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX")
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".onnx"
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))]
ort_session.return_value.get_inputs.return_value = inputs
ort_session.return_value.get_outputs.return_value = outputs
face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
face_recognizer.load()
assert face_recognizer.batch is True
update_dims.assert_not_called()
onnx.load.assert_not_called()
onnx.save.assert_not_called()
def test_recognition_does_not_add_batch_axis_for_armnn(
self, ann_session: mock.Mock, path: mock.Mock, mocker: MockerFixture
) -> None:
onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch(
"app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
)
mocker.patch("app.models.base.InferenceModel.download")
mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX")
path.return_value.__truediv__.return_value.__truediv__.return_value.suffix = ".armnn"
inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))]
ann_session.return_value.get_inputs.return_value = inputs
ann_session.return_value.get_outputs.return_value = outputs
face_recognizer = FaceRecognizer("buffalo_s", model_format=ModelFormat.ARMNN, cache_dir=path)
face_recognizer.load()
assert face_recognizer.batch is False
update_dims.assert_not_called()
onnx.load.assert_not_called()
onnx.save.assert_not_called()
@pytest.mark.asyncio
class TestCache:
@@ -627,6 +703,7 @@ class TestLoad:
async def test_load(self) -> None:
mock_model = mock.Mock(spec=InferenceModel)
mock_model.loaded = False
mock_model.load_attempts = 0
res = await load(mock_model)
@@ -650,6 +727,7 @@ class TestLoad:
mock_model.model_task = ModelTask.SEARCH
mock_model.load.side_effect = [OSError, None]
mock_model.loaded = False
mock_model.load_attempts = 0
res = await load(mock_model)
@@ -657,6 +735,41 @@ class TestLoad:
mock_model.clear_cache.assert_called_once()
assert mock_model.load.call_count == 2
async def test_load_raises_if_os_error_and_already_retried(self) -> None:
mock_model = mock.Mock(spec=InferenceModel)
mock_model.model_name = "test_model_name"
mock_model.model_type = ModelType.VISUAL
mock_model.model_task = ModelTask.SEARCH
mock_model.loaded = False
mock_model.load_attempts = 2
with pytest.raises(HTTPException):
await load(mock_model)
mock_model.clear_cache.assert_not_called()
mock_model.load.assert_not_called()
async def test_falls_back_to_onnx_if_other_format_does_not_exist(
self, exception: mock.Mock, warning: mock.Mock
) -> None:
mock_model = mock.Mock(spec=InferenceModel)
mock_model.model_name = "test_model_name"
mock_model.model_type = ModelType.VISUAL
mock_model.model_task = ModelTask.SEARCH
mock_model.model_format = ModelFormat.ARMNN
mock_model.loaded = False
mock_model.load_attempts = 0
error = FileNotFoundError()
mock_model.load.side_effect = [error, None]
await load(mock_model)
mock_model.clear_cache.assert_not_called()
assert mock_model.load.call_count == 2
exception.assert_called_once_with(error)
warning.assert_called_once_with("ARMNN is available, but model 'test_model_name' does not support it.")
mock_model.model_format = ModelFormat.ONNX
@pytest.mark.skipif(
not settings.test_full,

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:4688551ffd61358d5bebfd88e0aac12d5b4aed7a153c170dbc435da453476a13 as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:333f7598ff2c2400fb10bfe057709c68b7daab5d847143af85abcf224a07271a as builder
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
[[package]]
name = "aiocache"
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.23.3"
version = "0.23.4"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.23.3-py3-none-any.whl", hash = "sha256:22222c41223f1b7c209ae5511d2d82907325a0e3cdbce5f66949d43c598ff3bc"},
{file = "huggingface_hub-0.23.3.tar.gz", hash = "sha256:1a1118a0b3dea3bab6c325d71be16f5ffe441d32f3ac7c348d6875911b694b5b"},
{file = "huggingface_hub-0.23.4-py3-none-any.whl", hash = "sha256:3a0b957aa87150addf0cc7bd71b4d954b78e749850e1e7fb29ebbd2db64ca037"},
{file = "huggingface_hub-0.23.4.tar.gz", hash = "sha256:35d99016433900e44ae7efe1c209164a5a81dbbcd53a52f99c281dcd7ce22431"},
]
[package.dependencies]
@@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.28.0"
version = "2.29.0"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.28.0-py3-none-any.whl", hash = "sha256:766be879db030c0118e7d9fca712f3538c4e628bdebf59468fa1c6c2fab217d3"},
{file = "locust-2.28.0.tar.gz", hash = "sha256:260557eec866f7e34a767b6c916b5b278167562a280480aadb88f43d962fbdeb"},
{file = "locust-2.29.0-py3-none-any.whl", hash = "sha256:aa9d94d3604ed9f2aab3248460d91e55d3de980a821dffdf8658b439b049d03f"},
{file = "locust-2.29.0.tar.gz", hash = "sha256:649c99ce49d00720a3084c0109547035ad9021222835386599a8b545d31ebe51"},
]
[package.dependencies]
@@ -1550,7 +1550,10 @@ msgpack = ">=1.0.0"
psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
pyzmq = ">=25.0.0"
requests = ">=2.26.0"
requests = [
{version = ">=2.32.2", markers = "python_version > \"3.11\""},
{version = ">=2.26.0", markers = "python_version <= \"3.11\""},
]
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
Werkzeug = ">=2.0.0"
@@ -2054,80 +2057,81 @@ sympy = "*"
[[package]]
name = "opencv-python-headless"
version = "4.10.0.82"
version = "4.10.0.84"
description = "Wrapper package for OpenCV python bindings."
optional = false
python-versions = ">=3.6"
files = [
{file = "opencv-python-headless-4.10.0.82.tar.gz", hash = "sha256:de9e742c1b9540816fbd115b0b03841d41ed0c65566b0d7a5371f98b131b7e6d"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a09ed50ba21cc5bf5d436cb0e784ad09c692d6b1d1454252772f6c8f2c7b4088"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:977a5fd21e1fe0d3d2134887db4441f8725abeae95150126302f31fcd9f548fa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4ec6755838b0be12510bfc9ffb014779c612418f11f4f7e6f505c36124a3aa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37fa5276967ecf6eb297295b16b28b7a2eb3b568ca0ee469fb1a5954de298"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:94736e9b322d13db4768fd35588ad5e8995e78e207263076bfbee18aac835ad5"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:c1822fa23d1641c0249ed5eb906f4c385f7959ff1bd601a776d56b0c18914af4"},
{file = "opencv-python-headless-4.10.0.84.tar.gz", hash = "sha256:f2017c6101d7c2ef8d7bc3b414c37ff7f54d64413a1847d89970b6b7069b4e1a"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a4f4bcb07d8f8a7704d9c8564c224c8b064c63f430e95b61ac0bffaa374d330e"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:5ae454ebac0eb0a0b932e3406370aaf4212e6a3fdb5038cc86c7aea15a6851da"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46071015ff9ab40fccd8a163da0ee14ce9846349f06c6c8c0f2870856ffa45db"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:377d08a7e48a1405b5e84afcbe4798464ce7ee17081c1c23619c8b398ff18295"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:9092404b65458ed87ce932f613ffbb1106ed2c843577501e5768912360fc50ec"},
{file = "opencv_python_headless-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:afcf28bd1209dd58810d33defb622b325d3cbe49dcd7a43a902982c33e5fad05"},
]
[package.dependencies]
numpy = [
{version = ">=1.23.5", markers = "python_version >= \"3.11\""},
{version = ">=1.26.0", markers = "python_version >= \"3.12\""},
{version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
{version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""},
{version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""},
]
[[package]]
name = "orjson"
version = "3.10.3"
version = "3.10.5"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
{file = "orjson-3.10.3-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9fb6c3f9f5490a3eb4ddd46fc1b6eadb0d6fc16fb3f07320149c3286a1409dd8"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:252124b198662eee80428f1af8c63f7ff077c88723fe206a25df8dc57a57b1fa"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9f3e87733823089a338ef9bbf363ef4de45e5c599a9bf50a7a9b82e86d0228da"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c8334c0d87103bb9fbbe59b78129f1f40d1d1e8355bbed2ca71853af15fa4ed3"},
{file = "orjson-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1952c03439e4dce23482ac846e7961f9d4ec62086eb98ae76d97bd41d72644d7"},
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c0403ed9c706dcd2809f1600ed18f4aae50be263bd7112e54b50e2c2bc3ebd6d"},
{file = "orjson-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:382e52aa4270a037d41f325e7d1dfa395b7de0c367800b6f337d8157367bf3a7"},
{file = "orjson-3.10.3-cp310-none-win32.whl", hash = "sha256:be2aab54313752c04f2cbaab4515291ef5af8c2256ce22abc007f89f42f49109"},
{file = "orjson-3.10.3-cp310-none-win_amd64.whl", hash = "sha256:416b195f78ae461601893f482287cee1e3059ec49b4f99479aedf22a20b1098b"},
{file = "orjson-3.10.3-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:73100d9abbbe730331f2242c1fc0bcb46a3ea3b4ae3348847e5a141265479700"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:544a12eee96e3ab828dbfcb4d5a0023aa971b27143a1d35dc214c176fdfb29b3"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:520de5e2ef0b4ae546bea25129d6c7c74edb43fc6cf5213f511a927f2b28148b"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ccaa0a401fc02e8828a5bedfd80f8cd389d24f65e5ca3954d72c6582495b4bcf"},
{file = "orjson-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a7bc9e8bc11bac40f905640acd41cbeaa87209e7e1f57ade386da658092dc16"},
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:3582b34b70543a1ed6944aca75e219e1192661a63da4d039d088a09c67543b08"},
{file = "orjson-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c23dfa91481de880890d17aa7b91d586a4746a4c2aa9a145bebdbaf233768d5"},
{file = "orjson-3.10.3-cp311-none-win32.whl", hash = "sha256:1770e2a0eae728b050705206d84eda8b074b65ee835e7f85c919f5705b006c9b"},
{file = "orjson-3.10.3-cp311-none-win_amd64.whl", hash = "sha256:93433b3c1f852660eb5abdc1f4dd0ced2be031ba30900433223b28ee0140cde5"},
{file = "orjson-3.10.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:a39aa73e53bec8d410875683bfa3a8edf61e5a1c7bb4014f65f81d36467ea098"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0943a96b3fa09bee1afdfccc2cb236c9c64715afa375b2af296c73d91c23eab2"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e852baafceff8da3c9defae29414cc8513a1586ad93e45f27b89a639c68e8176"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18566beb5acd76f3769c1d1a7ec06cdb81edc4d55d2765fb677e3eaa10fa99e0"},
{file = "orjson-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bd2218d5a3aa43060efe649ec564ebedec8ce6ae0a43654b81376216d5ebd42"},
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cf20465e74c6e17a104ecf01bf8cd3b7b252565b4ccee4548f18b012ff2f8069"},
{file = "orjson-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ba7f67aa7f983c4345eeda16054a4677289011a478ca947cd69c0a86ea45e534"},
{file = "orjson-3.10.3-cp312-none-win32.whl", hash = "sha256:17e0713fc159abc261eea0f4feda611d32eabc35708b74bef6ad44f6c78d5ea0"},
{file = "orjson-3.10.3-cp312-none-win_amd64.whl", hash = "sha256:4c895383b1ec42b017dd2c75ae8a5b862fc489006afde06f14afbdd0309b2af0"},
{file = "orjson-3.10.3-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:be2719e5041e9fb76c8c2c06b9600fe8e8584e6980061ff88dcbc2691a16d20d"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb0175a5798bdc878956099f5c54b9837cb62cfbf5d0b86ba6d77e43861bcec2"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:978be58a68ade24f1af7758626806e13cff7748a677faf95fbb298359aa1e20d"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:16bda83b5c61586f6f788333d3cf3ed19015e3b9019188c56983b5a299210eb5"},
{file = "orjson-3.10.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ad1f26bea425041e0a1adad34630c4825a9e3adec49079b1fb6ac8d36f8b754"},
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:9e253498bee561fe85d6325ba55ff2ff08fb5e7184cd6a4d7754133bd19c9195"},
{file = "orjson-3.10.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0a62f9968bab8a676a164263e485f30a0b748255ee2f4ae49a0224be95f4532b"},
{file = "orjson-3.10.3-cp38-none-win32.whl", hash = "sha256:8d0b84403d287d4bfa9bf7d1dc298d5c1c5d9f444f3737929a66f2fe4fb8f134"},
{file = "orjson-3.10.3-cp38-none-win_amd64.whl", hash = "sha256:8bc7a4df90da5d535e18157220d7915780d07198b54f4de0110eca6b6c11e290"},
{file = "orjson-3.10.3-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:9059d15c30e675a58fdcd6f95465c1522b8426e092de9fff20edebfdc15e1cb0"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d40c7f7938c9c2b934b297412c067936d0b54e4b8ab916fd1a9eb8f54c02294"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a654ec1de8fdaae1d80d55cee65893cb06494e124681ab335218be6a0691e7"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:831c6ef73f9aa53c5f40ae8f949ff7681b38eaddb6904aab89dca4d85099cb78"},
{file = "orjson-3.10.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99b880d7e34542db89f48d14ddecbd26f06838b12427d5a25d71baceb5ba119d"},
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2e5e176c994ce4bd434d7aafb9ecc893c15f347d3d2bbd8e7ce0b63071c52e25"},
{file = "orjson-3.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b69a58a37dab856491bf2d3bbf259775fdce262b727f96aafbda359cb1d114d8"},
{file = "orjson-3.10.3-cp39-none-win32.whl", hash = "sha256:b8d4d1a6868cde356f1402c8faeb50d62cee765a1f7ffcfd6de732ab0581e063"},
{file = "orjson-3.10.3-cp39-none-win_amd64.whl", hash = "sha256:5102f50c5fc46d94f2033fe00d392588564378260d64377aec702f21a7a22912"},
{file = "orjson-3.10.3.tar.gz", hash = "sha256:2b166507acae7ba2f7c315dcf185a9111ad5e992ac81f2d507aac39193c2c818"},
{file = "orjson-3.10.5-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:545d493c1f560d5ccfc134803ceb8955a14c3fcb47bbb4b2fee0232646d0b932"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4324929c2dd917598212bfd554757feca3e5e0fa60da08be11b4aa8b90013c1"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8c13ca5e2ddded0ce6a927ea5a9f27cae77eee4c75547b4297252cb20c4d30e6"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b6c8e30adfa52c025f042a87f450a6b9ea29649d828e0fec4858ed5e6caecf63"},
{file = "orjson-3.10.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:338fd4f071b242f26e9ca802f443edc588fa4ab60bfa81f38beaedf42eda226c"},
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:6970ed7a3126cfed873c5d21ece1cd5d6f83ca6c9afb71bbae21a0b034588d96"},
{file = "orjson-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:235dadefb793ad12f7fa11e98a480db1f7c6469ff9e3da5e73c7809c700d746b"},
{file = "orjson-3.10.5-cp310-none-win32.whl", hash = "sha256:be79e2393679eda6a590638abda16d167754393f5d0850dcbca2d0c3735cebe2"},
{file = "orjson-3.10.5-cp310-none-win_amd64.whl", hash = "sha256:c4a65310ccb5c9910c47b078ba78e2787cb3878cdded1702ac3d0da71ddc5228"},
{file = "orjson-3.10.5-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cdf7365063e80899ae3a697def1277c17a7df7ccfc979990a403dfe77bb54d40"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6b68742c469745d0e6ca5724506858f75e2f1e5b59a4315861f9e2b1df77775a"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7d10cc1b594951522e35a3463da19e899abe6ca95f3c84c69e9e901e0bd93d38"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcbe82b35d1ac43b0d84072408330fd3295c2896973112d495e7234f7e3da2e1"},
{file = "orjson-3.10.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c0eb7e0c75e1e486c7563fe231b40fdd658a035ae125c6ba651ca3b07936f5"},
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:53ed1c879b10de56f35daf06dbc4a0d9a5db98f6ee853c2dbd3ee9d13e6f302f"},
{file = "orjson-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:099e81a5975237fda3100f918839af95f42f981447ba8f47adb7b6a3cdb078fa"},
{file = "orjson-3.10.5-cp311-none-win32.whl", hash = "sha256:1146bf85ea37ac421594107195db8bc77104f74bc83e8ee21a2e58596bfb2f04"},
{file = "orjson-3.10.5-cp311-none-win_amd64.whl", hash = "sha256:36a10f43c5f3a55c2f680efe07aa93ef4a342d2960dd2b1b7ea2dd764fe4a37c"},
{file = "orjson-3.10.5-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:68f85ecae7af14a585a563ac741b0547a3f291de81cd1e20903e79f25170458f"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28afa96f496474ce60d3340fe8d9a263aa93ea01201cd2bad844c45cd21f5268"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cd684927af3e11b6e754df80b9ffafd9fb6adcaa9d3e8fdd5891be5a5cad51e"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d21b9983da032505f7050795e98b5d9eee0df903258951566ecc358f6696969"},
{file = "orjson-3.10.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ad1de7fef79736dde8c3554e75361ec351158a906d747bd901a52a5c9c8d24b"},
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d97531cdfe9bdd76d492e69800afd97e5930cb0da6a825646667b2c6c6c0211"},
{file = "orjson-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d69858c32f09c3e1ce44b617b3ebba1aba030e777000ebdf72b0d8e365d0b2b3"},
{file = "orjson-3.10.5-cp312-none-win32.whl", hash = "sha256:64c9cc089f127e5875901ac05e5c25aa13cfa5dbbbd9602bda51e5c611d6e3e2"},
{file = "orjson-3.10.5-cp312-none-win_amd64.whl", hash = "sha256:b2efbd67feff8c1f7728937c0d7f6ca8c25ec81373dc8db4ef394c1d93d13dc5"},
{file = "orjson-3.10.5-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:03b565c3b93f5d6e001db48b747d31ea3819b89abf041ee10ac6988886d18e01"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:584c902ec19ab7928fd5add1783c909094cc53f31ac7acfada817b0847975f26"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a35455cc0b0b3a1eaf67224035f5388591ec72b9b6136d66b49a553ce9eb1e6"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1670fe88b116c2745a3a30b0f099b699a02bb3482c2591514baf5433819e4f4d"},
{file = "orjson-3.10.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:185c394ef45b18b9a7d8e8f333606e2e8194a50c6e3c664215aae8cf42c5385e"},
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ca0b3a94ac8d3886c9581b9f9de3ce858263865fdaa383fbc31c310b9eac07c9"},
{file = "orjson-3.10.5-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dfc91d4720d48e2a709e9c368d5125b4b5899dced34b5400c3837dadc7d6271b"},
{file = "orjson-3.10.5-cp38-none-win32.whl", hash = "sha256:c05f16701ab2a4ca146d0bca950af254cb7c02f3c01fca8efbbad82d23b3d9d4"},
{file = "orjson-3.10.5-cp38-none-win_amd64.whl", hash = "sha256:8a11d459338f96a9aa7f232ba95679fc0c7cedbd1b990d736467894210205c09"},
{file = "orjson-3.10.5-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:85c89131d7b3218db1b24c4abecea92fd6c7f9fab87441cfc342d3acc725d807"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66215277a230c456f9038d5e2d84778141643207f85336ef8d2a9da26bd7ca"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:51bbcdea96cdefa4a9b4461e690c75ad4e33796530d182bdd5c38980202c134a"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbead71dbe65f959b7bd8cf91e0e11d5338033eba34c114f69078d59827ee139"},
{file = "orjson-3.10.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df58d206e78c40da118a8c14fc189207fffdcb1f21b3b4c9c0c18e839b5a214"},
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c4057c3b511bb8aef605616bd3f1f002a697c7e4da6adf095ca5b84c0fd43595"},
{file = "orjson-3.10.5-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b39e006b00c57125ab974362e740c14a0c6a66ff695bff44615dcf4a70ce2b86"},
{file = "orjson-3.10.5-cp39-none-win32.whl", hash = "sha256:eded5138cc565a9d618e111c6d5c2547bbdd951114eb822f7f6309e04db0fb47"},
{file = "orjson-3.10.5-cp39-none-win_amd64.whl", hash = "sha256:cc28e90a7cae7fcba2493953cff61da5a52950e78dc2dacfe931a317ee3d8de7"},
{file = "orjson-3.10.5.tar.gz", hash = "sha256:7a5baef8a4284405d96c90c7c62b755e9ef1ada84c2406c24a9ebec86b89f46d"},
]
[[package]]
@@ -2346,47 +2350,54 @@ files = [
[[package]]
name = "pydantic"
version = "1.10.15"
version = "1.10.17"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
{file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"},
{file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"},
{file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"},
{file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"},
{file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"},
{file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"},
{file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"},
{file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"},
{file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"},
{file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"},
{file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"},
{file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"},
{file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"},
{file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"},
{file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"},
{file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"},
{file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"},
{file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"},
{file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"},
{file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"},
{file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"},
{file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"},
{file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"},
{file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
{file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
{file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
{file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
{file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
{file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
{file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
{file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
{file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
{file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
{file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
{file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
{file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
{file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
{file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
{file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
{file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
{file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
{file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
{file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
{file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
{file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
{file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
{file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
{file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
{file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
{file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
]
[package.dependencies]
@@ -2760,13 +2771,13 @@ typing-extensions = "*"
[[package]]
name = "requests"
version = "2.31.0"
version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.7"
python-versions = ">=3.8"
files = [
{file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
{file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
]
[package.dependencies]
@@ -2799,28 +2810,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.8"
version = "0.4.10"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
{file = "ruff-0.4.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:5c2c4d0859305ac5a16310eec40e4e9a9dec5dcdfbe92697acd99624e8638dac"},
{file = "ruff-0.4.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a79489607d1495685cdd911a323a35871abfb7a95d4f98fc6f85e799227ac46e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1dd1681dfa90a41b8376a61af05cc4dc5ff32c8f14f5fe20dba9ff5deb80cd6"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c75c53bb79d71310dc79fb69eb4902fba804a81f374bc86a9b117a8d077a1784"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18238c80ee3d9100d3535d8eb15a59c4a0753b45cc55f8bf38f38d6a597b9739"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d8f71885bce242da344989cae08e263de29752f094233f932d4f5cfb4ef36a81"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:330421543bd3222cdfec481e8ff3460e8702ed1e58b494cf9d9e4bf90db52b9d"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9e9b6fb3a37b772628415b00c4fc892f97954275394ed611056a4b8a2631365e"},
{file = "ruff-0.4.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f54c481b39a762d48f64d97351048e842861c6662d63ec599f67d515cb417f6"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:67fe086b433b965c22de0b4259ddfe6fa541c95bf418499bedb9ad5fb8d1c631"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:acfaaab59543382085f9eb51f8e87bac26bf96b164839955f244d07125a982ef"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3cea07079962b2941244191569cf3a05541477286f5cafea638cd3aa94b56815"},
{file = "ruff-0.4.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:338a64ef0748f8c3a80d7f05785930f7965d71ca260904a9321d13be24b79695"},
{file = "ruff-0.4.10-py3-none-win32.whl", hash = "sha256:ffe3cd2f89cb54561c62e5fa20e8f182c0a444934bf430515a4b422f1ab7b7ca"},
{file = "ruff-0.4.10-py3-none-win_amd64.whl", hash = "sha256:67f67cef43c55ffc8cc59e8e0b97e9e60b4837c8f21e8ab5ffd5d66e196e25f7"},
{file = "ruff-0.4.10-py3-none-win_arm64.whl", hash = "sha256:dd1fcee327c20addac7916ca4e2653fbbf2e8388d8a6477ce5b4e986b68ae6c0"},
{file = "ruff-0.4.10.tar.gz", hash = "sha256:3aa4f2bc388a30d346c56524f7cacca85945ba124945fe489952aadb6b5cd804"},
]
[[package]]
@@ -3571,5 +3582,5 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "db51ad1e631b569e106927683a13124252bd80974def1f2edbe23ac87d89c461"
python-versions = ">=3.10,<4.0"
content-hash = "df9afeda50e05cb62b322a047028a9b0851db197c4f379903c70adab3a98777a"

View File

@@ -1,13 +1,13 @@
[tool.poetry]
name = "machine-learning"
version = "1.106.2"
version = "1.108.0"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
packages = [{include = "app"}]
[tool.poetry.dependencies]
python = ">=3.10,<3.12"
python = ">=3.10,<4.0"
insightface = ">=0.7.3,<1.0"
opencv-python-headless = ">=4.7.0.72,<5.0"
pillow = ">=9.5.0,<11.0"
@@ -97,4 +97,4 @@ line-length = 120
target-version = ['py311']
[tool.pytest.ini_options]
markers = ["providers"]
markers = ["providers", "ov_device_ids"]

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.22.1"
"flutter": "3.22.2"
}

View File

@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"
xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true"
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true">
android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" android:largeHeap="true">
<meta-data
android:name="io.flutter.embedding.android.EnableImpeller"

View File

@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
"android.injected.version.code" => 142,
"android.injected.version.name" => "1.106.2",
"android.injected.version.code" => 148,
"android.injected.version.name" => "1.108.0",
}
)
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

@@ -1,23 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="fastlane.lanes">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000218">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="58.982292">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="31.253303">
</testcase>
</testsuite>
</testsuites>

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
"advanced_settings_tile_subtitle": "إعدادات المستخدم المتقدمة",
@@ -203,6 +205,13 @@
"favorites_page_title": "المفضلة",
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
"haptic_feedback_title": "ردود فعل لمسية",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.{فشل} الأصول موجودة بالفعل في الألبوم.",
"home_page_add_to_album_err_local": "لا يمكن إضافة الأصول المحلية إلى الألبومات حتى الآن ، سوف يتخطى",
"home_page_add_to_album_success": "تمت إضافة {تمت إضافة} الأصول إلى الألبوم {الألبوم}.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "التحقق مرة أخرى غدا لمزيد من الذكريات",
"memories_start_over": "ابدأ من جديد",
"memories_swipe_to_close": "اسحب لأعلى للإغلاق",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "ط ط ط",
"motion_photos_page_title": "الصور المتحركة",
"multiselect_grid_edit_date_time_err_read_only": "لا يمكن تعديل تاريخ الأصول (المواد) للقراءة فقط، سوف يتخطى",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Úroveň protokolování: {}",
"advanced_settings_prefer_remote_subtitle": "U některých zařízení je načítání miniatur z prostředků v zařízení velmi pomalé. Aktivujte toto nastavení, aby se místo toho načítaly vzdálené obrázky.",
"advanced_settings_prefer_remote_title": "Preferovat vzdálené obrázky",
"advanced_settings_proxy_headers_subtitle": "Definice hlaviček proxy serveru, které by měl Immich odesílat s každým síťovým požadavkem",
"advanced_settings_proxy_headers_title": "Proxy hlavičky",
"advanced_settings_self_signed_ssl_subtitle": "Vynechá ověření SSL certifikátu serveru. Vyžadováno pro self-signed certifikáty.",
"advanced_settings_self_signed_ssl_title": "Povolit self-signed SSL certifikáty",
"advanced_settings_tile_subtitle": "Pokročilé uživatelské nastavení",
@@ -203,6 +205,13 @@
"favorites_page_title": "Oblíbené",
"haptic_feedback_switch": "Povolit dotykovou zpětnou vazbu",
"haptic_feedback_title": "Dotyková zpětná vazba",
"header_settings_add_header_tip": "Přidat hlavičku",
"header_settings_field_validator_msg": "Hodnota nemůže být prázdná",
"header_settings_header_name_input": "Název hlavičky",
"header_settings_header_value_input": "Hodnota hlavičky",
"header_settings_page_title": "Proxy hlavičky",
"headers_settings_tile_subtitle": "Definice hlaviček proxy serveru, které má aplikace odesílat s každým síťovým požadavkem",
"headers_settings_tile_title": "Vlastní proxy hlavičky",
"home_page_add_to_album_conflicts": "Přidáno {added} položek do alba {album}. {failed} položek je již v albu.",
"home_page_add_to_album_err_local": "Zatím není možné přidat lokální média do alb, přeskakuji",
"home_page_add_to_album_success": "Přidáno {added} položek do alba {album}.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Zítra se podívejte na další vzpomínky",
"memories_start_over": "Začít znovu",
"memories_swipe_to_close": "Přejetím nahoru zavřete",
"memories_year_ago": "Před rokem",
"memories_years_ago": "Před {} roky",
"monthly_title_text_date_format": "LLLL y",
"motion_photos_page_title": "Pohyblivé fotky",
"multiselect_grid_edit_date_time_err_read_only": "Nelze upravit datum položek pouze pro čtení, přeskakuji",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Logniveau: {}",
"advanced_settings_prefer_remote_subtitle": "Nogle enheder tager meget lang tid om at indlæse miniaturebilleder af elementer på enheden. Aktiver denne indstilling for i stedetat indlæse elementer fra serveren.",
"advanced_settings_prefer_remote_title": "Foretræk elementer på serveren",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Spring verificering af SSL-certifikat over for serverens endelokation. Kræves for selvsignerede certifikater.",
"advanced_settings_self_signed_ssl_title": "Tillad selvsignerede certifikater",
"advanced_settings_tile_subtitle": "Avancerede brugerindstillinger",
@@ -203,6 +205,13 @@
"favorites_page_title": "Favoritter",
"haptic_feedback_switch": "Slå haptisk feedback til",
"haptic_feedback_title": "Haptisk feedback",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "Tilføjede {added} elementer til album {album}. {failed} elementer er allerede i albummet.",
"home_page_add_to_album_err_local": "Kan endnu ikke tilføje lokale elementer til album. Springer over..",
"home_page_add_to_album_success": "Tilføjede {added} elementer til album {album}.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Kom tilbage i morgen for at se nye minder",
"memories_start_over": "Start forfra",
"memories_swipe_to_close": "Stryg op for at lukke",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bevægelsesbilleder",
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Log-Level: {}",
"advanced_settings_prefer_remote_subtitle": "Manche Endgeräte laden Vorschaubilder von lokalen Bilder sehr langsam. Durch diese Einstellung werden diese stattdessen direkt vom Server geladen.",
"advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen",
"advanced_settings_proxy_headers_subtitle": "Definiere Proxy-Header, die Immich bei jeder Netzwerkanfrage mitschicken soll",
"advanced_settings_proxy_headers_title": "Proxy-Headers",
"advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.",
"advanced_settings_self_signed_ssl_title": "Selbstsignierte SSL-Zertifikate erlauben",
"advanced_settings_tile_subtitle": "Erweiterte Benutzereinstellungen",
@@ -52,14 +54,14 @@
"asset_list_settings_title": "Fotogitter",
"asset_viewer_settings_title": "Fotoanzeige",
"backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})",
"backup_album_selection_page_albums_tap": "Einmalig tippen um das Album zu verwenden, doppelt tippen um es zu entfernen.",
"backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern.",
"backup_album_selection_page_assets_scatter": "Elemente (Fotos / Videos) können sich über mehrere Alben verteilen. Daher können diese vor der Sicherung eingeschlossen oder ausgeschlossen werden.",
"backup_album_selection_page_select_albums": "Alben auswählen",
"backup_album_selection_page_selection_info": "Information",
"backup_album_selection_page_total_assets": "Elemente",
"backup_all": "Alle",
"backup_background_service_backup_failed_message": "Fehler beim Sichern von Elementen. Probiere erneut...",
"backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server herstellen. Neuer Versuch...",
"backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch...",
"backup_background_service_current_upload_notification": "Lädt {} hoch",
"backup_background_service_default_notification": "Suche nach neuen Elementen…",
"backup_background_service_error_title": "Fehler bei der Sicherung",
@@ -103,7 +105,7 @@
"backup_controller_page_status_on": "Sicherung im Vordergrund ist aktiv",
"backup_controller_page_storage_format": "{} von {} genutzt",
"backup_controller_page_to_backup": "Zu sichernde Alben",
"backup_controller_page_total": "Gesamt",
"backup_controller_page_total": "Gesamtübersicht",
"backup_controller_page_total_sub": "Alle Fotos und Videos",
"backup_controller_page_turn_off": "Sicherung im Vordergrund ausschalten",
"backup_controller_page_turn_on": "Sicherung im Vordergrund einschalten",
@@ -203,6 +205,13 @@
"favorites_page_title": "Favoriten",
"haptic_feedback_switch": "Haptisches Feedback aktivieren",
"haptic_feedback_title": "Haptisches Feedback",
"header_settings_add_header_tip": "Header hinzufügen",
"header_settings_field_validator_msg": "Der Wert darf nicht leer sein",
"header_settings_header_name_input": "Header-Name",
"header_settings_header_value_input": "Header-Wert",
"header_settings_page_title": "Proxy-Headers",
"headers_settings_tile_subtitle": "Definiere Proxy-Header, die die Anwendung bei jeder Netzwerkanfrage mitschicken soll",
"headers_settings_tile_title": "Benutzerdefinierte Proxy-Header",
"home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefügt. {failed} Elemente sind bereits vorhanden.",
"home_page_add_to_album_err_local": "Es können lokale Elemente noch nicht zu Alben hinzugefügt werden, überspringen...",
"home_page_add_to_album_success": "{added} Elemente zu {album} hinzugefügt.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Schau morgen wieder vorbei für weitere Erinnerungen!",
"memories_start_over": "Erneut beginnen",
"memories_swipe_to_close": "Nach oben Wischen zum schließen",
"memories_year_ago": "ein Jahr her",
"memories_years_ago": "{} Jahre her",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Live-Fotos",
"multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...",
@@ -502,7 +513,7 @@
"trash_page_empty_trash_dialog_content": "Elemente im Papierkorb löschen? Diese Elemente werden dauerhaft aus Immich entfernt",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Elemente im Papierkorb werden nach {} Tagen endgültig gelöscht.",
"trash_page_no_assets": "Keine Elemente im Papierkorb",
"trash_page_no_assets": "Es gibt keine Daten im Papierkorb",
"trash_page_restore": "Wiederherstellen",
"trash_page_restore_all": "Alle wiederherstellen",
"trash_page_select_assets_btn": "Elemente auswählen",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Επίπεδο καταγραφής: {}",
"advanced_settings_prefer_remote_subtitle": "Μερικές συσκευές αργούν πολύ να φορτώσουν μικρογραφίες από αρχεία στη συσκευή. Ενεργοποιήστε αυτήν τη ρύθμιση για να φορτώνονται αντί αυτού απομακρυσμένες εικόνες.",
"advanced_settings_prefer_remote_title": "Προτίμηση απομακρυσμένων εικόνων.",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Παρακάμπτει τον έλεγχο πιστοποιητικού SSL του διακομιστή. Απαραίτητο για αυτο-υπογεγραμμένα πιστοποιητικά.",
"advanced_settings_self_signed_ssl_title": "Να επιτρέπονται αυτο-υπογεγραμμένα πιστοποιητικά SSL",
"advanced_settings_tile_subtitle": "Ρυθμίσεις προχωρημένου χρήστη",
@@ -203,6 +205,13 @@
"favorites_page_title": "Αγαπημένα",
"haptic_feedback_switch": "Enable haptic feedback",
"haptic_feedback_title": "Haptic Feedback",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "Προστέθηκαν {added} στοιχεία στο άλμπουμ {album}. {failed} στοιχεία υπάρχουν ήδη στο άλμπουμ.",
"home_page_add_to_album_err_local": "Δεν είναι ακόμη δυνατή η προσθήκη τοπικών στοιχείων σε άλμπουμ, παράβλεψη",
"home_page_add_to_album_success": "Προστέθηκαν {added} στοιχεία στο άλμπουμ {album}.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
"advanced_settings_tile_subtitle": "Advanced user's settings",
@@ -203,6 +205,13 @@
"favorites_page_title": "Favorites",
"haptic_feedback_switch": "Enable haptic feedback",
"haptic_feedback_title": "Haptic Feedback",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "Added {added} assets to album {album}. {failed} assets are already in the album.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_success": "Added {added} assets to album {album}.",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Nivel de registro: {}",
"advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de los elementos encontrados en el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
"advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
"advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados",
"advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario",
@@ -203,6 +205,13 @@
"favorites_page_title": "Favoritos",
"haptic_feedback_switch": "Activar respuesta háptica",
"haptic_feedback_title": "Respuesta Háptica",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.{failed} elementos ya existen en el álbum.",
"home_page_add_to_album_err_local": "Aún no se pueden agregar elementos locales a álbumes, omitiendo",
"home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Vuelve mañana para más recuerdos",
"memories_start_over": "Empezar de nuevo",
"memories_swipe_to_close": "Desliza para cerrar",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Foto en Movimiento",
"multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo",

View File

@@ -9,6 +9,8 @@
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Algunos dispositivos tardan mucho en cargar las miniaturas de recursos encontrados el dispositivo. Activa esta opción para cargar imágenes remotas en su lugar.",
"advanced_settings_prefer_remote_title": "Preferir imágenes remotas",
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
"advanced_settings_proxy_headers_title": "Proxy Headers",
"advanced_settings_self_signed_ssl_subtitle": "Omitir verificación del certificado SSL del servidor. Requerido para certificados autofirmados",
"advanced_settings_self_signed_ssl_title": "Permitir certificados autofirmados",
"advanced_settings_tile_subtitle": "Configuraciones avanzadas del usuario",
@@ -203,6 +205,13 @@
"favorites_page_title": "Favoritos",
"haptic_feedback_switch": "Enable haptic feedback",
"haptic_feedback_title": "Haptic Feedback",
"header_settings_add_header_tip": "Add Header",
"header_settings_field_validator_msg": "Value cannot be empty",
"header_settings_header_name_input": "Header name",
"header_settings_header_value_input": "Header value",
"header_settings_page_title": "Proxy Headers",
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
"headers_settings_tile_title": "Custom proxy headers",
"home_page_add_to_album_conflicts": "{added} elementos agregados al álbum {album}.\n{failed} elementos ya existen en el álbum.",
"home_page_add_to_album_err_local": "Aún no se pueden agregar recursos locales a álbumes, omitiendo",
"home_page_add_to_album_success": "{added} elementos agregados al álbum {album}. ",
@@ -295,6 +304,8 @@
"memories_check_back_tomorrow": "Check back tomorrow for more memories",
"memories_start_over": "Start Over",
"memories_swipe_to_close": "Swipe up to close",
"memories_year_ago": "A year ago",
"memories_years_ago": "{} years ago",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Foto en Movimiento",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",

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