Compare commits

...

98 Commits

Author SHA1 Message Date
Alex The Bot c77702279c Version v1.109.0 2024-07-18 16:03:42 +00:00
Alex ef0e1a81b9 feat(web): license UI (#11182) 2024-07-18 10:56:27 -05:00
Mert 88f62087fd chore(ml): set higher worker timeout for openvino (#11174) 2024-07-18 10:50:57 -04:00
pokjay 4f89195702 feat(server): country geocoding for remote locations (#10950)
Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-07-18 13:27:07 +02:00
renovate[bot] ee22bbc85c chore(deps): update base-image to v20240717 (major) (#11172)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-17 20:41:18 +00:00
Jason Rasmussen 66fae76af2 fix(server): delete large album (#11042)
fix: large album asset operations
2024-07-17 07:43:35 -04:00
renovate[bot] f0d1dbccf4 chore(deps): update base-image to v20240716 (major) (#11144)
chore(deps): update base-image to v20240716

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

* search_by_filename

* filename example

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

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

This reverts commit 1b67ea2d

* fix: downgrade exiftool-vendored to 26.0.0

* chore: change motionphoto filenames to be kebab-case

* test: add pixel 6 pro motionphoto e2e test case

* test: add pixel 8a motion photo

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

* Update smart-search.md

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

---------

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

* add tests

* use string instead of date

* update sql

* update tests

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

* Update external-library.md

* Update external-library.md

* Update libraries.md

* Update docs/docs/features/libraries.md

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

* Update external-library.md

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2024-07-14 17:29:47 -04:00
Raj Dave d23aa5e8e2 fix(docs): minor grammar fix in external-library.md (#11073) 2024-07-14 02:09:28 +00:00
renovate[bot] 18b466ee52 chore(deps): update base-image to v20240713 (major) (#11066) 2024-07-13 20:58:21 -05:00
Alex e852971a13 fix(mobile): fix database out of size (#11065) 2024-07-13 20:55:35 -05:00
renovate[bot] fbe29bf4cd chore(deps): update dependency rimraf to v6 (#11079)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-13 21:45:45 -04:00
Michel Heusschen 5748f50c1f refactor(web): remove buffer package (#11058) 2024-07-12 16:40:57 +00:00
CanbiZ 1b3a7feb67 chore(docs): update German Readme (README_de_DE.md) (#11035)
Update README_de_DE.md

Update German Readme to newest version & improve some translations
2024-07-12 07:51:06 -04:00
Jason Rasmussen d68bd876c1 chore: clean up change date form spacing (#11037) 2024-07-11 12:10:49 -04:00
Florian Karuhtz c50ac55892 chore(web): use "immich-form-label" class for combobox label (#10254)
use "immich-form-label" class for combobox label
2024-07-11 11:23:58 -04:00
Adiel Tan b2dd4e1c2b chore(docs): correct susceptible spelling (#11029)
Edit Spelling

https://dictionary.cambridge.org/dictionary/english/susceptible
2024-07-11 17:22:28 +02:00
CanbiZ ff2ba240c9 feat(web): Rename the Menu Entry "admin.map_settings" to "admin.map_gps_settings" (#11033)
* Rename the Menu Entry "admin.map_settings" to "admin.map_gps_settings"

Explanation:
The main menu item is called Map & GPS-Settings. The sub-item below it is also called. 
It would be correct:

Main item:
Map & GPS-Settings
Sub-item 1:
Map settings

* Update en.json

* chore: formatting

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-07-11 15:20:57 +00:00
Weblate (bot) 96084355f0 chore(web): update translations (#11007)
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/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/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/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/ro/
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: CanbiZ <mickey.leskowitz@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: HonKami <bjfl6jvkav@randomu.aleeas.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Linerly <linerly@proton.me>
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: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Ponas <le.slab124@aleeas.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: Tyoda <tyoda@pm.me>
Co-authored-by: Ulices <hasecilu@tuta.io>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: Victor Sueiro <kiwicaja@gmail.com>
Co-authored-by: Vincenzo Nunziata <vinciosdev@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: guillezcurra <guillezcurra@gmail.com>
Co-authored-by: nazo6 <git@nazo6.dev>
Co-authored-by: opl- <jakub.trzy@op.pl>
Co-authored-by: vytautas <immichtranslation.a03gn@simplelogin.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Àlex Bravo <alexbravobosch@gmail.com>
Co-authored-by: Сергей Шагов <mrfenixvd@gmail.com>
2024-07-11 11:15:56 -04:00
Jason Rasmussen 25a380d023 feat(server): userinfo signing (#10756)
* feat(server): userinfo signing

* chore: e2e tests
2024-07-11 07:55:00 -04:00
CanbiZ 3cb42de931 feat(web): change mdiClose to mdiInfinity for User Management - quota (#11015)
Add Infinity Icon for Usermanagement (quota)

Add the "mdiInfinity" Icon instead of "mdiClose" for quota (0 for infinity)
2024-07-10 20:20:52 +00:00
Michel Heusschen 8dd1d95913 fix(web): prevent text wrapping for some languages (#11009) 2024-07-10 15:19:06 -05:00
Jason Rasmussen 0ee2390c7f docs: fix images (#11012) 2024-07-10 15:18:55 -05:00
MJ 52db9558b3 fix(doc): Fix broken link in Cursed Knowledge doc (#11008)
Fix broken link
2024-07-10 15:16:06 -04:00
Alex 0fbfbc86d2 chore(mobile): post release task (#11006) 2024-07-10 11:40:19 -05:00
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
336 changed files with 14843 additions and 8630 deletions
+1
View File
@@ -29,3 +29,4 @@ web/node_modules/
web/coverage/ web/coverage/
web/.svelte-kit web/.svelte-kit
web/build/ web/build/
web/.env
@@ -1,11 +1,13 @@
title: "[Feature] <feature-name-goes-here>" title: "[Feature] feature-name-goes-here"
labels: ["feature"] labels: ["feature"]
body: body:
- type: markdown - type: markdown
attributes: attributes:
value: | value: |
Please use this form to request new feature for Immich Please use this form to request new feature for Immich.
Stick to only a single feature per request. If you list multiple different features at once,
your request will be closed.
- type: checkboxes - type: checkboxes
attributes: attributes:
+3 -3
View File
@@ -56,10 +56,10 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU - 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 - 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 - name: Login to GitHub Container Registry
uses: docker/login-action@v3 uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }} type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.2.0 uses: docker/build-push-action@v6.3.0
with: with:
file: cli/Dockerfile file: cli/Dockerfile
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
+3 -3
View File
@@ -63,10 +63,10 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Set up QEMU - 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 - 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 - name: Login to Docker Hub
# Only push to Docker Hub when making a release # Only push to Docker Hub when making a release
@@ -115,7 +115,7 @@ jobs:
fi fi
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6.2.0 uses: docker/build-push-action@v6.3.0
with: with:
context: ${{ matrix.context }} context: ${{ matrix.context }}
file: ${{ matrix.file }} file: ${{ matrix.file }}
+1 -1
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 For answers to common questions about this code of conduct, see the
FAQ at https://www.contributor-covenant.org/faq. Translations are 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.
+1 -1
View File
@@ -1 +1 @@
20.15 20.15.1
+1 -1
View File
@@ -1,4 +1,4 @@
FROM node:20.15.0-alpine3.20@sha256:df01469346db2bf1cfc1f7261aeab86b2960efa840fe2bd46d83ff339f463665 as core FROM node:20.15.1-alpine3.20@sha256:34b7aa411056c85dbf71d240d26516949b3f72b318d796c26b57caaa1df5639a as core
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
+149 -77
View File
@@ -1,15 +1,16 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.7", "version": "2.2.9",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.7", "version": "2.2.9",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"bin": { "bin": {
@@ -21,7 +22,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2", "@vitest/coverage-v8": "^1.2.2",
@@ -34,11 +35,12 @@
"eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.12", "vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2", "vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1" "yaml": "^2.3.1"
}, },
"engines": { "engines": {
@@ -47,14 +49,14 @@
}, },
"../open-api/typescript-sdk": { "../open-api/typescript-sdk": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.107.2", "version": "1.109.0",
"dev": true, "dev": true,
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -1122,10 +1124,11 @@
"dev": true "dev": true
}, },
"node_modules/@types/cli-progress": { "node_modules/@types/cli-progress": {
"version": "3.11.5", "version": "3.11.6",
"resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.5.tgz", "resolved": "https://registry.npmjs.org/@types/cli-progress/-/cli-progress-3.11.6.tgz",
"integrity": "sha512-D4PbNRbviKyppS5ivBGyFO29POlySLmA2HyUFE4p5QGazAMM3CwkKWcvTl8gvElSuxRh6FPKL8XmidX873ou4g==", "integrity": "sha512-cE3+jb9WRlu+uOSAugewNpITJDt1VF8dHOopPO4IABFc3SXYL5WE/+PTz/FCdZRRfIujiWW3n3aMbv1eIGVRWA==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/node": "*" "@types/node": "*"
} }
@@ -1161,9 +1164,9 @@
} }
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.9", "version": "20.14.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1177,17 +1180,17 @@
"dev": true "dev": true
}, },
"node_modules/@typescript-eslint/eslint-plugin": { "node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz",
"integrity": "sha512-aAJd6bIf2vvQRjUG3ZkNXkmBpN+J7Wd0mfQiiVCJMu9Z5GcZZdcc0j8XwN/BM97Fl7e3SkTXODSk4VehUv7CGw==", "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/regexpp": "^4.10.0", "@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.14.1", "@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/type-utils": "7.14.1", "@typescript-eslint/type-utils": "7.16.0",
"@typescript-eslint/utils": "7.14.1", "@typescript-eslint/utils": "7.16.0",
"@typescript-eslint/visitor-keys": "7.14.1", "@typescript-eslint/visitor-keys": "7.16.0",
"graphemer": "^1.4.0", "graphemer": "^1.4.0",
"ignore": "^5.3.1", "ignore": "^5.3.1",
"natural-compare": "^1.4.0", "natural-compare": "^1.4.0",
@@ -1211,16 +1214,16 @@
} }
}, },
"node_modules/@typescript-eslint/parser": { "node_modules/@typescript-eslint/parser": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz",
"integrity": "sha512-8lKUOebNLcR0D7RvlcloOacTOWzOqemWEWkKSVpMZVF/XVcwjPR+3MD08QzbW9TCGJ+DwIc6zUSGZ9vd8cO1IA==", "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/scope-manager": "7.14.1", "@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.14.1", "@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.14.1", "@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/visitor-keys": "7.14.1", "@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4" "debug": "^4.3.4"
}, },
"engines": { "engines": {
@@ -1240,14 +1243,14 @@
} }
}, },
"node_modules/@typescript-eslint/scope-manager": { "node_modules/@typescript-eslint/scope-manager": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz",
"integrity": "sha512-gPrFSsoYcsffYXTOZ+hT7fyJr95rdVe4kGVX1ps/dJ+DfmlnjFN/GcMxXcVkeHDKqsq6uAcVaQaIi3cFffmAbA==", "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.14.1", "@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.14.1" "@typescript-eslint/visitor-keys": "7.16.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1258,14 +1261,14 @@
} }
}, },
"node_modules/@typescript-eslint/type-utils": { "node_modules/@typescript-eslint/type-utils": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz",
"integrity": "sha512-/MzmgNd3nnbDbOi3LfasXWWe292+iuo+umJ0bCCMCPc1jLO/z2BQmWUUUXvXLbrQey/JgzdF/OV+I5bzEGwJkQ==", "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/typescript-estree": "7.14.1", "@typescript-eslint/typescript-estree": "7.16.0",
"@typescript-eslint/utils": "7.14.1", "@typescript-eslint/utils": "7.16.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"ts-api-utils": "^1.3.0" "ts-api-utils": "^1.3.0"
}, },
@@ -1286,9 +1289,9 @@
} }
}, },
"node_modules/@typescript-eslint/types": { "node_modules/@typescript-eslint/types": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz",
"integrity": "sha512-mL7zNEOQybo5R3AavY+Am7KLv8BorIv7HCYS5rKoNZKQD9tsfGUpO4KdAn3sSUvTiS4PQkr2+K0KJbxj8H9NDg==", "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -1300,14 +1303,14 @@
} }
}, },
"node_modules/@typescript-eslint/typescript-estree": { "node_modules/@typescript-eslint/typescript-estree": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz",
"integrity": "sha512-k5d0VuxViE2ulIO6FbxxSZaxqDVUyMbXcidC8rHvii0I56XZPv8cq+EhMns+d/EVIL41sMXqRbK3D10Oza1bbA==", "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.14.1", "@typescript-eslint/types": "7.16.0",
"@typescript-eslint/visitor-keys": "7.14.1", "@typescript-eslint/visitor-keys": "7.16.0",
"debug": "^4.3.4", "debug": "^4.3.4",
"globby": "^11.1.0", "globby": "^11.1.0",
"is-glob": "^4.0.3", "is-glob": "^4.0.3",
@@ -1329,16 +1332,16 @@
} }
}, },
"node_modules/@typescript-eslint/utils": { "node_modules/@typescript-eslint/utils": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz",
"integrity": "sha512-CMmVVELns3nak3cpJhZosDkm63n+DwBlDX8g0k4QUa9BMnF+lH2lr3d130M1Zt1xxmB3LLk3NV7KQCq86ZBBhQ==", "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.4.0", "@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.14.1", "@typescript-eslint/scope-manager": "7.16.0",
"@typescript-eslint/types": "7.14.1", "@typescript-eslint/types": "7.16.0",
"@typescript-eslint/typescript-estree": "7.14.1" "@typescript-eslint/typescript-estree": "7.16.0"
}, },
"engines": { "engines": {
"node": "^18.18.0 || >=20.0.0" "node": "^18.18.0 || >=20.0.0"
@@ -1352,13 +1355,13 @@
} }
}, },
"node_modules/@typescript-eslint/visitor-keys": { "node_modules/@typescript-eslint/visitor-keys": {
"version": "7.14.1", "version": "7.16.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.14.1.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz",
"integrity": "sha512-Crb+F75U1JAEtBeQGxSKwI60hZmmzaqA3z9sYsVm8X7W5cwLEm5bRe0/uXS6+MR/y8CVpKSR/ontIAIEPFcEkA==", "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@typescript-eslint/types": "7.14.1", "@typescript-eslint/types": "7.16.0",
"eslint-visitor-keys": "^3.4.3" "eslint-visitor-keys": "^3.4.3"
}, },
"engines": { "engines": {
@@ -1853,6 +1856,15 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/cross-fetch": {
"version": "3.1.8",
"resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz",
"integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.12"
}
},
"node_modules/cross-spawn": { "node_modules/cross-spawn": {
"version": "7.0.3", "version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
@@ -3146,6 +3158,26 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.14", "version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
@@ -3369,10 +3401,11 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"dev": true "dev": true,
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -3406,9 +3439,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"dev": true, "dev": true,
"funding": [ "funding": [
{ {
@@ -3424,9 +3457,10 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.1",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
@@ -3471,21 +3505,22 @@
} }
}, },
"node_modules/prettier-plugin-organize-imports": { "node_modules/prettier-plugin-organize-imports": {
"version": "3.2.4", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.0.0.tgz",
"integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", "integrity": "sha512-vnKSdgv9aOlqKeEFGhf9SCBsTyzDSyScy1k7E0R1Uo4L0cTcOV7c1XQaT7jfXIOc/p08WLBfN2QUQA9zDSZMxA==",
"dev": true, "dev": true,
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@volar/vue-language-plugin-pug": "^1.0.4", "@vue/language-plugin-pug": "^2.0.24",
"@volar/vue-typescript": "^1.0.4",
"prettier": ">=2.0", "prettier": ">=2.0",
"typescript": ">=2.9" "typescript": ">=2.9",
"vue-tsc": "^2.0.24"
}, },
"peerDependenciesMeta": { "peerDependenciesMeta": {
"@volar/vue-language-plugin-pug": { "@vue/language-plugin-pug": {
"optional": true "optional": true
}, },
"@volar/vue-typescript": { "vue-tsc": {
"optional": true "optional": true
} }
} }
@@ -4168,6 +4203,12 @@
"node": ">=8.0" "node": ">=8.0"
} }
}, },
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/ts-api-utils": { "node_modules/ts-api-utils": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz",
@@ -4240,9 +4281,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.2", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
@@ -4315,14 +4356,14 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "5.3.2", "version": "5.3.3",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.3.2.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz",
"integrity": "sha512-6lA7OBHBlXUxiJxbO5aAY2fsHHzDr1q7DvXYnyZycRs2Dz+dXBWuhpWHvmljTRTpQC2uvGmUFFkSHF2vGo90MA==", "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.38", "postcss": "^8.4.39",
"rollup": "^4.13.0" "rollup": "^4.13.0"
}, },
"bin": { "bin": {
@@ -4476,6 +4517,37 @@
} }
} }
}, },
"node_modules/vitest-fetch-mock": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz",
"integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==",
"dev": true,
"dependencies": {
"cross-fetch": "^3.0.6"
},
"engines": {
"node": ">=14.14.0"
},
"peerDependencies": {
"vitest": ">=0.16.0"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+6 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/cli", "name": "@immich/cli",
"version": "2.2.7", "version": "2.2.9",
"description": "Command Line Interface (CLI) for Immich", "description": "Command Line Interface (CLI) for Immich",
"type": "module", "type": "module",
"exports": "./dist/index.js", "exports": "./dist/index.js",
@@ -18,7 +18,7 @@
"@types/cli-progress": "^3.11.0", "@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12", "@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1", "@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0", "@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2", "@vitest/coverage-v8": "^1.2.2",
@@ -31,11 +31,12 @@
"eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unicorn": "^54.0.0",
"mock-fs": "^5.2.0", "mock-fs": "^5.2.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
"vite": "^5.0.12", "vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2", "vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2", "vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"yaml": "^2.3.1" "yaml": "^2.3.1"
}, },
"scripts": { "scripts": {
@@ -59,9 +60,10 @@
}, },
"dependencies": { "dependencies": {
"fast-glob": "^3.3.2", "fast-glob": "^3.3.2",
"fastq": "^1.17.1",
"lodash-es": "^4.17.21" "lodash-es": "^4.17.21"
}, },
"volta": { "volta": {
"node": "20.15.0" "node": "20.15.1"
} }
} }
+187 -5
View File
@@ -1,10 +1,18 @@
import { platform } from 'node:os'; import * as fs from 'node:fs';
import { UploadOptionsDto, getAlbumName } from 'src/commands/asset'; import * as os from 'node:os';
import { describe, expect, it } from 'vitest'; import * as path from 'node:path';
import { describe, expect, it, vi } from 'vitest';
describe('Unit function tests', () => { 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', () => { it('should return a non-undefined value', () => {
if (platform() === 'win32') { if (os.platform() === 'win32') {
// This is meaningless for Unix systems. // This is meaningless for Unix systems.
expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test'); expect(getAlbumName(String.raw`D:\test\Filename.txt`, {} as UploadOptionsDto)).toBe('test');
} }
@@ -17,3 +25,177 @@ describe('Unit function tests', () => {
); );
}); });
}); });
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: [],
});
});
});
+68 -34
View File
@@ -16,6 +16,7 @@ import { chunk } from 'lodash-es';
import { Stats, createReadStream } from 'node:fs'; import { Stats, createReadStream } from 'node:fs';
import { stat, unlink } from 'node:fs/promises'; import { stat, unlink } from 'node:fs/promises';
import path, { basename } from 'node:path'; import path, { basename } from 'node:path';
import { Queue } from 'src/queue';
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils'; import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
const s = (count: number) => (count === 1 ? '' : 's'); const s = (count: number) => (count === 1 ? '' : 's');
@@ -83,7 +84,7 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
return files; return files;
}; };
const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => { export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
if (skipHash) { if (skipHash) {
console.log('Skipping hash check, assuming all files are new'); console.log('Skipping hash check, assuming all files are new');
return { newFiles: files, duplicates: [] }; return { newFiles: files, duplicates: [] };
@@ -99,32 +100,50 @@ const checkForDuplicates = async (files: string[], { concurrency, skipHash }: Up
const newFiles: string[] = []; const newFiles: string[] = [];
const duplicates: Asset[] = []; const duplicates: Asset[] = [];
try { const queue = new Queue<string[], AssetBulkUploadCheckResults>(
// TODO refactor into a queue async (filepaths: string[]) => {
for (const items of chunk(files, concurrency)) { const dto = await Promise.all(
const dto = await Promise.all(items.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) }))); filepaths.map(async (filepath) => ({ id: filepath, checksum: await sha1(filepath) })),
const { results } = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } }); );
const response = await checkBulkUpload({ assetBulkUploadCheckDto: { assets: dto } });
for (const { id: filepath, assetId, action } of results as AssetBulkUploadCheckResults) { const results = response.results as AssetBulkUploadCheckResults;
for (const { id: filepath, assetId, action } of results) {
if (action === Action.Accept) { if (action === Action.Accept) {
newFiles.push(filepath); newFiles.push(filepath);
} else { } else {
// rejects are always duplicates // rejects are always duplicates
duplicates.push({ id: assetId as string, filepath }); duplicates.push({ id: assetId as string, filepath });
} }
progressBar.increment();
} }
} progressBar.increment(filepaths.length);
} finally { return results;
progressBar.stop(); },
{ 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)}`); 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 }; 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) { if (files.length === 0) {
console.log('All assets were already uploaded, nothing to do.'); console.log('All assets were already uploaded, nothing to do.');
return []; return [];
@@ -158,37 +177,52 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
const newAssets: Asset[] = []; const newAssets: Asset[] = [];
try { const queue = new Queue<string, AssetMediaResponseDto>(
for (const items of chunk(files, concurrency)) { async (filepath: string) => {
await Promise.all( const stats = statsMap.get(filepath);
items.map(async (filepath) => { if (!stats) {
const stats = statsMap.get(filepath) as Stats; throw new Error(`Stats not found for ${filepath}`);
const response = await uploadFile(filepath, stats); }
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) { uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
duplicateCount++;
duplicateSize += stats.size ?? 0;
} else {
successCount++;
successSize += stats.size ?? 0;
}
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) }); return response;
},
{ concurrency, retry: 3 },
);
return response; for (const filepath of files) {
}), await queue.push(filepath);
);
}
} finally {
uploadProgress.stop();
} }
await queue.drained();
uploadProgress.stop();
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`); console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
if (duplicateCount > 0) { if (duplicateCount > 0) {
console.log(`Skipped ${duplicateCount} duplicate asset${s(duplicateCount)} (${byteSize(duplicateSize)})`); 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; return newAssets;
}; };
+131
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;
}
}
}
+30 -30
View File
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.36.0" version = "4.37.0"
constraints = "4.36.0" constraints = "4.37.0"
hashes = [ hashes = [
"h1:00/Y+l17VV4RquGSfwDnYsGYzyf2ZmdQwUgeIzXC7eg=", "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:489GpKItA/VRIUA5S4+F8MsnurGVciRvUFyIV81MJTU=", "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:7cnczyKGj3+gvaJ0r5JIVWLXPbQfkHYejac76MJx+I8=", "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:8rmr1PjJc14Xmor2eEvo5/WBojylt1eYdx6VbSU3Ulo=", "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:HjgphNjtgny5tkcUAQoGgBdcuQ+0IyhL8yLsiBqWAP0=", "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:LH3umxdBnJcAyeVoBLVn+PC0F0CzN6v9UN6lb6CqQPE=", "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:Xx6WUD/zB8fM9SjkFx06Fgx2K7aGJIVvsJS2pwqALEM=", "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:YizL5YN9zQ8YkSR6V/G201YrCVdnkF9EUIK4lpROWiA=", "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:aPcXVGjYcCJdqvWSzc/dEjwj05LnbWZje8IanygVjcI=", "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:eKCvfashdCqfDcFGXE2gq+XxAURD5SzuaQ9Brs3zLos=", "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:gpKcBYkBcfn/uF1A8W7MD/OysMZW7EU4QVYvPEEnxGc=", "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:kCkcxZZnkKAnMz9scUQHb19d9/l9FPOHovAyrvtA618=", "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:t8mXXnICTeKqoD29uvyLFHVWMfMzTUrJuHje8lpI0zU=", "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:zjzavjIdLDGRYsWd3v0HJz6ul12Cewj9RW/cqAQ4DxI=", "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:02665712b3893307596b3caab99cf1f2502d5caca18e22d4b37bb535e628e102", "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:1514b0d3ef62934484ac471113ee68cddec0c21e56b4f710922741fe9b6e6fdf", "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:1fab4dfcecbcea13267b42e5ff05ba0692aa2dcb247b8e633fea0daf49feb156", "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:24d8367295fe1f1b2be37802aecb96edf32f743364663ffe781d1bb92438395d", "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:34e84e7940c99dcf65663cfd25afac22bf5c8a5ff2cd21900c67180d3a072be9", "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:3d71d63204a329acf1d1de8638f2c725243cb94cf444d2d7acde54b3d1ac1696", "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:57831ba88e779a762bcfa224ba9eac8bc22ef9cd70cd541d848b351e0ba6a75c", "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:6407560f2e548afcb4852c91efc664627a9ee565c31a9c81fc9ea1806fca0567", "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:738ddbc664d75f4859aa09444a27809bc398795a8ea8f5be8531040690287712", "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"zh:841ca2b2d78b6f8d33ec3435bc090c5e04a3a7d85c80df11227a7ea00d36f6b1",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f", "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2", "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807", "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3", "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
] ]
} }
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.36.0" version = "4.37.0"
} }
} }
} }
+30 -30
View File
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates. # Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" { provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.36.0" version = "4.37.0"
constraints = "4.36.0" constraints = "4.37.0"
hashes = [ hashes = [
"h1:00/Y+l17VV4RquGSfwDnYsGYzyf2ZmdQwUgeIzXC7eg=", "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:489GpKItA/VRIUA5S4+F8MsnurGVciRvUFyIV81MJTU=", "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:7cnczyKGj3+gvaJ0r5JIVWLXPbQfkHYejac76MJx+I8=", "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:8rmr1PjJc14Xmor2eEvo5/WBojylt1eYdx6VbSU3Ulo=", "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:HjgphNjtgny5tkcUAQoGgBdcuQ+0IyhL8yLsiBqWAP0=", "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:LH3umxdBnJcAyeVoBLVn+PC0F0CzN6v9UN6lb6CqQPE=", "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:Xx6WUD/zB8fM9SjkFx06Fgx2K7aGJIVvsJS2pwqALEM=", "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:YizL5YN9zQ8YkSR6V/G201YrCVdnkF9EUIK4lpROWiA=", "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:aPcXVGjYcCJdqvWSzc/dEjwj05LnbWZje8IanygVjcI=", "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:eKCvfashdCqfDcFGXE2gq+XxAURD5SzuaQ9Brs3zLos=", "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:gpKcBYkBcfn/uF1A8W7MD/OysMZW7EU4QVYvPEEnxGc=", "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:kCkcxZZnkKAnMz9scUQHb19d9/l9FPOHovAyrvtA618=", "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:t8mXXnICTeKqoD29uvyLFHVWMfMzTUrJuHje8lpI0zU=", "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:zjzavjIdLDGRYsWd3v0HJz6ul12Cewj9RW/cqAQ4DxI=", "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:02665712b3893307596b3caab99cf1f2502d5caca18e22d4b37bb535e628e102", "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:1514b0d3ef62934484ac471113ee68cddec0c21e56b4f710922741fe9b6e6fdf", "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:1fab4dfcecbcea13267b42e5ff05ba0692aa2dcb247b8e633fea0daf49feb156", "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:24d8367295fe1f1b2be37802aecb96edf32f743364663ffe781d1bb92438395d", "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:34e84e7940c99dcf65663cfd25afac22bf5c8a5ff2cd21900c67180d3a072be9", "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:3d71d63204a329acf1d1de8638f2c725243cb94cf444d2d7acde54b3d1ac1696", "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:57831ba88e779a762bcfa224ba9eac8bc22ef9cd70cd541d848b351e0ba6a75c", "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:6407560f2e548afcb4852c91efc664627a9ee565c31a9c81fc9ea1806fca0567", "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:738ddbc664d75f4859aa09444a27809bc398795a8ea8f5be8531040690287712", "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"zh:841ca2b2d78b6f8d33ec3435bc090c5e04a3a7d85c80df11227a7ea00d36f6b1",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8b3d3d63354032ab9b2403c50728e9aa4e83c7367eaad2d18794221addeafc0f", "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:9e293443fe3127e488f540229983c1b9688268185f87567bb3d18e794697acd2", "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:b3a22439156e46461213db183e2e89569cd2e8d7cbcfc4b9f90469090e105807", "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:f430feb5d51891e84028459e57039045dea4f1f5fcf671161d8ac2d8f28763f3", "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
] ]
} }
+1 -1
View File
@@ -5,7 +5,7 @@ terraform {
required_providers { required_providers {
cloudflare = { cloudflare = {
source = "cloudflare/cloudflare" source = "cloudflare/cloudflare"
version = "4.36.0" version = "4.37.0"
} }
} }
} }
+1 -1
View File
@@ -73,7 +73,7 @@ services:
container_name: immich_prometheus container_name: immich_prometheus
ports: ports:
- 9090:9090 - 9090:9090
image: prom/prometheus@sha256:075b1ba2c4ebb04bc3a6ab86c06ec8d8099f8fda1c96ef6d104d9bb1def1d8bc image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
volumes: volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml - ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus - prometheus-data:/prometheus
+1 -1
View File
@@ -1 +1 @@
20.15 20.15.1
@@ -1,7 +1,7 @@
--- ---
title: The Immich core team goes full-time title: The Immich core team goes full-time
authors: [alextran] authors: [alextran]
tags: [update, announcement, futo] tags: [update, announcement, FUTO]
date: 2024-05-01T00:00 date: 2024-05-01T00:00
--- ---
+91
View File
@@ -0,0 +1,91 @@
---
title: Licensing announcement - Purchase a license to support Immich
authors: [alextran]
tags: [update, announcement, FUTO]
date: 2024-07-18T00:00
---
Hello everybody,
Firstly, on behalf of the Immich team, I'd like to thank everybody for your continuous support of Immich since the very first day! Your contributions, encouragement, and community engagement have helped bring Immich to its current state. The team and I are forever grateful for that.
Since our [last announcement of the core team joining FUTO to work on Immich full-time](https://immich.app/blog/2024/immich-core-team-goes-fulltime), one of the goals of our new position is to foster a healthy relationship between the developers and the users. We believe that this enables us to create great software, establish transparent policies and build trust.
We want to build a great software application that brings value to you and your loved ones' lives. We are not using you as a product, i.e., selling or tracking your data. We are not putting annoying ads into our software. We respect your privacy. We want to be compensated for the hard work we put in to build Immich for you.
With those notes, we have enabled a way for you to financially support the continued development of Immich, ensuring the software can move forward and will be maintained, by offering a lifetime license of the software. We think if you like and use software, you should pay for it, but _we're never going to force anyone to pay or try to limit Immich for those who don't._
There are two types of license that you can choose to purchase: **Server License** and **Individual License**.
### Server License
This is a lifetime license costing **$99.99**. The license is applied to the whole server. You and all users that use your server are licensed.
### Individual License
This is a lifetime license costing **$24.99**. The license is applied to a single user, and can be used on any server they choose to connect to.
<img
width="837"
alt="license-social-gh"
src="https://github.com/user-attachments/assets/241932ed-ef3b-44ec-a9e2-ee80754e0cca"
/>
You can purchase the license on [our page - https://buy.immich.app](https://buy.immich.app).
Starting with release `v1.109.0` you can purchase and enter your purchased license key directly in the app.
<img
width="1414"
alt="license-page-gh"
src="https://github.com/user-attachments/assets/364fc32a-f6ef-4594-9fea-28d5a26ad77c"
/>
## Thank you
Thank you again for your support, this will help create a strong foundation and stability for the Immich team to continue developing and maintaining the project that you love to use.
<p align="center">
<img
src="https://media.giphy.com/media/v1.Y2lkPTc5MGI3NjExbjY2eWc5Y2F0ZW56MmR4aWE0dDhzZXlidXRmYWZyajl1bWZidXZpcyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/87CKDqErVfMqY/giphy.gif"
width="550"
title="SUPPORT THE PROJECT!"
/>
</p>
<br />
<br />
Cheers! 🎉
Immich team
# FAQ
### 1. Where can I purchase a license?
There are several places where you can purchase the license from
- [https://buy.immich.app](https://buy.immich.app)
- [https://pay.futo.org](https://pay.futo.org/)
- or directly from the app.
### 2. Do I need both _Individual License_ and _Server License_?
No,
If you are the admin and the sole user, or your instance has less than a total of 4 users, you can buy the **Individual License** for each user.
If your instance has more than 4 users, it is more cost-effective to buy the **Server License**, which will license all the users on your instance.
### 3. What do I do if I don't pay?
You can continue using Immich for an unlimited trial period.
### 4. Will there be any paywalled features?
No, there will never be any paywalled features.
### 5. Where can I get support regarding payment issues?
You can email us with your `orderId` and your email address `billing@futo.org` or on our Discord server.
+16 -21
View File
@@ -167,7 +167,7 @@ Immich uses CLIP models. For more information about CLIP and its capabilities, r
### How does facial recognition work? ### How does facial recognition work?
For face detection and recognition, Immich uses [InsightFace models](https://github.com/deepinsight/insightface/tree/master/model_zoo). See [How Facial Recognition Works](/docs/features/facial-recognition#How-Facial-Recognition-Works) for details.
### How can I disable machine learning? ### How can I disable machine learning?
@@ -181,19 +181,15 @@ However, disabling all jobs will not disable the machine learning service itself
### I'm getting errors about models being corrupt or failing to download. What do I do? ### I'm getting errors about models being corrupt or failing to download. What do I do?
You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Huggingface][huggingface] and place them in the cache folder. You can delete the model cache volume, where models are downloaded. This will give the service a clean environment to download the model again. If models are failing to download entirely, you can manually download them from [Hugging Face][huggingface] and place them in the cache folder.
### Can I use a custom CLIP model? ### Can I use a custom CLIP model?
No, this is not supported. Only models listed in the [Huggingface][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added. No, this is not supported. Only models listed in the [Hugging Face][huggingface] page are compatible. Feel free to make a feature request if there's a model not listed here that you think should be added.
### I want to be able to search in other languages besides English. How can I do that? ### I want to be able to search in other languages besides English. How can I do that?
You can change to a multilingual model listed [here](https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7) by going to Administration > Machine Learning Settings > Smart Search and replacing the name of the model. Be sure to re-run Smart Search on all assets after this change. You can then search in over 100 languages. You can change to a multilingual CLIP model. See [here](/docs/features/smart-search#CLIP-model) for instructions.
:::note
Feel free to make a feature request if there's a model you want to use that isn't in [Immich Huggingface list][huggingface].
:::
### Does Immich support Facial Recognition for videos? ### Does Immich support Facial Recognition for videos?
@@ -234,7 +230,7 @@ ls clip/ facial-recognition/
### Why is Immich slow on low-memory systems like the Raspberry Pi? ### Why is Immich slow on low-memory systems like the Raspberry Pi?
Immich optionally uses machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely. Immich optionally uses transcoding and machine learning for several features. However, it can be too heavy to run on a Raspberry Pi. You can [mitigate](/docs/FAQ#can-i-lower-cpu-and-ram-usage) this or host Immich's machine-learning container on a [more powerful system](/docs/guides/remote-machine-learning), or [disable](/docs/FAQ#how-can-i-disable-machine-learning) machine learning entirely.
### Can I lower CPU and RAM usage? ### Can I lower CPU and RAM usage?
@@ -243,10 +239,12 @@ The initial backup is the most intensive due to the number of jobs running. The
- Lower the job concurrency for these jobs to 1. - Lower the job concurrency for these jobs to 1.
- Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2. - Under Settings > Transcoding Settings > Threads, set the number of threads to a low number like 1 or 2.
- Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good. - Under Settings > Machine Learning Settings > Facial Recognition > Model Name, you can change the facial recognition model to `buffalo_s` instead of `buffalo_l`. The former is a smaller and faster model, albeit not as good.
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this. - For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning. - If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit the amount of CPU and RAM usage? ### Can I limit CPU and RAM usage?
By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources. By default, a container has no resource constraints and can use as much of a given resource as the host's kernel scheduler allows. To limit this, you can add the following to the `docker-compose.yml` block of any containers that you want to have limited resources.
@@ -266,6 +264,8 @@ deploy:
</details> </details>
For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit). For more details, you can look at the [original docker docs](https://docs.docker.com/config/containers/resource_constraints/) or use this [guide](https://www.baeldung.com/ops/docker-memory-limit).
Note that memory constraints work by terminating the container, so this can introduce instability if set too low.
### How can I boost machine learning speed? ### How can I boost machine learning speed?
:::note :::note
@@ -275,21 +275,16 @@ This advice improves throughput, not latency. This is to say that it will make S
You can increase throughput by increasing the job concurrency for machine learning jobs (Smart Search, Face Detection). With higher concurrency, the host will work on more assets in parallel. You can do this by navigating to Administration > Settings > Job Settings and increasing concurrency as needed. You can increase throughput by increasing the job concurrency for machine learning jobs (Smart Search, Face Detection). With higher concurrency, the host will work on more assets in parallel. You can do this by navigating to Administration > Settings > Job Settings and increasing concurrency as needed.
:::danger :::danger
On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Beyond this, note that storage speed and latency may quickly become the limiting factor; particularly when using HDDs. On a normal machine, 2 or 3 concurrent jobs can probably max the CPU. Storage speed and latency can quickly become the limiting factor beyond this, particularly when using HDDs.
Do not exaggerate with the amount of jobs because you're probably thoroughly overloading the server. The concurrency can be increased more comfortably with a GPU, but should still not be above 16 in most cases.
More details can be found [here](https://discord.com/channels/979116623879368755/994044917355663450/1174711719994605708) Do not exaggerate with the job concurrency because you're probably thoroughly overloading the server.
::: :::
### Why is Immich using so much of my CPU? ### My server shows Server Status Offline | Version Unknown. What can I do?
When a large number of assets are uploaded to Immich, it makes sense that the CPU and RAM will be heavily used for machine learning work and creating image thumbnails. You need to enable WebSockets on your reverse proxy.
Once this process is completed, the percentage of CPU usage will drop to around 3-5% usage
### My server shows Server Status Offline | Version Unknown what can I do?
You need to enable Websocket on your reverse proxy.
--- ---
+68 -6
View File
@@ -2,7 +2,7 @@
## Overview ## Overview
Immich recognizes faces in your photos and videos and groups them together. You can then assign names to the faces and search for them. Immich recognizes faces in your photos and videos and groups them together into people. You can then assign names to these people and search for them.
The list of people is shown in the Explore page. The list of people is shown in the Explore page.
@@ -18,13 +18,75 @@ The asset detail view will also show the faces that are recognized in the asset.
## Actions ## Actions
Additional actions you can do with a detected person are: Additional actions you can do include:
- Change the feature face photo of the person - Changing the feature photo of the person
- Set date of birth - Setting a person's date of birth
- Merge two or more detected faces into one person - Merging two or more detected faces into one person
- Hide face - Hiding the faces of a person from the Explore page and detail view
- Assigning an unrecognized face to a person
It can be found from the app bar when you access the detail view of a person. It can be found from the app bar when you access the detail view of a person.
<img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/> <img src={require('./img/facial-recognition-4.png').default} title='Facial Recognition 4' width="70%"/>
## How Face Detection Works
Face detection sends the generated preview image to the machine learning service for processing. The service checks if it has the relevant model downloaded and downloads it if not. The image is decoded, pre-processed and passed to the face detection model (with hardware acceleration if configured). The bounding boxes and scores outputted from this model are used to crop and preprocess the image once again to be passed to a facial recognition model (also accelerated if configured). The embeddings from the recognition model, together with the bounding boxes and scores from the face detection model, are then sent back to the server to be added to the database. The embeddings in particular are indexed so they can be searched quickly during facial recognition clustering.
## How Facial Recognition Works
The facial recognition algorithm we use is derived from DBSCAN, a popular clustering algorithm. It essentially treats each detected face as a point in a graph and aims to group points that are close to each other.
:::note
An important concept is whether something is a _core point_. A core point has a minimum number of points around it within a certain distance. A non-core point can only be assigned to a cluster if it can reach a core point; a non-core point can't be used to extend a cluster even if it's part of one. In Immich, the _Minimum Recognized Faces_ setting controls the threshold to be considered a core point.
:::
For each face, it looks around it to find other faces within a certain distance. Faces within this distance are considered similar, so it then checks if any of these faces are associated with a person.
If there is an existing person, it assigns the person of the most similar face to the face being processed.
If there is none, then it has to determine something from the DBSCAN algorithm: whether the face is a _core point_. If there are a certain number of similar faces (by default 3, including the face being considered), then this face is a core point. A new person is created for this face and the face is assigned to it. When other faces are processed, if they're similar to this face, they'll see that it has an associated person and can be assigned to that person.
However, if there aren't enough similar faces, no new person will be created. Instead, the face will wait for all the other faces to be processed to see if any matches that previously didn't have an associated person now do. If they do, then the face will be assigned to that person. If not, this face will be considered an outlier, such as a stranger in the background of an image.
The algorithm has some subtle differences compared to DBSCAN:
- DBSCAN doesn't have a concept of incremental clustering: it clusters all points at once. In contrast, facial recognition has to evolve as more assets are added without re-clustering everything each time.
- The algorithm described above works within a set of queued assets. Once these faces are processed and a new round of faces are detected, the behavior will not be the same as traditional DBSCAN since it preserves the clusters (people) generated from the previous round.
- Facial recognition tries to wait for face detection and thumbnail generation to complete before starting for this reason: the larger the set of faces in the queue, the better the results will be.
- Re-running facial recognition on all assets afterwards does behave like DBSCAN, however.
- DBSCAN is designed for range-based searches (i.e. points within a distance), but high-dimensional vector indices are generally optimized for getting the closest K results. The recognition algorithm doesn't try to get _all_ similar faces within a distance for performance reasons. Instead, it searches for a small number of matches for each face. The end result should be very similar if not identical, but with possibly different performance characteristics.
- Because of this, part of the recognition process is handled during a nightly job to ensure that unassigned faces with potential matches can be recognized.
:::tip
If you didn't import your assets at once or if the server was able to process jobs faster than you could upload them, it's possible that the clustering was suboptimal. If you haven't put effort into the current results, it may be worth re-running facial recognition on all assets for the best starting point. If it's too late for that, you can also manually assign a selection of unassigned faces and queue _Missing_ for Facial Recognition to help it learn and assign more faces automatically.
:::
## Configuration
Navigating to Administration > Settings > Machine Learning Settings > Facial Recognition will show the options available.
:::tip
It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa.
:::
### Facial recognition model
There are a few different models available; the default is typically considered the best. On more constrained systems where the default is too intensive, you can choose a smaller model instead.
### Minimum detection score
This setting affects whether a result from the face detecton model is filtered out as a false positive. It may seem tempting to set this low to detect more faces, but it can lead to false positives that are difficult to deal with and can harm facial recognition. It is strongly recommended not to go below 0.5 for this setting. Setting it to a very high number like 0.9 is also not recommended: the default is already biased toward precision, so a threshold that high leads to many undetected faces.
After changing this setting, it will only apply to new face detection jobs. To apply the new setting to all assets, you need to re-run face detection for all assets.
### Maximum recognition distance
The distance threshold described in How Facial Recognition Works. The default works well for most people, but it may be worth lowering it if the library has twins or otherwise very similar looking people. A threshold that's too low just means needing to merge duplicate people after facial recognition, whereas a threshold too high can produce unsalvageable results. It is strongly recommended not to go below 0.3 or above 0.7.
### Minimum recognized faces
The core point threshold described in How Facial Recognition Works. This setting has a few implications. First, it takes effect immediately in that people with fewer faces than this are hidden from view. Secondly, it makes clustering more robust as it prevents loosely-related faces from being linked to each other by requiring a certain level of density.
Increasing this setting is a good idea if you increase the recognition distance or reduce the minimum detection score. Setting it to 1 effectively disables the concept of core points, but can be an option if you prefer a more hands-on approach.
@@ -123,6 +123,7 @@ Once this is done, you can continue to step 3 of "Basic Setup".
- You may want to choose a slower preset than for software transcoding to maintain quality and efficiency - You may want to choose a slower preset than for software transcoding to maintain quality and efficiency
- While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices - While you can use VAAPI with NVIDIA and Intel devices, prefer the more specific APIs since they're more optimized for their respective devices
- You can confirm the device is being recognized and used by checking its utilization (via `nvtop` for NVIDIA, `intel_gpu_top` for Intel, etc.) when transcoding. A lack of error logs when transcoding also indicates that it's being used.
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml [hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html [nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
+5 -4
View File
@@ -104,18 +104,19 @@ The `immich-server` container will need access to the gallery. Modify your docke
immich-server: immich-server:
volumes: volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload - ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro + - /mnt/nas/christmas-trip:/mnt/nas/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro + - /home/user/old-pics:/home/user/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro + - /mnt/media/videos:/mnt/media/videos:ro
+ - /mnt/media/videos2:/mnt/media/videos2 # the files in this folder can be deleted, as it does not end with :ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system. + - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
``` ```
:::tip :::tip
The `ro` flag at the end only gives read-only access to the volumes. While Immich does not modify files, it's a good practice to mount read-only. The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI.
::: :::
:::info :::info
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._ _Remember to run `docker compose up -d` to register the changes. Make sure you can see the mounted path in the container._
::: :::
### Create External Libraries ### Create External Libraries
+43 -18
View File
@@ -7,29 +7,30 @@ Immich uses Postgres as its search database for both metadata and smart search.
Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata. Smart search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
Archived photos are not included in search results by default. To include them, mark the checkbox in [advanced search filters](/docs/features/smart-search#advanced-search-filters).
:::tip Alternative CLIP Models
More powerful models can be used for more accurate search results. For more information, see the related [FAQ](/docs/FAQ#can-i-use-a-custom-clip-model).
:::
:::info
Smart Search is currently limited to 5,000 results for a single search on the web.
:::
## Advanced Search Filters ## Advanced Search Filters
In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app). In addition, Immich offers advanced search functionality, allowing you to find specific content using customizable search filters. These filters include location, one or more faces, specific albums, and more. You can try out the search filters on the [Demo site](https://demo.immich.app).
Smart search features include: The filters smart search allows you to search by include:
- Search for one or more faces (with or without context search). - People
- Search by Country or State or City or by all three. - Location
- Search by camera make and model. - Country
- Search by date range. - State
- Search by file name. - City
- Search by media types: image, video or all (**Note:** Image includes live images). - Camera
- Search by condition: not in any album or archive or Favorite or all conditions. - Make
- Model
- Date range
- File name or extension
- Media type
- Image (including live/motion photos)
- Video
- All
- Condition
- Not in any album
- Archived
- Favorited
<Tabs> <Tabs>
<TabItem value="Computer" label="Computer" default> <TabItem value="Computer" label="Computer" default>
@@ -47,3 +48,27 @@ Some search examples:
</TabItem> </TabItem>
</Tabs> </Tabs>
## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
### CLIP model
More powerful models can be used for more accurate search results, but are slower and can require more server resources. Check out the models [here][huggingface-clip] for more options!
[Multilingual models][huggingface-multilingual-clip] are also available so users can search in their native language. These models support over 100 languages; the `nllb` models in particular support 200.
:::note
Multilingual models are much slower and larger and perform slightly worse for English than English-only models. For this reason, only use them if you actually intend to search in a language besides English.
As a special case, the `ViT-H-14-quickgelu__dfn5b` and `ViT-H-14-378-quickgelu__dfn5b` models are excellent at many European languages despite not specifically being multilingual. They're very intensive regardless, however - especially the latter.
:::
Once you've chosen a model, change this setting to the name of the model you chose. Be sure to re-run Smart Search on all assets after this change.
:::note
Feel free to make a feature request if there's a model you want to use that we don't currently support.
:::
[huggingface-clip]: https://huggingface.co/collections/immich-app/clip-654eaefb077425890874cd07
[huggingface-multilingual-clip]: https://huggingface.co/collections/immich-app/multilingual-clip-654eb08c2382f591eeb8c2a7
+6 -24
View File
@@ -6,36 +6,18 @@ in a directory on the same machine.
# Mount the directory into the containers. # Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points in the section `immich-server:` under `volumes:` Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point.
```diff ```diff
immich-server: immich-server:
volumes: volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external - ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /home/user/photos1:/home/user/photos1:ro
+ - /mnt/photos2:/mnt/photos2:ro # you can delete this line if you only have one mount point, or you can add more lines if you have more than two
``` ```
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer: Restart Immich by running `docker compose up -d`.
```
EXTERNAL_PATH=<your-path-here>
```
On my computer, for example, I use this path:
```
EXTERNAL_PATH=/home/tenino/photos
```
:::info EXTERNAL_PATH design
The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously.
:::
Restart Immich.
```
docker compose down
docker compose up -d
```
# Create the library # Create the library
+1 -1
View File
@@ -4,7 +4,7 @@ This page gives a few pointers on how to access your Immich instance from outsid
You can read the [full discussion in Discord](https://discord.com/channels/979116623879368755/1122615710846308484) You can read the [full discussion in Discord](https://discord.com/channels/979116623879368755/1122615710846308484)
:::danger :::danger
Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you succeptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks. Never forward port 2283 directly to the internet without additional configuration. This will expose the web interface via http to the internet, making you susceptible to [man in the middle](https://en.wikipedia.org/wiki/Man-in-the-middle_attack) attacks.
::: :::
## Option 1: VPN to home network ## Option 1: VPN to home network
+6 -5
View File
@@ -4,14 +4,11 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`. - Set the URL in Machine Learning Settings on the Admin Settings page to point to the designated ML system, e.g. `http://workstation:3003`.
- Copy the following `docker-compose.yml` to your ML system. - Copy the following `docker-compose.yml` to your ML system.
- If using [hardware acceleration](/docs/features/ml-hardware-acceleration), the [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be added
- Start the container by running `docker compose up -d`. - Start the container by running `docker compose up -d`.
:::info :::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning container, but facial recognition is done in the `microservices` worker. Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
:::
:::note
The [hwaccel.ml.yml](https://github.com/immich-app/immich/releases/latest/download/hwaccel.ml.yml) file also needs to be in the same folder if trying to use [hardware acceleration](/docs/features/ml-hardware-acceleration).
::: :::
```yaml ```yaml
@@ -37,3 +34,7 @@ volumes:
``` ```
Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together. Please note that version mismatches between both hosts may cause instabilities and bugs, so make sure to always perform updates together.
:::caution
As an internal service, the machine learning container has no security measures whatsoever. Please be mindful of where it's deployed and who can access it.
:::
+12 -14
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_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_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | 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 | | | `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 | | | `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | | `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
@@ -157,18 +155,18 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning ## Machine Learning
| Variable | Description | Default | Containers | | Variable | Description | Default | Containers |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- | | :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | | `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | | `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | | `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | | `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | | `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | | `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning | | `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning | | `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | | `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | | `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
+4
View File
@@ -8,6 +8,10 @@ sidebar_position: 20
This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx). This method is experimental and not currently recommended for production use. For production, please refer to installing with [Docker Compose](/docs/install/docker-compose.mdx).
::: :::
:::note
The install script only supports Linux operating systems and requires Docker to be already installed on the system.
:::
In the shell, from a directory of your choice, run the following command: In the shell, from a directory of your choice, run the following command:
```bash ```bash
+1
View File
@@ -92,6 +92,7 @@ const config = {
alt: 'Immich Logo', alt: 'Immich Logo',
src: 'img/immich-logo-inline-light.png', src: 'img/immich-logo-inline-light.png',
srcDark: 'img/immich-logo-inline-dark.png', srcDark: 'img/immich-logo-inline-dark.png',
className: 'rounded-none',
}, },
items: [ items: [
{ {
+12 -10
View File
@@ -12640,9 +12640,10 @@
} }
}, },
"node_modules/picocolors": { "node_modules/picocolors": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
"integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==" "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
"license": "ISC"
}, },
"node_modules/picomatch": { "node_modules/picomatch": {
"version": "2.3.1", "version": "2.3.1",
@@ -12753,9 +12754,9 @@
} }
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.4.38", "version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -12770,9 +12771,10 @@
"url": "https://github.com/sponsors/ai" "url": "https://github.com/sponsors/ai"
} }
], ],
"license": "MIT",
"dependencies": { "dependencies": {
"nanoid": "^3.3.7", "nanoid": "^3.3.7",
"picocolors": "^1.0.0", "picocolors": "^1.0.1",
"source-map-js": "^1.2.0" "source-map-js": "^1.2.0"
}, },
"engines": { "engines": {
@@ -16374,9 +16376,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.2", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
+1 -1
View File
@@ -56,6 +56,6 @@
"node": ">=20" "node": ">=20"
}, },
"volta": { "volta": {
"node": "20.15.0" "node": "20.15.1"
} }
} }
+1 -1
View File
@@ -49,7 +49,7 @@ export function Timeline({ items }: Props): JSX.Element {
<div className="flex flex-col flex-grow justify-between gap-2"> <div className="flex flex-col flex-grow justify-between gap-2">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
{cardIcon === 'immich' ? ( {cardIcon === 'immich' ? (
<img src="img/immich-logo.svg" height="30" /> <img src="img/immich-logo.svg" height="30" className="rounded-none" />
) : ( ) : (
<Icon path={cardIcon} size={1} color={item.iconColor} /> <Icon path={cardIcon} size={1} color={item.iconColor} />
)} )}
-5
View File
@@ -8,7 +8,6 @@
@tailwind utilities; @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=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, html,
button { button {
@@ -48,7 +47,3 @@ img {
div[class^='announcementBar_'] { div[class^='announcementBar_'] {
min-height: 2rem; min-height: 2rem;
} }
.navbar__brand .navbar__title {
@apply font-immich-title text-2xl font-normal text-immich-primary dark:text-immich-dark-primary;
}
+77
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/pull/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>
);
}
+1 -1
View File
@@ -10,7 +10,7 @@ function HomepageHeader() {
<section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-slate-200 dark:bg-immich-dark-gray"> <section className="text-center m-6 p-12 border border-red-400 rounded-[50px] bg-slate-200 dark:bg-immich-dark-gray">
<img <img
src={isDarkTheme ? 'img/immich-logo-stacked-dark.svg' : 'img/immich-logo-stacked-light.svg'} src={isDarkTheme ? 'img/immich-logo-stacked-dark.svg' : 'img/immich-logo-stacked-light.svg'}
className="md:h-60 h-44 mb-2 antialiased" className="md:h-60 h-44 mb-2 antialiased rounded-none"
alt="Immich logo" alt="Immich logo"
/> />
<div className="sm:text-2xl text-lg md:text-4xl mb-12 sm:leading-tight"> <div className="sm:text-2xl text-lg md:text-4xl mb-12 sm:leading-tight">
+8
View File
@@ -1,4 +1,12 @@
[ [
{
"label": "v1.109.0",
"url": "https://v1.109.0.archive.immich.app"
},
{
"label": "v1.108.0",
"url": "https://v1.108.0.archive.immich.app"
},
{ {
"label": "v1.107.2", "label": "v1.107.2",
"url": "https://v1.107.2.archive.immich.app" "url": "https://v1.107.2.archive.immich.app"
-3
View File
@@ -21,9 +21,6 @@ module.exports = {
'immich-dark-fg': '#e5e7eb', 'immich-dark-fg': '#e5e7eb',
'immich-dark-gray': '#212121', 'immich-dark-gray': '#212121',
}, },
fontFamily: {
'immich-title': ['Snowburst One', 'cursive'],
},
}, },
}, },
plugins: [], plugins: [],
+1 -1
View File
@@ -1 +1 @@
20.15 20.15.1
+2
View File
@@ -26,6 +26,8 @@ services:
volumes: volumes:
- upload:/usr/src/app/upload - upload:/usr/src/app/upload
- ./test-assets:/test-assets - ./test-assets:/test-assets
extra_hosts:
- 'auth-server:host-gateway'
depends_on: depends_on:
- redis - redis
- database - database
+1099 -79
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -1,6 +1,6 @@
{ {
"name": "immich-e2e", "name": "immich-e2e",
"version": "1.107.2", "version": "1.109.0",
"description": "", "description": "",
"main": "index.js", "main": "index.js",
"type": "module", "type": "module",
@@ -23,7 +23,8 @@
"@immich/sdk": "file:../open-api/typescript-sdk", "@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1", "@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2", "@types/luxon": "^3.4.2",
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0", "@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4", "@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2", "@types/supertest": "^6.0.2",
@@ -35,11 +36,13 @@
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^54.0.0", "eslint-plugin-unicorn": "^54.0.0",
"exiftool-vendored": "^27.0.0", "exiftool-vendored": "^27.0.0",
"jose": "^5.6.3",
"luxon": "^3.4.4", "luxon": "^3.4.4",
"oidc-provider": "^8.5.1",
"pg": "^8.11.3", "pg": "^8.11.3",
"pngjs": "^7.0.0", "pngjs": "^7.0.0",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4", "prettier-plugin-organize-imports": "^4.0.0",
"socket.io-client": "^4.7.4", "socket.io-client": "^4.7.4",
"supertest": "^7.0.0", "supertest": "^7.0.0",
"typescript": "^5.3.3", "typescript": "^5.3.3",
@@ -47,6 +50,6 @@
"vitest": "^1.3.0" "vitest": "^1.3.0"
}, },
"volta": { "volta": {
"node": "20.15.0" "node": "20.15.1"
} }
} }
+27 -3
View File
@@ -507,6 +507,22 @@ describe('/asset', () => {
expect(status).toEqual(200); expect(status).toEqual(200);
}); });
it.skip('should geocode country from gps data in the middle of nowhere', async () => {
const { status } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ latitude: 42, longitude: 69 });
expect(status).toEqual(200);
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const asset = await getAssetInfo({ id: user1Assets[0].id }, { headers: asBearerAuth(user1.accessToken) });
expect(asset).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({ city: null, country: 'Kazakhstan' }),
});
});
it('should set the description', async () => { it('should set the description', async () => {
const { status, body } = await request(app) const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`) .put(`/assets/${user1Assets[0].id}`)
@@ -1170,17 +1186,25 @@ describe('/asset', () => {
// into the test here. // into the test here.
it.each([ it.each([
{ {
filepath: 'formats/motionphoto/Samsung One UI 5.jpg', filepath: 'formats/motionphoto/samsung-one-ui-5.jpg',
checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=', checksum: 'fr14niqCq6N20HB8rJYEvpsUVtI=',
}, },
{ {
filepath: 'formats/motionphoto/Samsung One UI 6.jpg', filepath: 'formats/motionphoto/samsung-one-ui-6.jpg',
checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=', checksum: 'lT9Uviw/FFJYCjfIxAGPTjzAmmw=',
}, },
{ {
filepath: 'formats/motionphoto/Samsung One UI 6.heic', filepath: 'formats/motionphoto/samsung-one-ui-6.heic',
checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=', checksum: '/ejgzywvgvzvVhUYVfvkLzFBAF0=',
}, },
{
filepath: 'formats/motionphoto/pixel-6-pro.jpg',
checksum: 'bFhLGbdK058PSk4FTfrSnoKWykc=',
},
{
filepath: 'formats/motionphoto/pixel-8a.jpg',
checksum: '7YdY+WF0h+CXHbiXpi0HiCMTTjs=',
},
])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => { ])(`should extract motionphoto video from $filepath`, async ({ filepath, checksum }) => {
const response = await utils.createAsset(admin.accessToken, { const response = await utils.createAsset(admin.accessToken, {
assetData: { assetData: {
+6
View File
@@ -100,6 +100,12 @@ describe('/auth/*', () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest()); 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 () => { it('should accept a correct password', async () => {
+241 -2
View File
@@ -1,12 +1,85 @@
import {
LoginResponseDto,
SystemConfigOAuthDto,
getConfigDefaults,
getMyUser,
startOAuth,
updateConfig,
} from '@immich/sdk';
import { errorDto } from 'src/responses'; import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils'; import { OAuthClient, OAuthUser } from 'src/setup/auth-server';
import { app, asBearerAuth, baseUrl, utils } from 'src/utils';
import request from 'supertest'; import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest'; import { beforeAll, describe, expect, it } from 'vitest';
const authServer = {
internal: 'http://auth-server:3000',
external: 'http://127.0.0.1:3000',
};
const redirect = async (url: string, cookies?: string[]) => {
const { headers } = await request(url)
.get('/')
.set('Cookie', cookies || []);
return { cookies: (headers['set-cookie'] as unknown as string[]) || [], location: headers.location };
};
const loginWithOAuth = async (sub: OAuthUser | string) => {
const { url } = await startOAuth({ oAuthConfigDto: { redirectUri: `${baseUrl}/auth/login` } });
// login
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
const response2 = await request(authServer.external + response1.location)
.post('/')
.set('Cookie', response1.cookies)
.type('form')
.send({ prompt: 'login', login: sub, password: 'password' });
// approve
const response3 = await redirect(response2.header.location, response1.cookies);
const response4 = await request(authServer.external + response3.location)
.post('/')
.type('form')
.set('Cookie', response3.cookies)
.send({ prompt: 'consent' });
const response5 = await redirect(response4.header.location, response3.cookies.slice(1));
const redirectUrl = response5.location;
expect(redirectUrl).toBeDefined();
const params = new URL(redirectUrl).searchParams;
expect(params.get('code')).toBeDefined();
expect(params.get('state')).toBeDefined();
return redirectUrl;
};
const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) => {
const options = { headers: asBearerAuth(token) };
const defaults = await getConfigDefaults(options);
const merged = {
...defaults.oauth,
buttonText: 'Login with Immich',
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
...dto,
};
await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options);
};
describe(`/oauth`, () => { describe(`/oauth`, () => {
let admin: LoginResponseDto;
beforeAll(async () => { beforeAll(async () => {
await utils.resetDatabase(); await utils.resetDatabase();
await utils.adminSetup(); admin = await utils.adminSetup();
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
}); });
describe('POST /oauth/authorize', () => { describe('POST /oauth/authorize', () => {
@@ -15,5 +88,171 @@ describe(`/oauth`, () => {
expect(status).toBe(400); expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty'])); expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
}); });
it('should return a redirect uri', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2283/auth/login' });
expect(status).toBe(201);
expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) });
const params = new URL(body.url).searchParams;
expect(params.get('client_id')).toBe('client-default');
expect(params.get('response_type')).toBe('code');
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2283/auth/login');
expect(params.get('state')).toBeDefined();
});
});
describe('POST /oauth/callback', () => {
it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty']));
});
it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['url should not be empty']));
});
it('should auto register the user by default', async () => {
const url = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
isAdmin: false,
name: 'OAuth User',
userEmail: 'oauth-auto-register@immich.app',
userId: expect.any(String),
});
});
it('should handle a user without an email', async () => {
const url = await loginWithOAuth(OAuthUser.NO_EMAIL);
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('OAuth profile does not have an email address'));
});
it('should set the quota from a claim', async () => {
const url = await loginWithOAuth(OAuthUser.WITH_QUOTA);
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-with-quota@immich.app',
});
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
expect(user.quotaSizeInBytes).toBe(25 * 2 ** 30); // 25 GiB;
});
it('should set the storage label from a claim', async () => {
const url = await loginWithOAuth(OAuthUser.WITH_USERNAME);
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
userId: expect.any(String),
userEmail: 'oauth-with-username@immich.app',
});
const user = await getMyUser({ headers: asBearerAuth(body.accessToken) });
expect(user.storageLabel).toBe('user-username');
});
it('should work with RS256 signed tokens', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.RS256_TOKENS,
clientSecret: OAuthClient.RS256_TOKENS,
autoRegister: true,
buttonText: 'Login with Immich',
signingAlgorithm: 'RS256',
});
const url = await loginWithOAuth('oauth-RS256-token');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
accessToken: expect.any(String),
isAdmin: false,
name: 'OAuth User',
userEmail: 'oauth-RS256-token@immich.app',
userId: expect.any(String),
});
});
it('should work with RS256 signed user profiles', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.RS256_PROFILE,
clientSecret: OAuthClient.RS256_PROFILE,
buttonText: 'Login with Immich',
profileSigningAlgorithm: 'RS256',
});
const url = await loginWithOAuth('oauth-signed-profile');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
userId: expect.any(String),
userEmail: 'oauth-signed-profile@immich.app',
});
});
it('should throw an error for an invalid token algorithm', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
signingAlgorithm: 'something-that-does-not-work',
});
const url = await loginWithOAuth('oauth-signed-bad');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(500);
expect(body).toMatchObject({
error: 'Internal Server Error',
message: 'Failed to finish oauth',
statusCode: 500,
});
});
describe('autoRegister: false', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
autoRegister: false,
buttonText: 'Login with Immich',
});
});
it('should not auto register the user', async () => {
const url = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
});
it('should link to an existing user by email', async () => {
const { userId } = await utils.userSetup(admin.accessToken, {
name: 'OAuth User 3',
email: 'oauth-user3@immich.app',
password: 'password',
});
const url = await loginWithOAuth('oauth-user3');
const { status, body } = await request(app).post('/oauth/callback').send({ url });
expect(status).toBe(201);
expect(body).toMatchObject({
userId,
userEmail: 'oauth-user3@immich.app',
});
});
});
}); });
}); });
+4 -4
View File
@@ -49,9 +49,9 @@ describe('/search', () => {
{ filename: '/albums/nature/silver_fir.jpg' }, { filename: '/albums/nature/silver_fir.jpg' },
{ filename: '/formats/heic/IMG_2682.heic' }, { filename: '/formats/heic/IMG_2682.heic' },
{ filename: '/formats/jpg/el_torcal_rocks.jpg' }, { filename: '/formats/jpg/el_torcal_rocks.jpg' },
{ filename: '/formats/motionphoto/Samsung One UI 6.jpg' }, { filename: '/formats/motionphoto/samsung-one-ui-6.jpg' },
{ filename: '/formats/motionphoto/Samsung One UI 6.heic' }, { filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
{ filename: '/formats/motionphoto/Samsung One UI 5.jpg' }, { filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } }, { filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
@@ -315,7 +315,7 @@ describe('/search', () => {
{ {
should: 'should search by originalFilename with spaces', should: 'should search by originalFilename with spaces',
deferred: () => ({ deferred: () => ({
dto: { originalFileName: 'Samsung One', type: 'IMAGE' }, dto: { originalFileName: 'samsung-one', type: 'IMAGE' },
assets: [assetOneJpg5, assetOneJpg6, assetOneHeic6], assets: [assetOneJpg5, assetOneJpg6, assetOneHeic6],
}), }),
}, },
@@ -9,11 +9,30 @@ describe(`immich-admin`, () => {
describe('list-users', () => { describe('list-users', () => {
it('should list the admin user', async () => { 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(exitCode).toBe(0);
expect(stderr).toBe(''); expect(stderr).toBe('');
expect(stdout).toContain("email: 'admin@immich.cloud'"); expect(stdout).toContain("email: 'admin@immich.cloud'");
expect(stdout).toContain("name: 'Immich Admin'"); 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:');
});
});
}); });
+6
View File
@@ -61,6 +61,12 @@ export const errorDto = {
message: 'The server already has an admin', message: 'The server already has an admin',
correlationId: expect.any(String), correlationId: expect.any(String),
}, },
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
correlationId: expect.any(String),
},
}; };
export const signupResponseDto = { export const signupResponseDto = {
+117
View File
@@ -0,0 +1,117 @@
import { exportJWK, generateKeyPair } from 'jose';
import Provider from 'oidc-provider';
export enum OAuthClient {
DEFAULT = 'client-default',
RS256_TOKENS = 'client-RS256-tokens',
RS256_PROFILE = 'client-RS256-profile',
}
export enum OAuthUser {
NO_EMAIL = 'no-email',
NO_NAME = 'no-name',
WITH_QUOTA = 'with-quota',
WITH_USERNAME = 'with-username',
}
const claims = [
{ sub: OAuthUser.NO_EMAIL },
{
sub: OAuthUser.NO_NAME,
email: 'oauth-no-name@immich.app',
email_verified: true,
},
{
sub: OAuthUser.WITH_USERNAME,
email: 'oauth-with-username@immich.app',
email_verified: true,
immich_username: 'user-username',
},
{
sub: OAuthUser.WITH_QUOTA,
email: 'oauth-with-quota@immich.app',
email_verified: true,
preferred_username: 'user-quota',
immich_quota: 25,
},
];
const withDefaultClaims = (sub: string) => ({
sub,
email: `${sub}@immich.app`,
name: 'OAuth User',
given_name: `OAuth`,
family_name: 'User',
email_verified: true,
});
const getClaims = (sub: string) => claims.find((user) => user.sub === sub) || withDefaultClaims(sub);
const setup = async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256');
const port = 3000;
const host = '0.0.0.0';
const oidc = new Provider(`http://${host}:${port}`, {
renderError: async (ctx, out, error) => {
console.error(out);
console.error(error);
ctx.body = 'Internal Server Error';
},
findAccount: (ctx, sub) => ({ accountId: sub, claims: () => getClaims(sub) }),
scopes: ['openid', 'email', 'profile'],
claims: {
openid: ['sub'],
email: ['email', 'email_verified'],
profile: ['name', 'given_name', 'family_name', 'preferred_username', 'immich_quota', 'immich_username'],
},
features: {
jwtUserinfo: {
enabled: true,
},
},
cookies: {
names: {
session: 'oidc.session',
interaction: 'oidc.interaction',
resume: 'oidc.resume',
state: 'oidc.state',
},
},
pkce: {
required: () => false,
},
jwks: { keys: [await exportJWK(privateKey)] },
clients: [
{
client_id: OAuthClient.DEFAULT,
client_secret: OAuthClient.DEFAULT,
redirect_uris: ['http://127.0.0.1:2283/auth/login'],
grant_types: ['authorization_code'],
response_types: ['code'],
},
{
client_id: OAuthClient.RS256_TOKENS,
client_secret: OAuthClient.RS256_TOKENS,
redirect_uris: ['http://127.0.0.1:2283/auth/login'],
grant_types: ['authorization_code'],
id_token_signed_response_alg: 'RS256',
jwks: { keys: [await exportJWK(publicKey)] },
},
{
client_id: OAuthClient.RS256_PROFILE,
client_secret: OAuthClient.RS256_PROFILE,
redirect_uris: ['http://127.0.0.1:2283/auth/login'],
grant_types: ['authorization_code'],
userinfo_signed_response_alg: 'RS256',
jwks: { keys: [await exportJWK(publicKey)] },
},
],
});
const onStart = () => console.log(`[auth-server] http://${host}:${port}/.well-known/openid-configuration`);
const app = oidc.listen(port, host, onStart);
return () => app.close();
};
export default setup;
+4 -9
View File
@@ -53,8 +53,7 @@ type AdminSetupOptions = { onboarding?: boolean };
type AssetData = { bytes?: Buffer; filename: string }; type AssetData = { bytes?: Buffer; filename: string };
const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich'; const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich';
const baseUrl = 'http://127.0.0.1:2283'; export const baseUrl = 'http://127.0.0.1:2283';
export const shareUrl = `${baseUrl}/share`; export const shareUrl = `${baseUrl}/share`;
export const app = `${baseUrl}/api`; export const app = `${baseUrl}/api`;
// TODO move test assets into e2e/assets // TODO move test assets into e2e/assets
@@ -64,13 +63,13 @@ export const tempDir = tmpdir();
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` }); export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key }); export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
export const immichCli = (args: string[]) => 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[]) => export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]); executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
const executeCommand = (command: string, args: string[]) => { const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void; 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' }); const child = spawn(command, args, { stdio: 'pipe' });
let stdout = ''; let stdout = '';
@@ -86,7 +85,7 @@ const executeCommand = (command: string, args: string[]) => {
}); });
}); });
return deferred; return { promise, child };
}; };
let client: pg.Client | null = null; let client: pg.Client | null = null;
@@ -152,10 +151,6 @@ export const utils = {
const sql: string[] = []; const sql: string[] = [];
if (tables.includes('asset_stack')) {
sql.push('UPDATE "assets" SET "stackId" = NULL;');
}
for (const table of tables) { for (const table of tables) {
if (table === 'system_metadata') { if (table === 'system_metadata') {
// prevent reverse geocoder from being re-initialized // prevent reverse geocoder from being re-initialized
@@ -51,6 +51,13 @@ test.describe('Shared Links', () => {
await page.getByText('DOWNLOADING', { exact: true }).waitFor(); 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 }) => { test('enter password for a shared link', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`); await page.goto(`/share/${sharedLinkPassword.key}`);
await page.getByPlaceholder('Password').fill('test-password'); await page.getByPlaceholder('Password').fill('test-password');
+2 -2
View File
@@ -1,11 +1,11 @@
import { defineConfig } from 'vitest/config'; import { defineConfig } from 'vitest/config';
// skip `docker compose up` if `make e2e` was already run // skip `docker compose up` if `make e2e` was already run
const globalSetup: string[] = []; const globalSetup: string[] = ['src/setup/auth-server.ts'];
try { try {
await fetch('http://127.0.0.1:2283/api/server-info/ping'); await fetch('http://127.0.0.1:2283/api/server-info/ping');
} catch { } catch {
globalSetup.push('src/setup.ts'); globalSetup.push('src/setup/docker-compose.ts');
} }
export default defineConfig({ export default defineConfig({
+1
View File
@@ -77,6 +77,7 @@ RUN apt-get update && \
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
WORKDIR /usr/src/app WORKDIR /usr/src/app
ARG DEVICE
ENV TRANSFORMERS_CACHE=/cache \ ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \ PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \ PYTHONUNBUFFERED=1 \
+6
View File
@@ -168,6 +168,12 @@ def warning() -> Iterator[mock.Mock]:
yield 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") @pytest.fixture(scope="function")
def snapshot_download() -> Iterator[mock.Mock]: def snapshot_download() -> Iterator[mock.Mock]:
with mock.patch("app.models.base.snapshot_download") as mocked: with mock.patch("app.models.base.snapshot_download") as mocked:
+12 -1
View File
@@ -29,6 +29,7 @@ from .schemas import (
InferenceEntry, InferenceEntry,
InferenceResponse, InferenceResponse,
MessageResponse, MessageResponse,
ModelFormat,
ModelIdentity, ModelIdentity,
ModelTask, ModelTask,
ModelType, ModelType,
@@ -195,7 +196,17 @@ async def load(model: InferenceModel) -> InferenceModel:
if model.load_attempts > 1: if model.load_attempts > 1:
raise HTTPException(500, f"Failed to load model '{model.model_name}'") raise HTTPException(500, f"Failed to load model '{model.model_name}'")
with lock: 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 return model
try: try:
+11 -12
View File
@@ -23,7 +23,7 @@ class InferenceModel(ABC):
self, self,
model_name: str, model_name: str,
cache_dir: Path | str | None = None, cache_dir: Path | str | None = None,
preferred_format: ModelFormat | None = None, model_format: ModelFormat | None = None,
session: ModelSession | None = None, session: ModelSession | None = None,
**model_kwargs: Any, **model_kwargs: Any,
) -> None: ) -> None:
@@ -31,7 +31,7 @@ class InferenceModel(ABC):
self.load_attempts = 0 self.load_attempts = 0
self.model_name = clean_name(model_name) 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.cache_dir = Path(cache_dir) if cache_dir is not None else self._cache_dir_default
self.model_format = preferred_format if preferred_format is not None else self._model_format_default self.model_format = model_format if model_format is not None else self._model_format_default
if session is not None: if session is not None:
self.session = session self.session = session
@@ -48,7 +48,7 @@ class InferenceModel(ABC):
self.load_attempts += 1 self.load_attempts += 1
self.download() self.download()
attempt = f"Attempt #{self.load_attempts + 1} to load" if self.load_attempts else "Loading" 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") log.info(f"{attempt} {self.model_type.replace('-', ' ')} model '{self.model_name}' to memory")
self.session = self._load() self.session = self._load()
self.loaded = True self.loaded = True
@@ -101,6 +101,9 @@ class InferenceModel(ABC):
self.cache_dir.mkdir(parents=True, exist_ok=True) self.cache_dir.mkdir(parents=True, exist_ok=True)
def _make_session(self, model_path: Path) -> ModelSession: def _make_session(self, model_path: Path) -> ModelSession:
if not model_path.is_file():
raise FileNotFoundError(f"Model file not found: {model_path}")
match model_path.suffix: match model_path.suffix:
case ".armnn": case ".armnn":
session: ModelSession = AnnSession(model_path) session: ModelSession = AnnSession(model_path)
@@ -144,17 +147,13 @@ class InferenceModel(ABC):
@property @property
def model_format(self) -> ModelFormat: def model_format(self) -> ModelFormat:
return self._preferred_format return self._model_format
@model_format.setter @model_format.setter
def model_format(self, preferred_format: ModelFormat) -> None: def model_format(self, model_format: ModelFormat) -> None:
log.debug(f"Setting preferred format to {preferred_format}") log.debug(f"Setting model format to {model_format}")
self._preferred_format = preferred_format self._model_format = model_format
@property @property
def _model_format_default(self) -> ModelFormat: def _model_format_default(self) -> ModelFormat:
prefer_ann = ann.ann.is_available and settings.ann return ModelFormat.ARMNN if ann.ann.is_available and settings.ann else ModelFormat.ONNX
ann_exists = (self.model_dir / "model.armnn").is_file()
if prefer_ann and not ann_exists:
log.warning(f"ARM NN is available, but '{self.model_name}' does not support ARM NN. Falling back to ONNX.")
return ModelFormat.ARMNN if prefer_ann and ann_exists else ModelFormat.ONNX
+2 -1
View File
@@ -22,11 +22,12 @@ class BaseCLIPTextualEncoder(InferenceModel):
return res return res
def _load(self) -> ModelSession: def _load(self) -> ModelSession:
session = super()._load()
log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'") log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'")
self.tokenizer = self._load_tokenizer() self.tokenizer = self._load_tokenizer()
log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'")
return super()._load() return session
@abstractmethod @abstractmethod
def _load_tokenizer(self) -> Tokenizer: def _load_tokenizer(self) -> Tokenizer:
@@ -1,4 +1,3 @@
from pathlib import Path
from typing import Any from typing import Any
import numpy as np import numpy as np
@@ -14,15 +13,9 @@ class FaceDetector(InferenceModel):
depends = [] depends = []
identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION) identity = (ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)
def __init__( def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None:
self,
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
self.min_score = model_kwargs.pop("minScore", min_score) 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: def _load(self) -> ModelSession:
session = self._make_session(self.model_path) session = self._make_session(self.model_path)
@@ -9,7 +9,7 @@ from numpy.typing import NDArray
from onnx.tools.update_model_dims import update_inputs_outputs_dims from onnx.tools.update_model_dims import update_inputs_outputs_dims
from PIL import Image 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.base import InferenceModel
from app.models.transforms import decode_cv2 from app.models.transforms import decode_cv2
from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType from app.schemas import FaceDetectionOutput, FacialRecognitionOutput, ModelFormat, ModelSession, ModelTask, ModelType
@@ -20,20 +20,14 @@ class FaceRecognizer(InferenceModel):
depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)] depends = [(ModelType.DETECTION, ModelTask.FACIAL_RECOGNITION)]
identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION) identity = (ModelType.RECOGNITION, ModelTask.FACIAL_RECOGNITION)
def __init__( def __init__(self, model_name: str, min_score: float = 0.7, **model_kwargs: Any) -> None:
self, super().__init__(model_name, **model_kwargs)
model_name: str,
min_score: float = 0.7,
cache_dir: Path | str | None = None,
**model_kwargs: Any,
) -> None:
super().__init__(clean_name(model_name), cache_dir, **model_kwargs)
self.min_score = model_kwargs.pop("minScore", min_score) self.min_score = model_kwargs.pop("minScore", min_score)
self.batch = self.model_format == ModelFormat.ONNX self.batch = self.model_format == ModelFormat.ONNX
def _load(self) -> ModelSession: def _load(self) -> ModelSession:
session = self._make_session(self.model_path) session = self._make_session(self.model_path)
if self.model_format == ModelFormat.ONNX and not has_batch_axis(session): if self.batch and not has_batch_axis(session):
self._add_batch_axis(self.model_path) self._add_batch_axis(self.model_path)
session = self._make_session(self.model_path) session = self._make_session(self.model_path)
self.model = ArcFaceONNX( self.model = ArcFaceONNX(
+74 -11
View File
@@ -43,7 +43,7 @@ class TestBase:
assert encoder.cache_dir == cache_dir 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.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", False) mocker.patch("ann.ann.is_available", False)
@@ -51,7 +51,7 @@ class TestBase:
assert encoder.model_format == ModelFormat.ONNX assert encoder.model_format == ModelFormat.ONNX
def test_sets_default_preferred_format_to_armnn_if_available(self, path: mock.Mock, 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.object(settings, "ann", True)
mocker.patch("ann.ann.is_available", True) mocker.patch("ann.ann.is_available", True)
path.suffix = ".armnn" path.suffix = ".armnn"
@@ -60,11 +60,11 @@ class TestBase:
assert encoder.model_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.object(settings, "ann", False)
mocker.patch("ann.ann.is_available", 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.model_format == ModelFormat.ARMNN assert encoder.model_format == ModelFormat.ARMNN
@@ -129,7 +129,7 @@ class TestBase:
) )
def test_download_downloads_armnn_if_preferred_format(self, snapshot_download: mock.Mock) -> None: def test_download_downloads_armnn_if_preferred_format(self, snapshot_download: mock.Mock) -> None:
encoder = OpenClipTextualEncoder("ViT-B-32__openai", preferred_format=ModelFormat.ARMNN) encoder = OpenClipTextualEncoder("ViT-B-32__openai", model_format=ModelFormat.ARMNN)
encoder.download() encoder.download()
snapshot_download.assert_called_once_with( snapshot_download.assert_called_once_with(
@@ -140,6 +140,19 @@ class TestBase:
ignore_patterns=[], 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") @pytest.mark.usefixtures("ort_session")
class TestOrtSession: class TestOrtSession:
@@ -467,16 +480,18 @@ class TestFaceRecognition:
assert isinstance(call_args[0][0], np.ndarray) assert isinstance(call_args[0][0], np.ndarray)
assert call_args[0][0].shape == (112, 112, 3) assert call_args[0][0].shape == (112, 112, 3)
def test_recognition_adds_batch_axis_for_ort(self, ort_session: mock.Mock, mocker: MockerFixture) -> None: 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) onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch( update_dims = mocker.patch(
"app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True "app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
) )
mocker.patch("app.models.base.InferenceModel.download") mocker.patch("app.models.base.InferenceModel.download")
mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX") 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_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))] 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() proto = mock.Mock()
@@ -492,27 +507,30 @@ class TestFaceRecognition:
onnx.load.return_value = proto onnx.load.return_value = proto
face_recognizer = FaceRecognizer("buffalo_s") face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
face_recognizer.load() face_recognizer.load()
assert face_recognizer.batch is True assert face_recognizer.batch is True
update_dims.assert_called_once_with(proto, {"input.1": ["batch", 3, 224, 224]}, {"output.1": ["batch", 800]}) 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) 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, mocker: MockerFixture) -> None: 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) onnx = mocker.patch("app.models.facial_recognition.recognition.onnx", autospec=True)
update_dims = mocker.patch( update_dims = mocker.patch(
"app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True "app.models.facial_recognition.recognition.update_inputs_outputs_dims", autospec=True
) )
mocker.patch("app.models.base.InferenceModel.download") mocker.patch("app.models.base.InferenceModel.download")
mocker.patch("app.models.facial_recognition.recognition.ArcFaceONNX") 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))] inputs = [SimpleNamespace(name="input.1", shape=("batch", 3, 224, 224))]
outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))] outputs = [SimpleNamespace(name="output.1", shape=("batch", 800))]
ort_session.return_value.get_inputs.return_value = inputs ort_session.return_value.get_inputs.return_value = inputs
ort_session.return_value.get_outputs.return_value = outputs ort_session.return_value.get_outputs.return_value = outputs
face_recognizer = FaceRecognizer("buffalo_s") face_recognizer = FaceRecognizer("buffalo_s", cache_dir=path)
face_recognizer.load() face_recognizer.load()
assert face_recognizer.batch is True assert face_recognizer.batch is True
@@ -520,6 +538,30 @@ class TestFaceRecognition:
onnx.load.assert_not_called() onnx.load.assert_not_called()
onnx.save.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 @pytest.mark.asyncio
class TestCache: class TestCache:
@@ -693,7 +735,7 @@ class TestLoad:
mock_model.clear_cache.assert_called_once() mock_model.clear_cache.assert_called_once()
assert mock_model.load.call_count == 2 assert mock_model.load.call_count == 2
async def test_load_clears_cache_and_raises_if_os_error_and_already_retried(self) -> None: async def test_load_raises_if_os_error_and_already_retried(self) -> None:
mock_model = mock.Mock(spec=InferenceModel) mock_model = mock.Mock(spec=InferenceModel)
mock_model.model_name = "test_model_name" mock_model.model_name = "test_model_name"
mock_model.model_type = ModelType.VISUAL mock_model.model_type = ModelType.VISUAL
@@ -707,6 +749,27 @@ class TestLoad:
mock_model.clear_cache.assert_not_called() mock_model.clear_cache.assert_not_called()
mock_model.load.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( @pytest.mark.skipif(
not settings.test_full, not settings.test_full,
+1 -1
View File
@@ -1,6 +1,6 @@
[tool.poetry] [tool.poetry]
name = "machine-learning" name = "machine-learning"
version = "1.107.2" version = "1.109.0"
description = "" description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"] authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md" readme = "README.md"
+3 -2
View File
@@ -5,12 +5,13 @@ lib_path="/usr/lib/$(arch)-linux-gnu/libmimalloc.so.2"
if ! [ "$DEVICE" = "openvino" ]; then if ! [ "$DEVICE" = "openvino" ]; then
export LD_PRELOAD="$lib_path" export LD_PRELOAD="$lib_path"
export LD_BIND_NOW=1 export LD_BIND_NOW=1
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
else
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=300}"
fi fi
: "${IMMICH_HOST:=[::]}" : "${IMMICH_HOST:=[::]}"
: "${IMMICH_PORT:=3003}" : "${IMMICH_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
: "${MACHINE_LEARNING_WORKER_TIMEOUT:=120}"
gunicorn app.main:app \ gunicorn app.main:app \
-k app.config.CustomUvicornWorker \ -k app.config.CustomUvicornWorker \
@@ -1,7 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich" <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.alextran.immich"
xmlns:tools="http://schemas.android.com/tools"> xmlns:tools="http://schemas.android.com/tools">
<application android:label="Immich" android:name=".ImmichApp" android:usesCleartextTraffic="true" <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 <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"
+2 -2
View File
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle', task: 'bundle',
build_type: 'Release', build_type: 'Release',
properties: { properties: {
"android.injected.version.code" => 147, "android.injected.version.code" => 149,
"android.injected.version.name" => "1.107.2", "android.injected.version.name" => "1.109.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') 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')
+1 -1
View File
@@ -167,7 +167,7 @@ SPEC CHECKSUMS:
path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943
path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02
permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6
photo_manager: 4f6810b7dfc4feb03b461ac1a70dacf91fba7604 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a
ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825
SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c
share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5
+11 -11
View File
@@ -44,7 +44,7 @@
886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 886774DBDDE6B35BF2B4F2CD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Immich-Debug.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Immich-Debug.app"; sourceTree = BUILT_PRODUCTS_DIR; };
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; }; 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
@@ -118,7 +118,7 @@
97C146EF1CF9000F007C117D /* Products */ = { 97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
97C146EE1CF9000F007C117D /* Runner.app */, 97C146EE1CF9000F007C117D /* Immich-Debug.app */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@@ -162,7 +162,7 @@
); );
name = Runner; name = Runner;
productName = Runner; productName = Runner;
productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productReference = 97C146EE1CF9000F007C117D /* Immich-Debug.app */;
productType = "com.apple.product-type.application"; productType = "com.apple.product-type.application";
}; };
/* End PBXNativeTarget section */ /* End PBXNativeTarget section */
@@ -383,7 +383,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 162; CURRENT_PROJECT_VERSION = 163;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -393,8 +393,8 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.107.0; MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.profile;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "Immich-Profile";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -525,7 +525,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 162; CURRENT_PROJECT_VERSION = 163;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -535,8 +535,8 @@
"@executable_path/Frameworks", "@executable_path/Frameworks",
); );
MARKETING_VERSION = 1.107.0; MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich.debug;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "Immich-Debug";
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -553,7 +553,7 @@
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 162; CURRENT_PROJECT_VERSION = 163;
DEVELOPMENT_TEAM = 2F67MQ8R79; DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO; ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist; INFOPLIST_FILE = Runner/Info.plist;
@@ -564,7 +564,7 @@
); );
MARKETING_VERSION = 1.107.0; MARKETING_VERSION = 1.107.0;
PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich; PRODUCT_BUNDLE_IDENTIFIER = app.alextran.immich;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = Immich;
PROVISIONING_PROFILE_SPECIFIER = ""; PROVISIONING_PROFILE_SPECIFIER = "";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
@@ -15,7 +15,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app" BuildableName = "Immich.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@@ -31,7 +31,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app" BuildableName = "Immich.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@@ -56,7 +56,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app" BuildableName = "Immich.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
@@ -73,7 +73,7 @@
<BuildableReference <BuildableReference
BuildableIdentifier = "primary" BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D" BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app" BuildableName = "Immich.app"
BlueprintName = "Runner" BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj"> ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference> </BuildableReference>
+121 -121
View File
@@ -1,124 +1,124 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>BGTaskSchedulerPermittedIdentifiers</key> <key>BGTaskSchedulerPermittedIdentifiers</key>
<array> <array>
<string>app.alextran.immich.backgroundFetch</string> <string>app.alextran.immich.backgroundFetch</string>
<string>app.alextran.immich.backgroundProcessing</string> <string>app.alextran.immich.backgroundProcessing</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>CADisableMinimumFrameDurationOnPhone</key>
<true /> <true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
<string>Immich</string> <string>${PRODUCT_NAME}</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string> <string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string> <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleLocalizations</key> <key>CFBundleLocalizations</key>
<array> <array>
<string>en</string> <string>en</string>
<string>ar</string> <string>ar</string>
<string>ca</string> <string>ca</string>
<string>cs</string> <string>cs</string>
<string>da</string> <string>da</string>
<string>de</string> <string>de</string>
<string>es</string> <string>es</string>
<string>fi</string> <string>fi</string>
<string>fr</string> <string>fr</string>
<string>he</string> <string>he</string>
<string>hi</string> <string>hi</string>
<string>hu</string> <string>hu</string>
<string>it</string> <string>it</string>
<string>ja</string> <string>ja</string>
<string>ko</string> <string>ko</string>
<string>lv</string> <string>lv</string>
<string>mn</string> <string>mn</string>
<string>nb</string> <string>nb</string>
<string>nl</string> <string>nl</string>
<string>pl</string> <string>pl</string>
<string>pt</string> <string>pt</string>
<string>ro</string> <string>ro</string>
<string>ru</string> <string>ru</string>
<string>sk</string> <string>sk</string>
<string>sl</string> <string>sl</string>
<string>sr</string> <string>sr</string>
<string>sv</string> <string>sv</string>
<string>th</string> <string>th</string>
<string>uk</string> <string>uk</string>
<string>vi</string> <string>vi</string>
<string>zh</string> <string>zh</string>
</array> </array>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>immich_mobile</string> <string>immich_mobile</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>1.107.1</string> <string>1.108.0</string>
<key>CFBundleSignature</key> <key>CFBundleSignature</key>
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>162</string> <string>163</string>
<key>FLTEnableImpeller</key> <key>FLTEnableImpeller</key>
<true /> <true/>
<key>ITSAppUsesNonExemptEncryption</key> <key>ITSAppUsesNonExemptEncryption</key>
<false /> <false/>
<key>LSApplicationQueriesSchemes</key> <key>LSApplicationQueriesSchemes</key>
<array> <array>
<string>https</string> <string>https</string>
</array> </array>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true /> <true/>
<key>MGLMapboxMetricsEnabledSettingShownInApp</key> <key>MGLMapboxMetricsEnabledSettingShownInApp</key>
<true /> <true/>
<key>NSAppTransportSecurity</key> <key>NSAppTransportSecurity</key>
<dict> <dict>
<key>NSAllowsArbitraryLoads</key> <key>NSAllowsArbitraryLoads</key>
<true /> <true/>
</dict> </dict>
<key>NSCameraUsageDescription</key> <key>NSCameraUsageDescription</key>
<string>We need to access the camera to let you take beautiful video using this app</string> <string>We need to access the camera to let you take beautiful video using this app</string>
<key>NSLocationWhenInUseUsageDescription</key> <key>NSLocationWhenInUseUsageDescription</key>
<string>Enable location setting to show position of assets on map</string> <string>Enable location setting to show position of assets on map</string>
<key>NSMicrophoneUsageDescription</key> <key>NSMicrophoneUsageDescription</key>
<string>We need to access the microphone to let you take beautiful video using this app</string> <string>We need to access the microphone to let you take beautiful video using this app</string>
<key>NSPhotoLibraryAddUsageDescription</key> <key>NSPhotoLibraryAddUsageDescription</key>
<string>We need to manage backup your photos album</string> <string>We need to manage backup your photos album</string>
<key>NSPhotoLibraryUsageDescription</key> <key>NSPhotoLibraryUsageDescription</key>
<string>We need to manage backup your photos album</string> <string>We need to manage backup your photos album</string>
<key>UIApplicationSupportsIndirectInputEvents</key> <key>UIApplicationSupportsIndirectInputEvents</key>
<true /> <true/>
<key>UIBackgroundModes</key> <key>UIBackgroundModes</key>
<array> <array>
<string>fetch</string> <string>fetch</string>
<string>processing</string> <string>processing</string>
</array> </array>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
<string>Main</string> <string>Main</string>
<key>UIStatusBarHidden</key> <key>UIStatusBarHidden</key>
<false /> <false/>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UISupportedInterfaceOrientations~ipad</key> <key>UISupportedInterfaceOrientations~ipad</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string> <string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<true /> <true/>
<key>io.flutter.embedded_views_preview</key> <key>io.flutter.embedded_views_preview</key>
<true /> <true/>
</dict> </dict>
</plist> </plist>
+3 -3
View File
@@ -16,10 +16,10 @@
default_platform(:ios) default_platform(:ios)
platform :ios do platform :ios do
desc "iOS Beta" desc "iOS Release"
lane :beta do lane :release do
increment_version_number( increment_version_number(
version_number: "1.107.2" version_number: "1.109.0"
) )
increment_build_number( increment_build_number(
build_number: latest_testflight_build_number + 1, build_number: latest_testflight_build_number + 1,
+2 -2
View File
@@ -104,7 +104,7 @@ Future<Isar> loadDb() async {
if (Platform.isIOS) IOSDeviceAssetSchema, if (Platform.isIOS) IOSDeviceAssetSchema,
], ],
directory: dir.path, directory: dir.path,
maxSizeMiB: 256, maxSizeMiB: 1024,
); );
Store.init(db); Store.init(db);
return db; return db;
@@ -192,7 +192,7 @@ class ImmichAppState extends ConsumerState<ImmichApp>
localizationsDelegates: context.localizationDelegates, localizationsDelegates: context.localizationDelegates,
supportedLocales: context.supportedLocales, supportedLocales: context.supportedLocales,
locale: context.locale, locale: context.locale,
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: true,
home: MaterialApp.router( home: MaterialApp.router(
title: 'Immich', title: 'Immich',
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
@@ -199,9 +199,12 @@ class SearchInputPage extends HookConsumerWidget {
padding: EdgeInsets.only( padding: EdgeInsets.only(
bottom: MediaQuery.of(context).viewInsets.bottom, bottom: MediaQuery.of(context).viewInsets.bottom,
), ),
child: LocationPicker( child: Padding(
onSelected: handleOnSelect, padding: const EdgeInsets.symmetric(horizontal: 16.0),
filter: filter.value.location, child: LocationPicker(
onSelected: handleOnSelect,
filter: filter.value.location,
),
), ),
), ),
), ),
@@ -242,7 +245,7 @@ class SearchInputPage extends HookConsumerWidget {
onSearch: search, onSearch: search,
onClear: handleClear, onClear: handleClear,
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(vertical: 16.0), padding: const EdgeInsets.all(16.0),
child: CameraPicker( child: CameraPicker(
onSelect: handleOnSelect, onSelect: handleOnSelect,
filter: filter.value.camera, filter: filter.value.camera,
+1 -13
View File
@@ -14,17 +14,6 @@ final partnerServiceProvider = Provider(
), ),
); );
enum PartnerDirection {
sharedWith("shared-with"),
sharedBy("shared-by");
const PartnerDirection(
this._value,
);
final String _value;
}
class PartnerService { class PartnerService {
final ApiService _apiService; final ApiService _apiService;
final Isar _db; final Isar _db;
@@ -34,8 +23,7 @@ class PartnerService {
Future<List<User>?> getPartners(PartnerDirection direction) async { Future<List<User>?> getPartners(PartnerDirection direction) async {
try { try {
final userDtos = final userDtos = await _apiService.partnersApi.getPartners(direction);
await _apiService.partnersApi.getPartners(direction._value);
if (userDtos != null) { if (userDtos != null) {
return userDtos.map((u) => User.fromPartnerDto(u)).toList(); return userDtos.map((u) => User.fromPartnerDto(u)).toList();
} }
+2 -2
View File
@@ -73,9 +73,9 @@ class UserService {
Future<List<User>?> getUsersFromServer() async { Future<List<User>?> getUsersFromServer() async {
final List<User>? users = await _getAllUsers(); final List<User>? users = await _getAllUsers();
final List<User>? sharedBy = final List<User>? sharedBy =
await _partnerService.getPartners(PartnerDirection.sharedBy); await _partnerService.getPartners(PartnerDirection.by);
final List<User>? sharedWith = final List<User>? sharedWith =
await _partnerService.getPartners(PartnerDirection.sharedWith); await _partnerService.getPartners(PartnerDirection.with_);
if (users == null || sharedBy == null || sharedWith == null) { if (users == null || sharedBy == null || sharedWith == null) {
_log.warning("Failed to refresh users"); _log.warning("Failed to refresh users");
@@ -194,7 +194,7 @@ class ExifBottomSheet extends HookConsumerWidget {
exifInfo.mm != null || exifInfo.mm != null ||
exifInfo.iso != null exifInfo.iso != null
? Text( ? Text(
"ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} s ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ",
style: context.textTheme.bodySmall, style: context.textTheme.bodySmall,
) )
: null, : null,
@@ -49,7 +49,7 @@ class ExifDetail extends StatelessWidget {
exifInfo?.mm != null || exifInfo?.mm != null ||
exifInfo?.iso != null exifInfo?.iso != null
? Text( ? Text(
"ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} s ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ", "ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ",
style: context.textTheme.bodySmall, style: context.textTheme.bodySmall,
) )
: null, : null,
@@ -38,10 +38,7 @@ class FilterBottomSheetScaffold extends StatelessWidget {
style: context.textTheme.headlineSmall, style: context.textTheme.headlineSmall,
), ),
), ),
Padding( buildChildWidget(),
padding: const EdgeInsets.symmetric(horizontal: 16),
child: buildChildWidget(),
),
Padding( Padding(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),
child: Row( child: Row(
+2 -1
View File
@@ -3,7 +3,7 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
- API version: 1.107.2 - API version: 1.109.0
- Generator version: 7.5.0 - Generator version: 7.5.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen - Build package: org.openapitools.codegen.languages.DartClientCodegen
@@ -353,6 +353,7 @@ Class | Method | HTTP request | Description
- [OAuthCallbackDto](doc//OAuthCallbackDto.md) - [OAuthCallbackDto](doc//OAuthCallbackDto.md)
- [OAuthConfigDto](doc//OAuthConfigDto.md) - [OAuthConfigDto](doc//OAuthConfigDto.md)
- [OnThisDayDto](doc//OnThisDayDto.md) - [OnThisDayDto](doc//OnThisDayDto.md)
- [PartnerDirection](doc//PartnerDirection.md)
- [PartnerResponseDto](doc//PartnerResponseDto.md) - [PartnerResponseDto](doc//PartnerResponseDto.md)
- [PathEntityType](doc//PathEntityType.md) - [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md) - [PathType](doc//PathType.md)
+1
View File
@@ -166,6 +166,7 @@ part 'model/o_auth_authorize_response_dto.dart';
part 'model/o_auth_callback_dto.dart'; part 'model/o_auth_callback_dto.dart';
part 'model/o_auth_config_dto.dart'; part 'model/o_auth_config_dto.dart';
part 'model/on_this_day_dto.dart'; part 'model/on_this_day_dto.dart';
part 'model/partner_direction.dart';
part 'model/partner_response_dto.dart'; part 'model/partner_response_dto.dart';
part 'model/path_entity_type.dart'; part 'model/path_entity_type.dart';
part 'model/path_type.dart'; part 'model/path_type.dart';
+4 -4
View File
@@ -67,8 +67,8 @@ class PartnersApi {
/// Performs an HTTP 'GET /partners' operation and returns the [Response]. /// Performs an HTTP 'GET /partners' operation and returns the [Response].
/// Parameters: /// Parameters:
/// ///
/// * [String] direction (required): /// * [PartnerDirection] direction (required):
Future<Response> getPartnersWithHttpInfo(String direction,) async { Future<Response> getPartnersWithHttpInfo(PartnerDirection direction,) async {
// ignore: prefer_const_declarations // ignore: prefer_const_declarations
final path = r'/partners'; final path = r'/partners';
@@ -97,8 +97,8 @@ class PartnersApi {
/// Parameters: /// Parameters:
/// ///
/// * [String] direction (required): /// * [PartnerDirection] direction (required):
Future<List<PartnerResponseDto>?> getPartners(String direction,) async { Future<List<PartnerResponseDto>?> getPartners(PartnerDirection direction,) async {
final response = await getPartnersWithHttpInfo(direction,); final response = await getPartnersWithHttpInfo(direction,);
if (response.statusCode >= HttpStatus.badRequest) { if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response)); throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+2
View File
@@ -390,6 +390,8 @@ class ApiClient {
return OAuthConfigDto.fromJson(value); return OAuthConfigDto.fromJson(value);
case 'OnThisDayDto': case 'OnThisDayDto':
return OnThisDayDto.fromJson(value); return OnThisDayDto.fromJson(value);
case 'PartnerDirection':
return PartnerDirectionTypeTransformer().decode(value);
case 'PartnerResponseDto': case 'PartnerResponseDto':
return PartnerResponseDto.fromJson(value); return PartnerResponseDto.fromJson(value);
case 'PathEntityType': case 'PathEntityType':
+3
View File
@@ -103,6 +103,9 @@ String parameterToString(dynamic value) {
if (value is MemoryType) { if (value is MemoryType) {
return MemoryTypeTypeTransformer().encode(value).toString(); return MemoryTypeTypeTransformer().encode(value).toString();
} }
if (value is PartnerDirection) {
return PartnerDirectionTypeTransformer().encode(value).toString();
}
if (value is PathEntityType) { if (value is PathEntityType) {
return PathEntityTypeTypeTransformer().encode(value).toString(); return PathEntityTypeTypeTransformer().encode(value).toString();
} }
+85
View File
@@ -0,0 +1,85 @@
//
// AUTO-GENERATED FILE, DO NOT MODIFY!
//
// @dart=2.18
// ignore_for_file: unused_element, unused_import
// ignore_for_file: always_put_required_named_parameters_first
// ignore_for_file: constant_identifier_names
// ignore_for_file: lines_longer_than_80_chars
part of openapi.api;
class PartnerDirection {
/// Instantiate a new enum with the provided [value].
const PartnerDirection._(this.value);
/// The underlying value of this enum member.
final String value;
@override
String toString() => value;
String toJson() => value;
static const by = PartnerDirection._(r'shared-by');
static const with_ = PartnerDirection._(r'shared-with');
/// List of all possible values in this [enum][PartnerDirection].
static const values = <PartnerDirection>[
by,
with_,
];
static PartnerDirection? fromJson(dynamic value) => PartnerDirectionTypeTransformer().decode(value);
static List<PartnerDirection> listFromJson(dynamic json, {bool growable = false,}) {
final result = <PartnerDirection>[];
if (json is List && json.isNotEmpty) {
for (final row in json) {
final value = PartnerDirection.fromJson(row);
if (value != null) {
result.add(value);
}
}
}
return result.toList(growable: growable);
}
}
/// Transformation class that can [encode] an instance of [PartnerDirection] to String,
/// and [decode] dynamic data back to [PartnerDirection].
class PartnerDirectionTypeTransformer {
factory PartnerDirectionTypeTransformer() => _instance ??= const PartnerDirectionTypeTransformer._();
const PartnerDirectionTypeTransformer._();
String encode(PartnerDirection data) => data.value;
/// Decodes a [dynamic value][data] to a PartnerDirection.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
PartnerDirection? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
case r'shared-by': return PartnerDirection.by;
case r'shared-with': return PartnerDirection.with_;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
}
}
}
return null;
}
/// Singleton [PartnerDirectionTypeTransformer] instance.
static PartnerDirectionTypeTransformer? _instance;
}
+9 -1
View File
@@ -23,6 +23,7 @@ class SystemConfigOAuthDto {
required this.issuerUrl, required this.issuerUrl,
required this.mobileOverrideEnabled, required this.mobileOverrideEnabled,
required this.mobileRedirectUri, required this.mobileRedirectUri,
required this.profileSigningAlgorithm,
required this.scope, required this.scope,
required this.signingAlgorithm, required this.signingAlgorithm,
required this.storageLabelClaim, required this.storageLabelClaim,
@@ -50,6 +51,8 @@ class SystemConfigOAuthDto {
String mobileRedirectUri; String mobileRedirectUri;
String profileSigningAlgorithm;
String scope; String scope;
String signingAlgorithm; String signingAlgorithm;
@@ -70,6 +73,7 @@ class SystemConfigOAuthDto {
other.issuerUrl == issuerUrl && other.issuerUrl == issuerUrl &&
other.mobileOverrideEnabled == mobileOverrideEnabled && other.mobileOverrideEnabled == mobileOverrideEnabled &&
other.mobileRedirectUri == mobileRedirectUri && other.mobileRedirectUri == mobileRedirectUri &&
other.profileSigningAlgorithm == profileSigningAlgorithm &&
other.scope == scope && other.scope == scope &&
other.signingAlgorithm == signingAlgorithm && other.signingAlgorithm == signingAlgorithm &&
other.storageLabelClaim == storageLabelClaim && other.storageLabelClaim == storageLabelClaim &&
@@ -88,13 +92,14 @@ class SystemConfigOAuthDto {
(issuerUrl.hashCode) + (issuerUrl.hashCode) +
(mobileOverrideEnabled.hashCode) + (mobileOverrideEnabled.hashCode) +
(mobileRedirectUri.hashCode) + (mobileRedirectUri.hashCode) +
(profileSigningAlgorithm.hashCode) +
(scope.hashCode) + (scope.hashCode) +
(signingAlgorithm.hashCode) + (signingAlgorithm.hashCode) +
(storageLabelClaim.hashCode) + (storageLabelClaim.hashCode) +
(storageQuotaClaim.hashCode); (storageQuotaClaim.hashCode);
@override @override
String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim]'; String toString() => 'SystemConfigOAuthDto[autoLaunch=$autoLaunch, autoRegister=$autoRegister, buttonText=$buttonText, clientId=$clientId, clientSecret=$clientSecret, defaultStorageQuota=$defaultStorageQuota, enabled=$enabled, issuerUrl=$issuerUrl, mobileOverrideEnabled=$mobileOverrideEnabled, mobileRedirectUri=$mobileRedirectUri, profileSigningAlgorithm=$profileSigningAlgorithm, scope=$scope, signingAlgorithm=$signingAlgorithm, storageLabelClaim=$storageLabelClaim, storageQuotaClaim=$storageQuotaClaim]';
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
final json = <String, dynamic>{}; final json = <String, dynamic>{};
@@ -108,6 +113,7 @@ class SystemConfigOAuthDto {
json[r'issuerUrl'] = this.issuerUrl; json[r'issuerUrl'] = this.issuerUrl;
json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled; json[r'mobileOverrideEnabled'] = this.mobileOverrideEnabled;
json[r'mobileRedirectUri'] = this.mobileRedirectUri; json[r'mobileRedirectUri'] = this.mobileRedirectUri;
json[r'profileSigningAlgorithm'] = this.profileSigningAlgorithm;
json[r'scope'] = this.scope; json[r'scope'] = this.scope;
json[r'signingAlgorithm'] = this.signingAlgorithm; json[r'signingAlgorithm'] = this.signingAlgorithm;
json[r'storageLabelClaim'] = this.storageLabelClaim; json[r'storageLabelClaim'] = this.storageLabelClaim;
@@ -133,6 +139,7 @@ class SystemConfigOAuthDto {
issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!, issuerUrl: mapValueOfType<String>(json, r'issuerUrl')!,
mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!, mobileOverrideEnabled: mapValueOfType<bool>(json, r'mobileOverrideEnabled')!,
mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!, mobileRedirectUri: mapValueOfType<String>(json, r'mobileRedirectUri')!,
profileSigningAlgorithm: mapValueOfType<String>(json, r'profileSigningAlgorithm')!,
scope: mapValueOfType<String>(json, r'scope')!, scope: mapValueOfType<String>(json, r'scope')!,
signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!, signingAlgorithm: mapValueOfType<String>(json, r'signingAlgorithm')!,
storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!, storageLabelClaim: mapValueOfType<String>(json, r'storageLabelClaim')!,
@@ -194,6 +201,7 @@ class SystemConfigOAuthDto {
'issuerUrl', 'issuerUrl',
'mobileOverrideEnabled', 'mobileOverrideEnabled',
'mobileRedirectUri', 'mobileRedirectUri',
'profileSigningAlgorithm',
'scope', 'scope',
'signingAlgorithm', 'signingAlgorithm',
'storageLabelClaim', 'storageLabelClaim',
+2 -2
View File
@@ -1162,10 +1162,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: photo_manager name: photo_manager
sha256: "8cf79918f6de9843b394a1670fe1aec54ebcac852b4b4c9ef88211894547dc61" sha256: "68d6099d07ce5033170f8368af8128a4555cf1d590a97242f83669552de989b1"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0-dev.5" version: "3.2.0"
photo_manager_image_provider: photo_manager_image_provider:
dependency: "direct main" dependency: "direct main"
description: description:
+2 -2
View File
@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none' publish_to: 'none'
version: 1.107.2+147 version: 1.109.0+149
environment: environment:
sdk: '>=3.3.0 <4.0.0' sdk: '>=3.3.0 <4.0.0'
@@ -15,7 +15,7 @@ dependencies:
path_provider_ios: path_provider_ios:
# TODO: upgrade to stable after 3.0.1 is released. 3.0.0 is broken # TODO: upgrade to stable after 3.0.1 is released. 3.0.0 is broken
# https://github.com/fluttercandies/flutter_photo_manager/pull/990#issuecomment-2058066427 # https://github.com/fluttercandies/flutter_photo_manager/pull/990#issuecomment-2058066427
photo_manager: ^3.0.0-dev.5 photo_manager: ^3.2.0
photo_manager_image_provider: ^2.1.0 photo_manager_image_provider: ^2.1.0
flutter_hooks: ^0.20.4 flutter_hooks: ^0.20.4
hooks_riverpod: ^2.4.9 hooks_riverpod: ^2.4.9
+1 -1
View File
@@ -50,7 +50,7 @@ final class TestUtils {
AndroidDeviceAssetSchema, AndroidDeviceAssetSchema,
IOSDeviceAssetSchema, IOSDeviceAssetSchema,
], ],
maxSizeMiB: 256, maxSizeMiB: 1024,
directory: "test/", directory: "test/",
); );
+13 -6
View File
@@ -3660,11 +3660,7 @@
"required": true, "required": true,
"in": "query", "in": "query",
"schema": { "schema": {
"enum": [ "$ref": "#/components/schemas/PartnerDirection"
"shared-by",
"shared-with"
],
"type": "string"
} }
} }
], ],
@@ -7011,7 +7007,7 @@
"info": { "info": {
"title": "Immich", "title": "Immich",
"description": "Immich API", "description": "Immich API",
"version": "1.107.2", "version": "1.109.0",
"contact": {} "contact": {}
}, },
"tags": [], "tags": [],
@@ -9473,6 +9469,13 @@
], ],
"type": "object" "type": "object"
}, },
"PartnerDirection": {
"enum": [
"shared-by",
"shared-with"
],
"type": "string"
},
"PartnerResponseDto": { "PartnerResponseDto": {
"properties": { "properties": {
"avatarColor": { "avatarColor": {
@@ -11027,6 +11030,9 @@
"mobileRedirectUri": { "mobileRedirectUri": {
"type": "string" "type": "string"
}, },
"profileSigningAlgorithm": {
"type": "string"
},
"scope": { "scope": {
"type": "string" "type": "string"
}, },
@@ -11051,6 +11057,7 @@
"issuerUrl", "issuerUrl",
"mobileOverrideEnabled", "mobileOverrideEnabled",
"mobileRedirectUri", "mobileRedirectUri",
"profileSigningAlgorithm",
"scope", "scope",
"signingAlgorithm", "signingAlgorithm",
"storageLabelClaim", "storageLabelClaim",
+1 -1
View File
@@ -1 +1 @@
20.15.0 20.15.1
+9 -9
View File
@@ -1,18 +1,18 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.107.2", "version": "1.109.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.107.2", "version": "1.109.0",
"license": "GNU Affero General Public License version 3", "license": "GNU Affero General Public License version 3",
"dependencies": { "dependencies": {
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"typescript": "^5.3.3" "typescript": "^5.3.3"
} }
}, },
@@ -22,9 +22,9 @@
"integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g=="
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "20.14.9", "version": "20.14.10",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.9.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz",
"integrity": "sha512-06OCtnTXtWOZBJlRApleWndH4JsRVs1pDCc8dLSQp+7PpUpX3ePdHyeNSFTeSe7FtKyQkrlPvHwJOW3SLd8Oyg==", "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -32,9 +32,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.5.2", "version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-NcRtPEOsPFFWjobJEtfihkLCZCXZt/os3zf8nTxjVH3RvTSxjrCamJpbExGvYOF+tFHc3pA65qpdwPbzjohhew==", "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
+3 -3
View File
@@ -1,6 +1,6 @@
{ {
"name": "@immich/sdk", "name": "@immich/sdk",
"version": "1.107.2", "version": "1.109.0",
"description": "Auto-generated TypeScript SDK for the Immich API", "description": "Auto-generated TypeScript SDK for the Immich API",
"type": "module", "type": "module",
"main": "./build/index.js", "main": "./build/index.js",
@@ -19,7 +19,7 @@
"@oazapfts/runtime": "^1.0.2" "@oazapfts/runtime": "^1.0.2"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^20.14.9", "@types/node": "^20.14.10",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, },
"repository": { "repository": {
@@ -28,6 +28,6 @@
"directory": "open-api/typescript-sdk" "directory": "open-api/typescript-sdk"
}, },
"volta": { "volta": {
"node": "20.15.0" "node": "20.15.1"
} }
} }
+7 -2
View File
@@ -1,6 +1,6 @@
/** /**
* Immich * Immich
* 1.107.2 * 1.109.0
* DO NOT MODIFY - This file has been generated using oazapfts. * DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts * See https://www.npmjs.com/package/oazapfts
*/ */
@@ -1063,6 +1063,7 @@ export type SystemConfigOAuthDto = {
issuerUrl: string; issuerUrl: string;
mobileOverrideEnabled: boolean; mobileOverrideEnabled: boolean;
mobileRedirectUri: string; mobileRedirectUri: string;
profileSigningAlgorithm: string;
scope: string; scope: string;
signingAlgorithm: string; signingAlgorithm: string;
storageLabelClaim: string; storageLabelClaim: string;
@@ -2128,7 +2129,7 @@ export function unlinkOAuthAccount(opts?: Oazapfts.RequestOpts) {
})); }));
} }
export function getPartners({ direction }: { export function getPartners({ direction }: {
direction: "shared-by" | "shared-with"; direction: PartnerDirection;
}, opts?: Oazapfts.RequestOpts) { }, opts?: Oazapfts.RequestOpts) {
return oazapfts.ok(oazapfts.fetchJson<{ return oazapfts.ok(oazapfts.fetchJson<{
status: 200; status: 200;
@@ -3131,6 +3132,10 @@ export enum Type2 {
export enum MemoryType { export enum MemoryType {
OnThisDay = "on_this_day" OnThisDay = "on_this_day"
} }
export enum PartnerDirection {
SharedBy = "shared-by",
SharedWith = "shared-with"
}
export enum PathEntityType { export enum PathEntityType {
Asset = "asset", Asset = "asset",
Person = "person", Person = "person",
+51 -27
View File
@@ -37,72 +37,96 @@
## Warnung ## Warnung
- ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung. - ⚠️ Das Projekt befindet sich in **sehr aktiver** Entwicklung.
- ⚠️ Erwarte Fehler und Änderungen mit Breaking-Changes. - ⚠️ Gehe von möglichen Fehlern und von Änderungen mit Breaking-Changes aus.
- ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.** - ⚠️ **Nutze die App auf keinen Fall als einziges Speichermedium für deine Fotos und Videos.**
- ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos! - ⚠️ Befolge immer die [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) Backup-Regel für deine wertvollen Fotos und Videos!
> [!NOTE]
> Die Hauptdokumentation, einschließlich der Installationsanleitungen, befinden sich unter https://immich.app/.
## Inhalt ## Inhalt
- [Offizielle Dokumentation](https://immich.app/docs) - [Offizielle Dokumentation](https://immich.app/docs)
- [Über Immich](https://immich.app/docs/overview/introduction)
- [Installation](https://immich.app/docs/install/requirements)
- [Roadmap](https://github.com/orgs/immich-app/projects/1) - [Roadmap](https://github.com/orgs/immich-app/projects/1)
- [Demo](#demo) - [Demo](#demo)
- [Funktionen](#funktionen) - [Funktionen](#funktionen)
- [Einführung](https://immich.app/docs/overview/introduction) - [Übersetzungen](https://immich.app/docs/developer/translations)
- [Installation](https://immich.app/docs/install/requirements)
- [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project) - [Beitragsrichtlinien](https://immich.app/docs/overview/support-the-project)
## Dokumentation
Die Hauptdokumentation, inklusive Installationsanleitungen, ist unter https://immich.app zu finden.
## Demo ## Demo
Die Web-Demo kannst Du unter https://demo.immich.app finden. Die Web-Demo kannst Du unter https://demo.immich.app finden.
Die Demo läuft auf einer Free Tier Oracle VM in Amsterdam mit einer 2.4Ghz Quad-Core ARM64 CPU und 24GB RAM.
Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben. Für die Handy-App kannst Du `https://demo.immich.app/api` als `Server Endpoint URL` angeben.
```bash title="Demo Credential" ### Login Daten
Die Anmeldedaten
email: demo@immich.app
passwort: demo
```
``` | Email | Password |
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM | --------------- | -------- |
``` | demo@immich.app | demo |
## Funktionen ## Funktionen
| Funktionen | Mobil | Web | | Funktionen | Mobil | Web |
| ---------------------------------------------------- | ------ | ----- | | ---------------------------------------------------- | ------ | ----- |
| Fotos & Videos hochladen und ansehen | Ja | Ja | | Fotos & Videos hochladen und ansehen | Ja | Ja |
| Automatisches Backup wenn die App geöffnet ist | Ja | n. a. | | Automatische Sicherung beim öffnen der App | Ja | k. A |
| Selektive Auswahl von Alben zum Sichern | Ja | n. a. | | Selektive Auswahl von Alben zum Sichern | Ja | k. A |
| Vermeidung von Duplikaten | Ja | Ja |
| Fotos und Videos auf das Gerät herunterladen | Ja | Ja | | Fotos und Videos auf das Gerät herunterladen | Ja | Ja |
| Unterstützt mehrere Benutzer | Ja | Ja | | Unterstützung mehrerer Benutzer | Ja | Ja |
| Album und geteilte Alben | Ja | Ja | | Alben und geteilte Alben | Ja | Ja |
| Scrollleiste | Ja | Ja | | Scrollbar mit Scrubbing-/Drag-Funktion | Ja | Ja |
| Unterstützt RAW Formate | Ja | Ja | | Unterstützung für RAW Formate | Ja | Ja |
| Metadaten anzeigen (EXIF, Karte) | Ja | Ja | | Metadaten anzeigen (EXIF, Karte) | Ja | Ja |
| Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja | | Suchen nach Metadaten, Objekten, Gesichtern und CLIP | Ja | Ja |
| Administrative Funktionen (Benutzerverwaltung) | Nein | Ja | | Administrative Funktionen (Benutzerverwaltung) | Nein | Ja |
| Backup im Hintergrund | Ja | n. a. | | Hintergrundsicherung | Ja | k. A |
| Virtuelles Scrollen | Ja | Ja | | Virtuelles Scrollen | Ja | Ja |
| OAuth Unterstützung | Ja | Ja | | OAuth Unterstützung | Ja | Ja |
| API-Schlüssel | n. a. | Ja | | API-Schlüssel | k. A | Ja |
| LivePhoto/MotionPhoto Backup und Wiedergabe | Ja | Ja | | LivePhoto/MotionPhoto Sicherung und Wiedergabe | Ja | Ja |
| Unterstützung für 360-Grad-Bilder | Nein | Ja |
| Benutzerdefinierte Speicherstruktur | Ja | Ja | | Benutzerdefinierte Speicherstruktur | Ja | Ja |
| Öffentliches Teilen | Nein | Ja | | Öffentliches Teilen | Nein | Ja |
| Archive und Favoriten | Ja | Ja | | Archiv und Favoriten | Ja | Ja |
| Globale Karte | Ja | Ja | | Globale Karte | Ja | Ja |
| Teilen mit Partner | Ja | Ja | | Partnerfreigabe (Teilen) | Ja | Ja |
| Gesichtserkennung und Gruppierung | Ja | Ja | | Gesichtserkennung und -gruppierung | Ja | Ja |
| Rückblicke (heute vor x Jahren) | Ja | Ja | | Rückblicke (heute vor x Jahren) | Ja | Ja |
| Offline Unterstützung | Ja | Nein | | Offline Unterstützung | Ja | Nein |
| Schreibgeschützte Gallerie | Ja | Ja | | Schreibgeschützte Gallerie | Ja | Ja |
| Gestapelte Bilder | Ja | Ja | | Gestapelte Bilder | Ja | Ja |
## Übersetzungen
Mehr zum Thema Übersetzungen kannst du [hier](https://immich.app/docs/developer/translations) erfahren.
<a href="https://hosted.weblate.org/engage/immich/">
<img src="https://hosted.weblate.org/widget/immich/immich/multi-auto.svg" alt="Translation status" />
</a>
## Repository-Aktivität
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Github Sterne
<a href="https://star-history.com/#immich-app/immich&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" />
<img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture>
</a>
## Mitwirkende ## Mitwirkende
<a href="https://github.com/alextran1502/immich/graphs/contributors"> <a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/> <img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a> </a>
+20 -20
View File
@@ -42,7 +42,7 @@
- ⚠️ Volg altijd het [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan voor je kostbare foto's en video's! - ⚠️ Volg altijd het [3-2-1](https://www.backblaze.com/blog/the-3-2-1-backup-strategy/) backup plan voor je kostbare foto's en video's!
## Inhoud ## Inhoud
s
- [Officiële documentatie](https://immich.app/docs) - [Officiële documentatie](https://immich.app/docs)
- [Toekomstplannen](https://github.com/orgs/immich-app/projects/1) - [Toekomstplannen](https://github.com/orgs/immich-app/projects/1)
- [Demo](#demo) - [Demo](#demo)
@@ -57,24 +57,15 @@ De belangrijkste documentatie, inclusief installatie handleidingen, zijn te vind
## Demo ## Demo
De demo is te bekijken op https://demo.immich.app. Je kunt de demo [hier](https://demo.immich.app/) bekijken. De demo server is actief op een Free-tier Oracle VM in Amsterdam met een 2.4GHz quad-core ARM64 CPU en 24GB RAM.
Voor de mobiele app kunt u gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL` Voor de mobiele app kun je gebruik maken van `https://demo.immich.app/api` voor de `Server Endpoint URL`
```bash title="Demo inloggegevens" ### Login gegevens
De inloggegevens
email: demo@immich.app
wachtwoord: demo
```
```
Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
```
## Activiteit
![Activiteit](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image"
| Email | Wachtwoord |
| --------------- | ---------- |
| demo@immich.app | demo |
# Functies # Functies
@@ -107,12 +98,15 @@ Spec: Free-tier Oracle VM - Amsterdam - 2.4Ghz quad-core ARM64 CPU, 24GB RAM
| Herinneringen (x jaar geleden) | Ja | Ja | | Herinneringen (x jaar geleden) | Ja | Ja |
| Offline-ondersteuning | Ja | Nee | | Offline-ondersteuning | Ja | Nee |
| Alleen-lezen galerij | Ja | Ja | | Alleen-lezen galerij | Ja | Ja |
| Gestapelde foto's | Ja | Ja |
## Contributie-leden ## Vertalingen
<a href="https://github.com/alextran1502/immich/graphs/contributors"> Je kunt [hier](https://immich.app/docs/developer/translations) meer over vertalingen lezen.
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a> ## Repository activiteit
![Activities](https://repobeats.axiom.co/api/embed/9e86d9dc3ddd137161f2f6d2e758d7863b1789cb.svg "Repobeats analytics image")
## Ster geschiedenis ## Ster geschiedenis
@@ -123,3 +117,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%" /> <img alt="Star History Chart" src="https://api.star-history.com/svg?repos=immich-app/immich&type=Date" width="100%" />
</picture> </picture>
</a> </a>
## Contributie-leden
<a href="https://github.com/alextran1502/immich/graphs/contributors">
<img src="https://contrib.rocks/image?repo=immich-app/immich" width="100%"/>
</a>
+1 -1
View File
@@ -1 +1 @@
20.15 20.15.1
+5 -5
View File
@@ -1,5 +1,5 @@
# dev build # dev build
FROM ghcr.io/immich-app/base-server-dev:20240625@sha256:3899114e00a55cb4138b02e1a5da4d2bd8d401a30c72a706134e0d5567c296eb as dev FROM ghcr.io/immich-app/base-server-dev:20240717@sha256:11fca55ca8f3ca693929eb3a5dd318e75c7b2ed2ed181658816666b1c93a33f8 as dev
RUN apt-get install --no-install-recommends -yqq tini RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app WORKDIR /usr/src/app
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
# web build # web build
FROM node:20.15.0-alpine3.20@sha256:df01469346db2bf1cfc1f7261aeab86b2960efa840fe2bd46d83ff339f463665 as web FROM node:20.15.1-alpine3.20@sha256:34b7aa411056c85dbf71d240d26516949b3f72b318d796c26b57caaa1df5639a as web
WORKDIR /usr/src/open-api/typescript-sdk WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
@@ -41,7 +41,7 @@ RUN npm run build
# prod build # prod build
FROM ghcr.io/immich-app/base-server-prod:20240625@sha256:19d095589b75bee07c4fd72f53a25c527cd6c5b78162a723d2d8db8e7cd51483 FROM ghcr.io/immich-app/base-server-prod:20240717@sha256:7679f08295d289dbad1e50f93340132d8a4ec4ac19709888faf9df5fdd34d1d1
WORKDIR /usr/src/app WORKDIR /usr/src/app
ENV NODE_ENV=production \ ENV NODE_ENV=production \
@@ -50,7 +50,7 @@ ENV NODE_ENV=production \
COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/node_modules ./node_modules
COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/dist ./dist
COPY --from=prod /usr/src/app/bin ./bin COPY --from=prod /usr/src/app/bin ./bin
COPY --from=web /usr/src/app/build ./www COPY --from=web /usr/src/app/build /build/www
COPY server/resources resources COPY server/resources resources
COPY server/package.json server/package-lock.json ./ COPY server/package.json server/package-lock.json ./
COPY server/start*.sh ./ COPY server/start*.sh ./
@@ -80,4 +80,4 @@ EXPOSE 3001
ENTRYPOINT ["tini", "--", "/bin/bash"] ENTRYPOINT ["tini", "--", "/bin/bash"]
CMD ["start.sh"] CMD ["start.sh"]
HEALTHCHECK CMD npm run healthcheck HEALTHCHECK CMD immich-healthcheck
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
node /usr/src/app/dist/utils/healthcheck.js
+2220 -2041
View File
File diff suppressed because it is too large Load Diff

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