Compare commits

...

74 Commits

Author SHA1 Message Date
Alex Tran
e04d25d8f5 chore(mobile): better second to first assets shown 2024-06-12 16:58:58 -05:00
Alex
c642150b85 chore(mobile): post release task (#10228) 2024-06-12 14:17:58 -05:00
Alex The Bot
a8a7d29891 Version v1.106.3 2024-06-12 18:26:10 +00:00
Alex
67e98ed313 fix(mobile): video player not updating state (#10220)
* fix(mobile): video player not updating state

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

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

* fixes

* format fix

* translation keys fix

* "merge" key fix

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

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

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

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

* suggestion fix

* trash pluralization

* video+photo count fix

* format fix

* unused removal

* translation key fix

* duplicate key removal

* format fix

---------

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

* Update server/src/utils/misc.ts

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

---------

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

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

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

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

* styling

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

* color

---------

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

* more unused keys

* formatting

* fix(web): incorrectly used translations

* fix and remove unused translations

---------

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

* remove href attribute from Thumbnail

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

* JobSettings translations

* feedback

* missed one

* feedback
2024-06-12 11:54:40 +01:00
Weblate (bot)
0e1311e3d3 chore(web): update translations (#10152)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

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

This reverts commit afa10ebcb2.

* feat(server): enable exiftool largefilesupport by passing options to read
2024-06-12 05:43:38 -05:00
Mert
cdc98de848 fix(server): increase pixel limit for thumbnail generation (#10181)
disable input limit
2024-06-11 22:11:03 -04:00
Mert
126cbeabe8 feat(server): add av1 support for vaapi (#10180)
add av1
2024-06-12 00:24:06 +00:00
Mert
2e0c6f6fff fix: postgres health check reporting any db without checksums as unhealthy (#10178)
handle disabled checksumming
2024-06-12 00:18:24 +00:00
Alex The Bot
81790ab166 Version v1.106.2 2024-06-11 19:09:13 +00:00
Alejandro Armas
69b948f3d0 fix(mobile): Motion Photos stopping music (#10151)
Add videoPlayer opt to prevent motionPhotos pausing music
2024-06-11 11:14:49 -05:00
Weblate (bot)
4b2ed28b1a chore: update translations (#10141)
chore(web): update translations

Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
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/ja/
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/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alex van den Hoogen <alex3305@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Joseph <josephlegrand33+hosted.weblate.org@gmail.com>
Co-authored-by: Kentai Radiquum <kentai.waah@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Marcos Besteiro López (MarcosBL) <marcosbl@gmail.com>
Co-authored-by: Mario <shopping.uncate@aleeas.com>
Co-authored-by: Michał Kulik <michal.kulik91@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Zack Pollard <zack@futo.org>
Co-authored-by: upsetdog <upsetdog@proton.me>
Co-authored-by: Łukasz Kierepka <lukasz_kierepka@hotmail.com>
2024-06-11 17:06:53 +01:00
Michel Heusschen
b8e6ae65b1 fix(web): backwards asset navigation in GalleryViewer (#10132)
* fix(web): backwards asset navigation in GalleryViewer

* fix ctrl/cmd click

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-11 15:27:18 +00:00
renovate[bot]
36bdbf93a7 fix(deps): update machine-learning (#10099)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-06-11 11:24:10 -04:00
Alex
3eee6c4dcf fix(web): cannot view image when metadata sharing is turned off for public sharing (#10145)
* fix(web): cannot view image when metadata sharing is turned off for public sharing

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

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

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-06-11 15:23:48 +00:00
Michel Heusschen
3a3676bc82 fix(server): cache-control header missing from / requests (#10131)
* fix(server): cache-control header missing from / requests

* disable extension fallback
2024-06-11 10:18:52 -05:00
Zack Pollard
34fc572276 chore: update translations (#10140)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 81.0% (631 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 81.0% (631 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 15:28:03 +01:00
Zack Pollard
ef17c257ef chore: update translations (#10138)
* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Spanish)

Currently translated at 4.2% (33 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Russian)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Czech)

Currently translated at 2.6% (21 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Russian)

Currently translated at 0.7% (6 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Italian)

Currently translated at 4.4% (35 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Polish)

Currently translated at 0.6% (5 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 6.0% (47 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Russian)

Currently translated at 2.0% (16 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 80.2% (625 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 80.2% (625 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Vietnamese)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

---------

Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Héctor Martínez Juste <hectorzin@hotmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Stefan Gries <stefan@gries.nrw>
Co-authored-by: Bouchet Mateo <mateo.bouchet+hosted.weblate.org@mhaz42.fr>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Quan <weiyideai520@hotmail.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 15:16:23 +01:00
Weblate (bot)
4c69cb89d7 chore: update translations (#10125)
* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 8.9% (70 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Spanish)

Currently translated at 4.2% (33 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 2.4% (19 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Russian)

Currently translated at 0.5% (4 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.6% (589 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (French)

Currently translated at 4.1% (32 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Italian)

Currently translated at 3.9% (31 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Czech)

Currently translated at 2.6% (21 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 75.7% (590 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Russian)

Currently translated at 0.7% (6 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (German)

Currently translated at 34.2% (267 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Italian)

Currently translated at 4.4% (35 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Polish)

Currently translated at 0.6% (5 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Russian)

Currently translated at 1.9% (15 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 6.0% (47 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 76.5% (596 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

---------

Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Luca Kröger <l.kroeger01@gmail.com>
Co-authored-by: Héctor Martínez Juste <hectorzin@hotmail.com>
Co-authored-by: Nathan <bonnemainsnathan@gmail.com>
Co-authored-by: Fanfouer <fanfouer@outlook.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Stefan Gries <stefan@gries.nrw>
Co-authored-by: Bouchet Mateo <mateo.bouchet+hosted.weblate.org@mhaz42.fr>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: ZtereoHYPE <me@ztereohype.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Eryk Michalak <gnu.ewm@protonmail.com>
Co-authored-by: Pavel Shamshin <odan@selaz.org>
Co-authored-by: Quan <weiyideai520@hotmail.com>
Co-authored-by: PolairsYHNL-Immich <polarisyhnl@gmail.com>
2024-06-11 13:42:57 +00:00
Jason Rasmussen
735455508c feat(cli): auto-release (#10127) 2024-06-11 08:33:36 -05:00
Alex
eba166a2f1 fix(web): cannot click on explore place (#10121) 2024-06-11 08:32:39 -05:00
Jason Rasmussen
8cf8a2cb35 chore(cli): prepare release (#10124) 2024-06-11 12:16:49 +00:00
Zack Pollard
1767ed2192 chore(web): enable prettier json key sorting recursively (#10120) 2024-06-11 12:52:20 +01:00
Zack Pollard
3c15dae341 docs: fix archive script labels and change to variable to nextVersion (#10119) 2024-06-11 12:37:20 +01:00
Zack Pollard
8568c2e8b9 docs: add archived docs back to v1.100.0 (#10116)
chore: add archived docs back to v1.100.0
2024-06-11 12:36:59 +01:00
Alex
d558ea819a fix(web): cannot perform duplication actions as normal user (#10115)
* fix(web): cannot perform duplication actions as normal user

* use immich dialog
2024-06-11 11:30:42 +00:00
Alex
60701d131e chore(mobile): post release pump (#10114) 2024-06-11 06:26:52 -05:00
Alex
04808f8b5c fix(mobile): warning message not resetting when changing server URL (#10112) 2024-06-11 10:48:49 +00:00
Zack Pollard
8a866297f7 docs: fix archive version url to include v prefix (#10111)
* docs: fix archive version url to include v prefix

* docs: fix archived versions to add missing v to 1.106.1
2024-06-11 05:43:39 -05:00
Alex The Bot
b5991c908e Version v1.106.1 2024-06-11 09:39:23 +00:00
Jason Rasmussen
321c3ccfc6 docs: version switcher (#10091)
* docs: version switcher

* chore: pump script

* chore: fix linting on bash script

* chore: remove 1.106.0 from archived versions

* chore: change version archive script to take next server version not current version

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-06-11 10:33:45 +01:00
Weblate (bot)
05874bd84e chore: update translations (#10096)
* chore:  (German)

Currently translated at 5.9% (46 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Italian)

Currently translated at 3.5% (28 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (German)

Currently translated at 8.7% (68 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 74.9% (584 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.7% (45 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (English (Developer))

Currently translated at 100.0% (779 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/

---------

Co-authored-by: Martin Bosner <martin@bosner.de>
Co-authored-by: Alessandro Saglia <webslate.eskimo0977@bear-d.me>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-11 10:09:58 +01:00
Michel Heusschen
79705dc58d fix(web): language selector for chinese and norwegian (#10107)
* fix(web): language selector for chinese and norwegian

* add unit test

* formatter

* undo name change
2024-06-11 09:07:42 +00:00
aviv926
71a132b0b8 docs: Update the system settings page (#10094)
* updating

* npm run format

* fix \ > /
2024-06-11 03:52:29 -05:00
Alex
d14f23497c fix(server): album update disable no effect (#10090) 2024-06-11 03:51:58 -05:00
Jason Rasmussen
a916df56ee fix: roadmap docs (#10095) 2024-06-10 22:11:50 +00:00
aviv926
73dcb9b452 docs: add detect duplicate assets to roadmap (#10093)
* updating

* uncomment
2024-06-10 21:49:10 +00:00
Alex The Bot
f32c02bd25 Version v1.106.0 2024-06-10 17:50:00 +00:00
Zack Pollard
b16c9405d8 docs: otel metrics port worker split (#10085) 2024-06-10 12:44:10 -05:00
Alex
46df165ef2 feat(mobile): compatibility message warning (#10065)
* feat(mobile): compatibility message warning

* refactor and better signature
2024-06-10 12:43:54 -05:00
Zack Pollard
19e35d8d3f chore(server): remove unused imagemin type dependency (#10084) 2024-06-10 17:08:25 +00:00
Alex
c4c070569f fix(web): mouse-wheel scrolling on detail panel is disabled (#10080) 2024-06-10 12:05:52 -05:00
Jason Rasmussen
7651f70c88 fix(server): asset delete logic (#10077)
* fix(server): asset delete logic

* test: e2e
2024-06-10 13:04:34 -04:00
Alex
4698c39855 chore: remove pr labeler requirement (#10081) 2024-06-10 12:59:19 -04:00
Zack Pollard
2f2aecfb47 fix(server): otel not working due to port conflicts after combining containers (#10078)
fix: otel not working due to port conflicts after combining containers

Fixes #9759
2024-06-10 16:01:04 +00:00
dependabot[bot]
20efd82461 chore(deps): bump docker/build-push-action from 5.3.0 to 5.4.0 (#10069)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.3.0 to 5.4.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.3.0...v5.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-10 14:52:50 +01:00
Zack Pollard
22a0b4d900 chore(web): order json files alphabetically (#10076) 2024-06-10 09:37:21 -04:00
Weblate (bot)
2f25a8a437 chore: update translations (#10075)
chore:  (Vietnamese)

Currently translated at 0.3% (3 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:16:21 +01:00
Weblate (bot)
7a0bc0ea87 chore: update translations (#10074)
chore:  (Vietnamese)

Currently translated at 0.2% (2 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:09:12 +01:00
Weblate (bot)
a564c80193 chore: update translations (#10073)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:06:20 +01:00
Weblate (bot)
f4671617d1 chore: update translations (#10072)
chore:  (Vietnamese)

Currently translated at 0.1% (1 of 779 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

Co-authored-by: Zack Pollard <zack@futo.org>
2024-06-10 14:03:48 +01:00
Zack Pollard
d331da0ced chore(web): fix weblate conflicts (#10071)
* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 29.2% (228 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 51.2% (400 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore:  (German)

Currently translated at 5.7% (45 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/

* chore:  (Dutch)

Currently translated at 5.8% (46 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/

* chore:  (Spanish)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/

* chore:  (Arabic)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/

* chore:  (Catalan)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/

* chore:  (Danish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/

* chore:  (Finnish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/

* chore:  (French)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/

* chore:  (Hebrew)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/

* chore:  (Hindi)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/

* chore:  (Hungarian)

Currently translated at 0.1% (1 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/

* chore:  (Italian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/

* chore:  (Japanese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/

* chore:  (Korean)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/

* chore:  (Lithuanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/

* chore:  (Latvian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/

* chore:  (Mongolian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mn/

* chore:  (Norwegian Bokmål)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/

* chore:  (Polish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/

* chore:  (Portuguese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/

* chore:  (Romanian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/

* chore:  (Russian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/

* chore:  (Slovak)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/

* chore:  (Slovenian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/

* chore:  (Serbian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr/

* chore:  (Swedish)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/

* chore:  (Thai)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/

* chore:  (Ukrainian)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/

* chore:  (Vietnamese)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/

* chore:  (Czech)

Currently translated at 0.0% (0 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/

* chore:  (Chinese (Simplified) (zh_SIMPLIFIED))

Currently translated at 55.3% (432 of 780 strings)

Translation: Immich/immich
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/

* chore(web): enable prettier for json files in web

---------

Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: LLL <326867814@qq.com>
Co-authored-by: jie65535 <jie65535@qq.com>
Co-authored-by: bo0tzz <git@bo0tzz.me>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Manic87 <nicolas@familie-mach.net>
Co-authored-by: Peter Suba <peter.suba@gmail.com>
Co-authored-by: Anonymous <noreply@weblate.org>
2024-06-10 13:59:54 +01:00
aviv926
84da9abcbc docs: Add Email Notifications info (#9930)
* Add Email Notifications info

* remove spaces

* Add ` ` to smtp link

* Small fixes

* PR feedback

* npm run format:fix

* PR feedback

* update docs

* formatting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-06-09 20:07:08 +00:00
Robert Schäfer
48eede59b9 refactor: dedicated icon for permanently delete (#10052)
Motivation
----------
It's a follow up to #10028. I think it would be better user experience if one can tell by the icon what the delete button is about to do.

I hope I caught all the occurences where one can permanently delete assets.

How to test
-----------
1. Visit e.g. `/trash`
2. If you select some assets, the delete button in the top right corner
   looks different.
2024-06-09 14:25:27 -05:00
Fynn Petersen-Frey
972c66d467 fix(server): proper asset sync (#10019)
* fix(server,mobile): proper asset sync

* fix CI issues

* only use id instead of createdAt+id

* remove createdAt index

* fix typo

* cleanup createdAt usage

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-06-09 14:19:28 -05:00
Robert Schäfer
69795a3763 refactor: remove dead code from Makefile (#10061)
Motivation
----------
I guess these make targets should have been deleted in 57136e48fb.

How to test
-----------
1. Nothing really, this removes dead code
2024-06-09 19:18:41 +00:00
Robert Schäfer
9c337223e6 ci: automatically apply PR labels (#10064)
Motivation
----------
For me as a new contributor it is frustrating to submit a PR and it will always fail. Even worse: I have to wait for another contributor with more power to assign the label for me.

This will improve developer experience, as some of the labels can be assigned automatically based on changed files.

How to test
-----------
1. Merge this PR
2. Submit a couple of PRs with changes in the respective directories
3. Labels should be automatically applied
4. "Enforce PR labels" github workflow will re-run when "Pull Request Labeler" completes
2024-06-09 14:18:02 -05:00
233 changed files with 10051 additions and 7614 deletions

23
.github/labeler.yml vendored Normal file
View File

@@ -0,0 +1,23 @@
cli:
- changed-files:
- any-glob-to-any-file: cli/**
documentation:
- changed-files:
- any-glob-to-any-file: docs/**
🖥web:
- changed-files:
- any-glob-to-any-file: web/**
📱mobile:
- changed-files:
- any-glob-to-any-file: mobile/**
🗄server:
- changed-files:
- any-glob-to-any-file: server/**
🧠machine-learning:
- changed-files:
- any-glob-to-any-file: machine-learning/**

View File

@@ -1,16 +1,17 @@
name: CLI Build
on:
workflow_dispatch:
push:
branches: [main]
paths:
- "cli/**"
- ".github/workflows/cli.yml"
- 'cli/**'
- '.github/workflows/cli.yml'
pull_request:
branches: [main]
paths:
- "cli/**"
- ".github/workflows/cli.yml"
- 'cli/**'
- '.github/workflows/cli.yml'
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -32,8 +33,8 @@ jobs:
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v4
with:
node-version: "20.x"
registry-url: "https://registry.npmjs.org"
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
- name: Prepare SDK
run: npm ci --prefix ../open-api/typescript-sdk/
- name: Build SDK
@@ -41,7 +42,7 @@ jobs:
- run: npm ci
- run: npm run build
- run: npm publish
if: ${{ github.event_name == 'workflow_dispatch' }}
if: ${{ github.event_name == 'release' }}
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -83,15 +84,15 @@ jobs:
images: |
name=ghcr.io/${{ github.repository_owner }}/immich-cli
tags: |
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'workflow_dispatch' }}
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
type=raw,value=${{ steps.package-version.outputs.version }},enable=${{ github.event_name == 'release' }}
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v5.4.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name == 'workflow_dispatch' }}
push: ${{ github.event_name == 'release' }}
cache-from: type=gha
cache-to: type=gha,mode=max
tags: ${{ steps.metadata.outputs.tags }}

View File

@@ -115,7 +115,7 @@ jobs:
fi
- name: Build and push image
uses: docker/build-push-action@v5.3.0
uses: docker/build-push-action@v5.4.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}

12
.github/workflows/pr-labeler.yml vendored Normal file
View File

@@ -0,0 +1,12 @@
name: "Pull Request Labeler"
on:
- pull_request_target
jobs:
labeler:
permissions:
contents: read
pull-requests: write
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v5

View File

@@ -1,13 +0,0 @@
name: Enforce PR labels
on:
pull_request:
types: [labeled, unlabeled, opened, edited, synchronize]
jobs:
enforce-label:
name: Enforce label
runs-on: ubuntu-latest
steps:
- if: toJson(github.event.pull_request.labels) == '[]'
run: exit 1

View File

@@ -10,12 +10,6 @@ dev-update:
dev-scale:
docker compose -f ./docker/docker-compose.dev.yml up --build -V --scale immich-server=3 --remove-orphans
stage:
docker compose -f ./docker/docker-compose.staging.yml up --build -V --remove-orphans
pull-stage:
docker compose -f ./docker/docker-compose.staging.yml pull
.PHONY: e2e
e2e:
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans

8
cli/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
"version": "2.2.0",
"version": "2.2.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
"version": "2.2.0",
"version": "2.2.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -47,14 +47,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},

View File

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

View File

@@ -103,7 +103,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m

View File

@@ -61,7 +61,7 @@ services:
ports:
- 5432:5432
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m

View File

@@ -59,7 +59,7 @@ services:
volumes:
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT SUM(checksum_failures) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
test: pg_isready --dbname='${DB_DATABASE_NAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m

View File

@@ -3,10 +3,10 @@ global:
evaluation_interval: 15s
scrape_configs:
- job_name: immich_server
- job_name: immich_api
static_configs:
- targets: ['immich-server:8081']
- job_name: immich_microservices
static_configs:
- targets: ['immich-microservices:8081']
- targets: ['immich-server:8082']

View File

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

View File

@@ -0,0 +1,23 @@
# Email Notifications
Immich supports the option to send notifications via Email for the following events:
- Creating a new user
- Notifying a user when they get added to a shared album
- Informing other users about the addition of new assets to a shared album
## SMTP settings
You can access the settings panel from the web at `Administration -> Settings -> Notification settings`
Under Email, enter the following details to connect with SMTP servers.
You can use the following [guide](/docs/guides/smtp-gmail) to use Gmail's SMTP server.
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />
## User's notifications settings
Users can manage their email notification settings from their account settings page on the web. They can choose to turn email notifications on or off for the following events:
<img src={require('./img/user-notifications-settings.png').default} width="80%" title="User notification settings" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

View File

@@ -10,6 +10,59 @@ Viewing and modifying the system settings is restricted to the Administrator.
You can always return to the default settings by clicking the `Reset to default` button.
:::
## Authentication Settings
Manage password, OAuth, and other authentication settings
### OAuth Authentication
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
### Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
## Image Settings (thumbnails and previews)
- Thumbnails - Used in the main timeline.
- Previews - Used in the asset viewer.
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small - thumbnails (webp) , and Large - previews (jpeg/webp), using these settings you can change the quality for the thumbnails and previews files that are created.
**Thumbnail format**
Allows you to choose the type of format you want for the Thumbnail images, Webp produces smaller files than jpeg, but is slower to encode.
:::tip
You can read in detail about the advantages and disadvantages of using webp over jpeg on [Adobe's website](https://www.adobe.com/creativecloud/file-types/image/raster/webp-file.html)
:::
**Thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Preview format**
Allows you to choose the type of format you want for the Preview images, Webp produces smaller files than jpeg, but is slower to encode.
**Preview resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Image quality from 1-100. Higher is better for quality but produces larger files, this option affects the Preview and Thumbnail images.
**Prefer wide gamut**
Use Display P3 for thumbnails. This better preserves the vibrance of images with wide colorspaces, but images may appear differently on old devices with an old browser version. sRGB images are kept as sRGB to avoid color shifts.
**Prefer embedded preview**
Use embedded previews in RAW photos as the input to image processing when available. This can produce more accurate colors for some images, but the quality of the preview is camera-dependent and the image may have more compression artifacts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Job Settings
Using these settings, you can determine the amount of work that will run concurrently for each task in microservices. Some tasks can be set to higher values on computers with powerful hardware and storage with good I/O capabilities.
@@ -92,17 +145,9 @@ The map can be adjusted via [OpenMapTiles](https://openmaptiles.org/styles/) for
Immich supports [Reverse Geocoding](/docs/features/reverse-geocoding) using data from the [GeoNames](https://www.geonames.org/) geographical database.
## OAuth Authentication
## Notification Settings
Immich supports OAuth Authentication. Read more about this feature and its configuration [here](/docs/administration/oauth).
## Password Authentication
The administrator can choose to disable login with username and password for the entire instance. This means that **no one**, including the system administrator, will be able to log using this method. If [OAuth Authentication](/docs/administration/oauth) is also disabled, no users will be able to login using **any** method. Changing this setting does not affect existing sessions, just new login attempts.
:::tip
You can always use the [Server CLI](/docs/administration/server-commands) to re-enable password login.
:::
SMTP server setup, for user creation notifications, new albums, etc. More information can be found [here](/docs/administration/email-notification)
## Server Settings
@@ -130,27 +175,6 @@ p {
}
```
## Thumbnail Settings
By default Immich creates 3 thumbnails for each asset,
Blurred (thumbhash) , Small (webp) , and Large (jpeg), using these settings you can change the quality for the thumbnail files that are created.
**Small thumbnail resolution**
Used when viewing groups of photos (main timeline, album view, etc.). Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Large thumbnail resolution**
Used when viewing a single photo and for machine learning. Higher resolutions can preserve more detail but take longer to encode, have larger file sizes, and can reduce app responsiveness.
**Quality**
Thumbnail quality from 1-100. Higher is better for quality but produces larger files.
**Prefer wide gamut**
Use display p3 for thumbnails. This better preserves the vibrance of images with wide color spaces, but images may appear differently on old devices with an old browser version. Srgb images are kept as srgb to avoid color shifts.
:::tip
The default resolution for Large thumbnails can be lowered from 1440p (default) to 1080p or 720p to save storage space.
:::
## Trash Settings
In the system administrator's option to set a trash for deleted files, these files will remain in the trash until the deletion date 30 days (default) or as defined by the system administrator.

View File

@@ -13,6 +13,20 @@ Immich supports multiple users, each with their own library.
<UserCreate />
## Send new user email notification
:::note
This option is only available if an SMTP server has been configured in the administrator settings.
:::
Admin can send a welcome email if the Email option is set, you can learn here how to set up the SMTP server in Immich.
<img
src={require('./img/send-user-email-notification.webp').default}
width="40%"
title="Send user email notification"
/>
## Set Storage Quota For User
Admin can specify the storage quota for the user as the instance's admin; once the limit is reached, the user won't be able to upload to the instance anymore.

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 218 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -0,0 +1,20 @@
# SMTP settings using Gmail
This guide walks you through how to get the information you need to set up your Immich instance to send emails using Gmail's SMTP server.
## Create an app password
From your Google account settings
- Add [2-Step Verification](https://support.google.com/accounts/answer/185839) to your Google account (Required)
- [Create an app password](https://myaccount.google.com/apppasswords).
At the end of creating your app passwords, a password will be displayed; save it, it will be used for the password field when setting up the SMTP server in Immich.
<img src={require('./img/google-app-password.webp').default} title="Authorised redirect URIs" />
## Entering the SMTP credential in Immich
Entering your credential in Immich's email notification settings at `Administration -> Settings -> Notification Settings`
<img src={require('./img/email-settings.png').default} width="80%" title="SMTP settings" />

View File

@@ -38,17 +38,19 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.

View File

@@ -94,6 +94,10 @@ const config = {
srcDark: 'img/immich-logo-inline-dark.png',
},
items: [
{
type: 'custom-versionSwitcher',
position: 'right',
},
{
to: '/docs/overview/introduction',
position: 'right',

View File

@@ -0,0 +1,59 @@
import '@docusaurus/theme-classic/lib/theme/Unlisted/index';
import { useWindowSize } from '@docusaurus/theme-common';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import React, { useEffect, useState } from 'react';
export default function VersionSwitcher(): JSX.Element {
const [versions, setVersions] = useState([]);
const [label, setLabel] = useState('Versions');
const windowSize = useWindowSize();
useEffect(() => {
async function getVersions() {
try {
let baseUrl = 'https://immich.app';
if (window.location.origin === 'http://localhost:3005') {
baseUrl = window.location.origin;
}
const response = await fetch(`${baseUrl}/archived-versions.json`);
const archiveVersions = await response.json();
const allVersions = [
{ label: 'Next', url: 'https://main.preview.immich.app' },
{ label: 'Latest', url: 'https://immich.app' },
...archiveVersions,
];
setVersions(allVersions);
const activeVersion = allVersions.find((version) => new URL(version.url).origin === window.location.origin);
if (activeVersion) {
setLabel(activeVersion.label);
}
} catch (error) {
console.error('Failed to fetch versions', error);
}
}
if (versions.length === 0) {
getVersions();
}
}, []);
return (
versions.length > 0 && (
<DropdownNavbarItem
className="navbar__item"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: url,
target: '_self',
}))}
/>
)
);
}

View File

@@ -63,12 +63,14 @@ import {
mdiVectorCombine,
mdiVideo,
mdiWeb,
mdiContentDuplicate,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
'v1.106.0': new Date(2024, 5, 11),
'v1.104.0': new Date(2024, 4, 13),
'v1.103.0': new Date(2024, 3, 29),
'v1.102.0': new Date(2024, 3, 15),
@@ -216,13 +218,19 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
// withRelease({
// icon: mdiVectorCombine,
// title: 'Container consolidation',
// description:
// 'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
// release: 'v1.106.0',
// }),
withRelease({
icon: mdiContentDuplicate,
title: 'Similar image detection',
description: 'Detect duplicate assets that arent exactly identical',
release: 'v1.106.0',
}),
withRelease({
icon: mdiVectorCombine,
title: 'Container consolidation',
description:
'The microservices container can be run as a worker within the server image, allowing us to remove it from the default stack.',
release: 'v1.106.0',
}),
withRelease({
icon: mdiPencil,
iconColor: 'saddlebrown',

View File

@@ -0,0 +1,7 @@
import ComponentTypes from '@theme-original/NavbarItem/ComponentTypes';
import VersionSwitcher from '@site/src/components/version-switcher';
export default {
...ComponentTypes,
'custom-versionSwitcher': VersionSwitcher,
};

58
docs/static/archived-versions.json vendored Normal file
View File

@@ -0,0 +1,58 @@
[
{
"label": "v1.106.3",
"url": "https://v1.106.3.archive.immich.app"
},
{
"label": "v1.106.2",
"url": "https://v1.106.2.archive.immich.app"
},
{
"label": "v1.106.1",
"url": "https://v1.106.1.archive.immich.app"
},
{
"label": "v1.105.1",
"url": "https://v1.105.1.archive.immich.app/"
},
{
"label": "v1.105.0",
"url": "https://v1.105.0.archive.immich.app/"
},
{
"label": "v1.104.0",
"url": "https://v1.104.0.archive.immich.app/"
},
{
"label": "v1.103.1",
"url": "https://v1.103.1.archive.immich.app/"
},
{
"label": "v1.103.0",
"url": "https://v1.103.0.archive.immich.app/"
},
{
"label": "v1.102.3",
"url": "https://v1.102.3.archive.immich.app/"
},
{
"label": "v1.102.2",
"url": "https://v1.102.2.archive.immich.app/"
},
{
"label": "v1.102.1",
"url": "https://v1.102.1.archive.immich.app/"
},
{
"label": "v1.102.0",
"url": "https://v1.102.0.archive.immich.app/"
},
{
"label": "v1.101.0",
"url": "https://v1.101.0.archive.immich.app/"
},
{
"label": "v1.100.0",
"url": "https://v1.100.0.archive.immich.app/"
}
]

10
e2e/package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.106.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
"version": "1.105.1",
"version": "1.106.3",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@immich/cli": "file:../cli",
@@ -39,7 +39,7 @@
},
"../cli": {
"name": "@immich/cli",
"version": "2.2.0",
"version": "2.2.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -81,14 +81,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
"version": "1.105.1",
"version": "1.106.3",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},

View File

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

View File

@@ -1148,4 +1148,29 @@ describe('/asset', () => {
expect(video.checksum).toStrictEqual(checksum);
});
});
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['invalid', 'INVALID'],
});
expect(response.existingIds).toHaveLength(0);
});
it('returns the IDs of existing assets', async () => {
await utils.createAsset(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetId: 'test-asset-0',
});
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['test-asset-0'],
});
expect(response.existingIds).toEqual(['test-asset-0']);
});
});
});

View File

@@ -1,4 +1,11 @@
import { LibraryResponseDto, LoginResponseDto, ScanLibraryDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import {
LibraryResponseDto,
LoginResponseDto,
ScanLibraryDto,
getAllLibraries,
removeOfflineFiles,
scanLibrary,
} from '@immich/sdk';
import { cpSync, existsSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -384,6 +391,51 @@ describe('/libraries', () => {
);
});
it('should not try to delete offline files', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline1`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(initialAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
isOffline: true,
});
expect(offlineAssets).toEqual({
count: 1,
total: 1,
facets: [],
items: [expect.objectContaining({ originalFileName: 'assetA.png' })],
nextPage: null,
});
utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
await removeOfflineFiles({ id: library.id }, { headers: asBearerAuth(admin.accessToken) });
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
});
it('should scan new files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
@@ -507,10 +559,10 @@ describe('/libraries', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
importPaths: [`${testAssetDirInternal}/temp/offline2`],
});
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -518,9 +570,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(3);
expect(initialAssets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -541,7 +593,7 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2);
expect(assets.count).toBe(0);
});
it('should not remove online files', async () => {

View File

@@ -3,6 +3,7 @@ import {
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetResponseDto,
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
MetadataSearchDto,
@@ -10,6 +11,7 @@ import {
SharedLinkCreateDto,
UserAdminCreateDto,
ValidateLibraryDto,
checkExistingAssets,
createAlbum,
createApiKey,
createLibrary,
@@ -374,6 +376,9 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
metadataSearch: async (accessToken: string, dto: MetadataSearchDto) => {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},

View File

@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.23.2"
version = "0.23.3"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.23.2-py3-none-any.whl", hash = "sha256:48727a16e704d409c4bb5913613308499664f22a99743435dc3a13b23c485827"},
{file = "huggingface_hub-0.23.2.tar.gz", hash = "sha256:f6829b62d5fdecb452a76fdbec620cba4c1573655a8d710c1df71735fd9edbd2"},
{file = "huggingface_hub-0.23.3-py3-none-any.whl", hash = "sha256:22222c41223f1b7c209ae5511d2d82907325a0e3cdbce5f66949d43c598ff3bc"},
{file = "huggingface_hub-0.23.3.tar.gz", hash = "sha256:1a1118a0b3dea3bab6c325d71be16f5ffe441d32f3ac7c348d6875911b694b5b"},
]
[package.dependencies]
@@ -2054,18 +2054,18 @@ sympy = "*"
[[package]]
name = "opencv-python-headless"
version = "4.9.0.80"
version = "4.10.0.82"
description = "Wrapper package for OpenCV python bindings."
optional = false
python-versions = ">=3.6"
files = [
{file = "opencv-python-headless-4.9.0.80.tar.gz", hash = "sha256:71a4cd8cf7c37122901d8e81295db7fb188730e33a0e40039a4e59c1030b0958"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_10_16_x86_64.whl", hash = "sha256:2ea8a2edc4db87841991b2fbab55fc07b97ecb602e0f47d5d485bd75cee17c1a"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e0ee54e27be493e8f7850847edae3128e18b540dac1d7b2e4001b8944e11e1c6"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57ce2865e8fec431c6f97a81e9faaf23fa5be61011d0a75ccf47a3c0d65fa73d"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:976656362d68d9f40a5c66f83901430538002465f7db59142784f3893918f3df"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win32.whl", hash = "sha256:11e3849d83e6651d4e7699aadda9ec7ed7c38957cbbcb99db074f2a2d2de9670"},
{file = "opencv_python_headless-4.9.0.80-cp37-abi3-win_amd64.whl", hash = "sha256:a8056c2cb37cd65dfcdf4153ca16f7362afcf3a50d600d6bb69c660fc61ee29c"},
{file = "opencv-python-headless-4.10.0.82.tar.gz", hash = "sha256:de9e742c1b9540816fbd115b0b03841d41ed0c65566b0d7a5371f98b131b7e6d"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:a09ed50ba21cc5bf5d436cb0e784ad09c692d6b1d1454252772f6c8f2c7b4088"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:977a5fd21e1fe0d3d2134887db4441f8725abeae95150126302f31fcd9f548fa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4ec6755838b0be12510bfc9ffb014779c612418f11f4f7e6f505c36124a3aa"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10a37fa5276967ecf6eb297295b16b28b7a2eb3b568ca0ee469fb1a5954de298"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win32.whl", hash = "sha256:94736e9b322d13db4768fd35588ad5e8995e78e207263076bfbee18aac835ad5"},
{file = "opencv_python_headless-4.10.0.82-cp37-abi3-win_amd64.whl", hash = "sha256:c1822fa23d1641c0249ed5eb906f4c385f7959ff1bd601a776d56b0c18914af4"},
]
[package.dependencies]
@@ -2438,13 +2438,13 @@ files = [
[[package]]
name = "pytest"
version = "8.2.1"
version = "8.2.2"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
{file = "pytest-8.2.2-py3-none-any.whl", hash = "sha256:c434598117762e2bd304e526244f67bf66bbd7b5d6cf22138be51ff661980343"},
{file = "pytest-8.2.2.tar.gz", hash = "sha256:de4bb8104e201939ccdc688b27a89a7be2079b22e2bd2b07f806b6ba71117977"},
]
[package.dependencies]
@@ -2799,28 +2799,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.7"
version = "0.4.8"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"},
{file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"},
{file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"},
{file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"},
{file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"},
{file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"},
{file = "ruff-0.4.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7663a6d78f6adb0eab270fa9cf1ff2d28618ca3a652b60f2a234d92b9ec89066"},
{file = "ruff-0.4.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eeceb78da8afb6de0ddada93112869852d04f1cd0f6b80fe464fd4e35c330913"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aad360893e92486662ef3be0a339c5ca3c1b109e0134fcd37d534d4be9fb8de3"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:284c2e3f3396fb05f5f803c9fffb53ebbe09a3ebe7dda2929ed8d73ded736deb"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7354f921e3fbe04d2a62d46707e569f9315e1a613307f7311a935743c51a764"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:72584676164e15a68a15778fd1b17c28a519e7a0622161eb2debdcdabdc71883"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9678d5c9b43315f323af2233a04d747409d1e3aa6789620083a82d1066a35199"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704977a658131651a22b5ebeb28b717ef42ac6ee3b11e91dc87b633b5d83142b"},
{file = "ruff-0.4.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d05f8d6f0c3cce5026cecd83b7a143dcad503045857bc49662f736437380ad45"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6ea874950daca5697309d976c9afba830d3bf0ed66887481d6bca1673fc5b66a"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:fc95aac2943ddf360376be9aa3107c8cf9640083940a8c5bd824be692d2216dc"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:384154a1c3f4bf537bac69f33720957ee49ac8d484bfc91720cc94172026ceed"},
{file = "ruff-0.4.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e9d5ce97cacc99878aa0d084c626a15cd21e6b3d53fd6f9112b7fc485918e1fa"},
{file = "ruff-0.4.8-py3-none-win32.whl", hash = "sha256:6d795d7639212c2dfd01991259460101c22aabf420d9b943f153ab9d9706e6a9"},
{file = "ruff-0.4.8-py3-none-win_amd64.whl", hash = "sha256:e14a3a095d07560a9d6769a72f781d73259655919d9b396c650fc98a8157555d"},
{file = "ruff-0.4.8-py3-none-win_arm64.whl", hash = "sha256:14019a06dbe29b608f6b7cbcec300e3170a8d86efaddb7b23405cb7f7dcaf780"},
{file = "ruff-0.4.8.tar.gz", hash = "sha256:16d717b1d57b2e2fd68bd0bf80fb43931b79d05a7131aa477d66fc40fbd86268"},
]
[[package]]

View File

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

17
misc/release/archive-version.js Executable file
View File

@@ -0,0 +1,17 @@
#! /usr/bin/env node
const { readFileSync, writeFileSync } = require('node:fs');
const nextVersion = process.argv[2];
if (!nextVersion) {
console.log('Usage: archive-version.js <version>');
process.exit(1);
}
const filename = './docs/static/archived-versions.json';
const oldVersions = JSON.parse(readFileSync(filename));
const newVersions = [
{ label: `v${nextVersion}`, url: `https://v${nextVersion}.archive.immich.app` },
...oldVersions,
];
writeFileSync(filename, JSON.stringify(newVersions, null, 2) + '\n');

View File

@@ -66,10 +66,12 @@ if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
npm --prefix server run build
make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP"
npm --prefix e2e version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
# TODO use $SERVER_PUMP once we pass 2.2.x
npm --prefix cli version patch
npm --prefix cli i --package-lock-only
npm --prefix web version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
npm --prefix e2e version "$SERVER_PUMP"
npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP"
fi
@@ -83,4 +85,6 @@ sed -i "s/version_number: \"$CURRENT_SERVER\"$/version_number: \"$NEXT_SERVER\"/
sed -i "s/\"android\.injected\.version\.code\" => $CURRENT_MOBILE,/\"android\.injected\.version\.code\" => $NEXT_MOBILE,/" mobile/android/fastlane/Fastfile
sed -i "s/^version: $CURRENT_SERVER+$CURRENT_MOBILE$/version: $NEXT_SERVER+$NEXT_MOBILE/" mobile/pubspec.yaml
./misc/release/archive-version.js "$NEXT_SERVER"
echo "IMMICH_VERSION=v$NEXT_SERVER" >>"$GITHUB_ENV"

View File

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

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000374">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000381">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="84.292464">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="52.832426">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="33.336934">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="27.616558">
</testcase>

View File

@@ -383,7 +383,7 @@
CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 157;
CURRENT_PROJECT_VERSION = 160;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -525,7 +525,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 157;
CURRENT_PROJECT_VERSION = 160;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;
@@ -553,7 +553,7 @@
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 157;
CURRENT_PROJECT_VERSION = 160;
DEVELOPMENT_TEAM = 2F67MQ8R79;
ENABLE_BITCODE = NO;
INFOPLIST_FILE = Runner/Info.plist;

View File

@@ -58,11 +58,11 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.105.0</string>
<string>1.106.3</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>157</string>
<string>160</string>
<key>FLTEnableImpeller</key>
<true />
<key>ITSAppUsesNonExemptEncryption</key>

View File

@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Beta"
lane :beta do
increment_version_number(
version_number: "1.105.1"
version_number: "1.106.3"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,

View File

@@ -5,32 +5,32 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.020864">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000491">
</testcase>
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="0.917777">
<testcase classname="fastlane.lanes" name="1: increment_version_number" time="39.414297">
</testcase>
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="5.283943">
<testcase classname="fastlane.lanes" name="2: latest_testflight_build_number" time="32.521647">
</testcase>
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.944748">
<testcase classname="fastlane.lanes" name="3: increment_build_number" time="0.511733">
</testcase>
<testcase classname="fastlane.lanes" name="4: build_app" time="215.971639">
<testcase classname="fastlane.lanes" name="4: build_app" time="202.628277">
</testcase>
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="76.674601">
<testcase classname="fastlane.lanes" name="5: upload_to_testflight" time="73.861852">
</testcase>

View File

@@ -75,9 +75,7 @@ class VideoViewerPage extends HookConsumerWidget {
// Also sets the error if there is an error in the playback
void updateVideoPlayback() {
final videoPlayback = VideoPlaybackValue.fromController(controller);
if (!loopVideo) {
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
}
ref.read(videoPlaybackValueProvider.notifier).value = videoPlayback;
final state = videoPlayback.state;
// Enable the WakeLock while the video is playing
@@ -110,7 +108,9 @@ class VideoViewerPage extends HookConsumerWidget {
}
// Subscribes to listener
controller.addListener(updateVideoPlayback);
Future.microtask(() {
controller.addListener(updateVideoPlayback);
});
return () {
// Removes listener when we dispose
controller.removeListener(updateVideoPlayback);

View File

@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/forms/login_form.dart';
import 'package:immich_mobile/widgets/forms/login/login_form.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:package_info_plus/package_info_plus.dart';

View File

@@ -50,8 +50,19 @@ class AssetNotifier extends StateNotifier<bool> {
await clearAssetsAndAlbums(_db);
log.info("Manual refresh requested, cleared assets and albums from db");
}
final bool changedUsers = await _userService.refreshUsers();
final assetCount = await _db.assets.count();
if (assetCount == 0) {
debugPrint("First sync, refreshing all assets");
await _assetService.refreshRemoteAssets(firstSync: true);
debugPrint("First sync, DONE refreshing all assets");
}
debugPrint("First sync, CONTINUE refreshing all assets");
final bool newRemote = await _assetService.refreshRemoteAssets();
final bool newLocal = await _albumService.refreshDeviceAlbums();
debugPrint(
"changedUsers: $changedUsers, newRemote: $newRemote, newLocal: $newLocal",

View File

@@ -31,6 +31,9 @@ Future<VideoPlayerController> videoPlayerController(
controller = VideoPlayerController.networkUrl(
url,
httpHeaders: {"x-immich-user-token": accessToken},
videoPlayerOptions: asset.livePhotoVideoId != null
? VideoPlayerOptions(mixWithOthers: true)
: VideoPlayerOptions(mixWithOthers: false),
);
}

View File

@@ -7,7 +7,7 @@ part of 'video_player_controller_provider.dart';
// **************************************************************************
String _$videoPlayerControllerHash() =>
r'40b31f7b1a73fab84c311b0f06bedf5322143cd9';
r'642469a44287188a7c301f5cad3df3e23c84d85c';
/// Copied from Dart SDK
class _SystemHash {

View File

@@ -93,4 +93,18 @@ class VideoPlayerControls extends StateNotifier<VideoPlaybackControls> {
pause: !state.pause,
);
}
void restart() {
state = VideoPlaybackControls(
position: 0,
mute: state.mute,
pause: true,
);
state = VideoPlaybackControls(
position: 0,
mute: state.mute,
pause: false,
);
}
}

View File

@@ -6,7 +6,7 @@ part of 'map_state.provider.dart';
// RiverpodGenerator
// **************************************************************************
String _$mapStateNotifierHash() => r'87a8623f726d438d115d5a15609c71372726ee2f';
String _$mapStateNotifierHash() => r'31fafe17aa85c48379a22ed3db3cc94af59ce5b8';
/// See also [MapStateNotifier].
@ProviderFor(MapStateNotifier)

View File

@@ -43,7 +43,7 @@ class AssetService {
/// Checks the server for updated assets and updates the local database if
/// required. Returns `true` if there were any changes.
Future<bool> refreshRemoteAssets() async {
Future<bool> refreshRemoteAssets({bool firstSync = false}) async {
final syncedUserIds = await _db.eTags.where().idProperty().findAll();
final List<User> syncedUsers = syncedUserIds.isEmpty
? []
@@ -57,6 +57,7 @@ class AssetService {
getChangedAssets: _getRemoteAssetChanges,
loadAssets: _getRemoteAssets,
refreshUsers: _userService.getUsersFromServer,
firstSync: firstSync,
);
debugPrint("refreshRemoteAssets full took ${sw.elapsedMilliseconds}ms");
return changes;
@@ -97,11 +98,14 @@ class AssetService {
}
/// Returns `null` if the server state did not change, else list of assets
Future<List<Asset>?> _getRemoteAssets(User user, DateTime until) async {
const int chunkSize = 10000;
Future<List<Asset>?> _getRemoteAssets(
User user,
DateTime until,
bool firstSync,
) async {
int chunkSize = firstSync ? 1000 : 10000;
try {
final List<Asset> allAssets = [];
DateTime? lastCreationDate;
String? lastId;
// will break on error or once all assets are loaded
while (true) {
@@ -109,16 +113,23 @@ class AssetService {
limit: chunkSize,
updatedUntil: until,
lastId: lastId,
lastCreationDate: lastCreationDate,
userId: user.id,
);
log.fine("Requesting $chunkSize assets from $lastId");
final List<AssetResponseDto>? assets =
await _apiService.syncApi.getFullSyncForUser(dto);
if (assets == null) return null;
log.fine(
"Received ${assets.length} assets from ${assets.firstOrNull?.id} to ${assets.lastOrNull?.id}",
);
allAssets.addAll(assets.map(Asset.remote));
if (assets.isEmpty) break;
lastCreationDate = assets.last.fileCreatedAt;
if (assets.length != chunkSize) break;
lastId = assets.last.id;
if (firstSync) {
// first sync only loads the first chunk
break;
}
}
return allAssets;
} catch (error, stack) {

View File

@@ -46,14 +46,18 @@ class SyncService {
List<User> users,
DateTime since,
) getChangedAssets,
required FutureOr<List<Asset>?> Function(User user, DateTime until)
loadAssets,
required FutureOr<List<Asset>?> Function(
User user,
DateTime until,
bool firstSync,
) loadAssets,
required FutureOr<List<User>?> Function() refreshUsers,
required bool firstSync,
}) =>
_lock.run(
() async =>
await _syncRemoteAssetChanges(users, getChangedAssets) ??
await _syncRemoteAssetsFull(refreshUsers, loadAssets),
await _syncRemoteAssetsFull(refreshUsers, loadAssets, firstSync),
);
/// Syncs remote albums to the database
@@ -212,7 +216,9 @@ class SyncService {
/// Syncs assets by loading and comparing all assets from the server.
Future<bool> _syncRemoteAssetsFull(
FutureOr<List<User>?> Function() refreshUsers,
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
loadAssets,
bool firstSync,
) async {
final serverUsers = await refreshUsers();
if (serverUsers == null) {
@@ -228,17 +234,19 @@ class SyncService {
.findAll();
bool changes = false;
for (User u in users) {
changes |= await _syncRemoteAssetsForUser(u, loadAssets);
changes |= await _syncRemoteAssetsForUser(u, loadAssets, firstSync);
}
return changes;
}
Future<bool> _syncRemoteAssetsForUser(
User user,
FutureOr<List<Asset>?> Function(User user, DateTime until) loadAssets,
FutureOr<List<Asset>?> Function(User user, DateTime until, bool firstSync)
loadAssets,
bool firstSync,
) async {
final DateTime now = DateTime.now().toUtc();
final List<Asset>? remote = await loadAssets(user, now);
final List<Asset>? remote = await loadAssets(user, now, firstSync);
if (remote == null) {
return false;
}

View File

@@ -0,0 +1,17 @@
String? getVersionCompatibilityMessage(
int appMajor,
int appMinor,
int serverMajor,
int serverMinor,
) {
if (serverMajor != appMajor) {
return 'Your app major version is not compatible with the server!';
}
// Add latest compat info up top
if (serverMinor < 106 && appMinor >= 106) {
return 'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login';
}
return null;
}

View File

@@ -64,6 +64,8 @@ class CustomVideoPlayerControls extends HookConsumerWidget {
final state = ref.read(videoPlaybackValueProvider).state;
if (state == VideoPlaybackState.playing) {
ref.read(videoPlayerControlsProvider.notifier).pause();
} else if (state == VideoPlaybackState.completed) {
ref.read(videoPlayerControlsProvider.notifier).restart();
} else {
ref.read(videoPlayerControlsProvider.notifier).play();
}

View File

@@ -0,0 +1,49 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}

View File

@@ -0,0 +1,21 @@
import 'package:flutter/material.dart';
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

View File

@@ -0,0 +1,27 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}

View File

@@ -15,11 +15,19 @@ import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/authentication.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
import 'package:immich_mobile/widgets/common/immich_logo.dart';
import 'package:immich_mobile/widgets/common/immich_title_text.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/forms/login/email_input.dart';
import 'package:immich_mobile/widgets/forms/login/loading_icon.dart';
import 'package:immich_mobile/widgets/forms/login/login_button.dart';
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
import 'package:openapi/api.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget {
@@ -45,9 +53,31 @@ class LoginForm extends HookConsumerWidget {
final logoAnimationController = useAnimationController(
duration: const Duration(seconds: 60),
)..repeat();
final serverInfo = ref.watch(serverInfoProvider);
final warningMessage = useState<String?>(null);
final ValueNotifier<String?> serverEndpoint = useState<String?>(null);
checkVersionMismatch() async {
try {
final packageInfo = await PackageInfo.fromPlatform();
final appVersion = packageInfo.version;
final appMajorVersion = int.parse(appVersion.split('.')[0]);
final appMinorVersion = int.parse(appVersion.split('.')[1]);
final serverMajorVersion = serverInfo.serverVersion.major;
final serverMinorVersion = serverInfo.serverVersion.minor;
warningMessage.value = getVersionCompatibilityMessage(
appMajorVersion,
appMinorVersion,
serverMajorVersion,
serverMinorVersion,
);
} catch (error) {
warningMessage.value = 'Error checking version compatibility';
}
}
/// Fetch the server login credential and enables oAuth login if necessary
/// Returns true if successful, false otherwise
Future<bool> getServerLoginCredential() async {
@@ -308,11 +338,40 @@ class LoginForm extends HookConsumerWidget {
);
}
buildVersionCompatWarning() {
checkVersionMismatch();
if (warningMessage.value == null) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(bottom: 8.0),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color:
context.isDarkTheme ? Colors.red.shade700 : Colors.red.shade100,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color:
context.isDarkTheme ? Colors.red.shade900 : Colors.red[200]!,
),
),
child: Text(
warningMessage.value!,
textAlign: TextAlign.center,
),
),
);
}
buildLogin() {
return AutofillGroup(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
buildVersionCompatWarning(),
Text(
sanitizeUrl(serverEndpointController.text),
style: context.textTheme.displaySmall,
@@ -416,7 +475,6 @@ class LoginForm extends HookConsumerWidget {
),
],
),
const SizedBox(height: 18),
// Note: This used to have an AnimatedSwitcher, but was removed
// because of https://github.com/flutter/flutter/issues/120874
@@ -430,218 +488,3 @@ class LoginForm extends HookConsumerWidget {
);
}
}
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
);
}
}
class EmailInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const EmailInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
String? _validateInput(String? email) {
if (email == null || email == '') return null;
if (email.endsWith(' ')) return 'login_form_err_trailing_whitespace'.tr();
if (email.startsWith(' ')) return 'login_form_err_leading_whitespace'.tr();
if (email.contains(' ') || !email.contains('@')) {
return 'login_form_err_invalid_email'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return TextFormField(
autofocus: true,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_email'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_email_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
autofillHints: const [AutofillHints.email],
keyboardType: TextInputType.emailAddress,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.next,
);
}
}
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}
class LoginButton extends ConsumerWidget {
final Function() onPressed;
const LoginButton({
super.key,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.login_rounded),
label: const Text(
"login_form_button_text",
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
).tr(),
);
}
}
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}
class LoadingIcon extends StatelessWidget {
const LoadingIcon({super.key});
@override
Widget build(BuildContext context) {
return const Padding(
padding: EdgeInsets.only(top: 18.0),
child: SizedBox(
width: 24,
height: 24,
child: FittedBox(
child: CircularProgressIndicator(
strokeWidth: 2,
),
),
),
);
}
}

View File

@@ -0,0 +1,34 @@
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
class OAuthLoginButton extends ConsumerWidget {
final TextEditingController serverEndpointController;
final ValueNotifier<bool> isLoading;
final String buttonLabel;
final Function() onPressed;
const OAuthLoginButton({
super.key,
required this.serverEndpointController,
required this.isLoading,
required this.buttonLabel,
required this.onPressed,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: context.primaryColor.withAlpha(230),
padding: const EdgeInsets.symmetric(vertical: 12),
),
onPressed: onPressed,
icon: const Icon(Icons.pin_rounded),
label: Text(
buttonLabel,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
);
}
}

View File

@@ -0,0 +1,49 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
class PasswordInput extends HookConsumerWidget {
final TextEditingController controller;
final FocusNode? focusNode;
final Function()? onSubmit;
const PasswordInput({
super.key,
required this.controller,
this.focusNode,
this.onSubmit,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final isPasswordVisible = useState<bool>(false);
return TextFormField(
obscureText: !isPasswordVisible.value,
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_label_password'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_password_hint'.tr(),
hintStyle: const TextStyle(
fontWeight: FontWeight.normal,
fontSize: 14,
),
suffixIcon: IconButton(
onPressed: () => isPasswordVisible.value = !isPasswordVisible.value,
icon: Icon(
isPasswordVisible.value
? Icons.visibility_off_sharp
: Icons.visibility_sharp,
),
),
),
autofillHints: const [AutofillHints.password],
keyboardType: TextInputType.text,
onFieldSubmitted: (_) => onSubmit?.call(),
focusNode: focusNode,
textInputAction: TextInputAction.go,
);
}
}

View File

@@ -0,0 +1,54 @@
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:immich_mobile/utils/url_helper.dart';
class ServerEndpointInput extends StatelessWidget {
final TextEditingController controller;
final FocusNode focusNode;
final Function()? onSubmit;
const ServerEndpointInput({
super.key,
required this.controller,
required this.focusNode,
this.onSubmit,
});
String? _validateInput(String? url) {
if (url == null || url.isEmpty) return null;
final parsedUrl = Uri.tryParse(sanitizeUrl(url));
if (parsedUrl == null ||
!parsedUrl.isAbsolute ||
!parsedUrl.scheme.startsWith("http") ||
parsedUrl.host.isEmpty) {
return 'login_form_err_invalid_url'.tr();
}
return null;
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(top: 16.0),
child: TextFormField(
controller: controller,
decoration: InputDecoration(
labelText: 'login_form_endpoint_url'.tr(),
border: const OutlineInputBorder(),
hintText: 'login_form_endpoint_hint'.tr(),
errorMaxLines: 4,
),
validator: _validateInput,
autovalidateMode: AutovalidateMode.always,
focusNode: focusNode,
autofillHints: const [AutofillHints.url],
keyboardType: TextInputType.url,
autocorrect: false,
onFieldSubmitted: (_) => onSubmit?.call(),
textInputAction: TextInputAction.go,
),
);
}
}

View File

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

View File

@@ -13,21 +13,12 @@ part of openapi.api;
class AssetFullSyncDto {
/// Returns a new [AssetFullSyncDto] instance.
AssetFullSyncDto({
this.lastCreationDate,
this.lastId,
required this.limit,
required this.updatedUntil,
this.userId,
});
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
/// source code must fall back to having a nullable type.
/// Consider adding a "default:" property in the specification file to hide this note.
///
DateTime? lastCreationDate;
///
/// Please note: This property should have been non-nullable! Since the specification file
/// does not include a default value (using the "default:" property), however, the generated
@@ -51,7 +42,6 @@ class AssetFullSyncDto {
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFullSyncDto &&
other.lastCreationDate == lastCreationDate &&
other.lastId == lastId &&
other.limit == limit &&
other.updatedUntil == updatedUntil &&
@@ -60,22 +50,16 @@ class AssetFullSyncDto {
@override
int get hashCode =>
// ignore: unnecessary_parenthesis
(lastCreationDate == null ? 0 : lastCreationDate!.hashCode) +
(lastId == null ? 0 : lastId!.hashCode) +
(limit.hashCode) +
(updatedUntil.hashCode) +
(userId == null ? 0 : userId!.hashCode);
@override
String toString() => 'AssetFullSyncDto[lastCreationDate=$lastCreationDate, lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
String toString() => 'AssetFullSyncDto[lastId=$lastId, limit=$limit, updatedUntil=$updatedUntil, userId=$userId]';
Map<String, dynamic> toJson() {
final json = <String, dynamic>{};
if (this.lastCreationDate != null) {
json[r'lastCreationDate'] = this.lastCreationDate!.toUtc().toIso8601String();
} else {
// json[r'lastCreationDate'] = null;
}
if (this.lastId != null) {
json[r'lastId'] = this.lastId;
} else {
@@ -99,7 +83,6 @@ class AssetFullSyncDto {
final json = value.cast<String, dynamic>();
return AssetFullSyncDto(
lastCreationDate: mapDateTime(json, r'lastCreationDate', r''),
lastId: mapValueOfType<String>(json, r'lastId'),
limit: mapValueOfType<int>(json, r'limit')!,
updatedUntil: mapDateTime(json, r'updatedUntil', r'')!,

View File

@@ -53,7 +53,7 @@ class DuplicateDetectionConfig {
return DuplicateDetectionConfig(
enabled: mapValueOfType<bool>(json, r'enabled')!,
maxDistance: mapValueOfType<double>(json, r'maxDistance')!,
maxDistance: (mapValueOfType<num>(json, r'maxDistance')!).toDouble(),
);
}
return null;

View File

@@ -74,9 +74,9 @@ class FacialRecognitionConfig {
return FacialRecognitionConfig(
enabled: mapValueOfType<bool>(json, r'enabled')!,
maxDistance: mapValueOfType<double>(json, r'maxDistance')!,
maxDistance: (mapValueOfType<num>(json, r'maxDistance')!).toDouble(),
minFaces: mapValueOfType<int>(json, r'minFaces')!,
minScore: mapValueOfType<double>(json, r'minScore')!,
minScore: (mapValueOfType<num>(json, r'minScore')!).toDouble(),
modelName: mapValueOfType<String>(json, r'modelName')!,
);
}

View File

@@ -84,7 +84,7 @@ class ServerStorageResponseDto {
diskAvailableRaw: mapValueOfType<int>(json, r'diskAvailableRaw')!,
diskSize: mapValueOfType<String>(json, r'diskSize')!,
diskSizeRaw: mapValueOfType<int>(json, r'diskSizeRaw')!,
diskUsagePercentage: mapValueOfType<double>(json, r'diskUsagePercentage')!,
diskUsagePercentage: (mapValueOfType<num>(json, r'diskUsagePercentage')!).toDouble(),
diskUse: mapValueOfType<String>(json, r'diskUse')!,
diskUseRaw: mapValueOfType<int>(json, r'diskUseRaw')!,
);

View File

@@ -2,7 +2,7 @@ name: immich_mobile
description: Immich - selfhosted backup media file on mobile phone
publish_to: 'none'
version: 1.105.1+140
version: 1.106.3+143
environment:
sdk: '>=3.3.0 <4.0.0'

View File

@@ -78,8 +78,9 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c1, isFalse);
expect(db.assets.countSync(), 5);
@@ -99,8 +100,9 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c1, isTrue);
expect(db.assets.countSync(), 7);
@@ -120,16 +122,18 @@ void main() {
final bool c1 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c1, isTrue);
expect(db.assets.countSync(), 8);
final bool c2 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c2, isFalse);
expect(db.assets.countSync(), 8);
@@ -137,8 +141,9 @@ void main() {
final bool c3 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c3, isTrue);
expect(db.assets.countSync(), 7);
@@ -147,8 +152,9 @@ void main() {
final bool c4 = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: _failDiff,
loadAssets: (u, d) => remoteAssets,
loadAssets: (u, d, firstSync) => remoteAssets,
refreshUsers: () => [owner],
firstSync: false,
);
expect(c4, isTrue);
expect(db.assets.countSync(), 9);
@@ -166,8 +172,9 @@ void main() {
final bool c = await s.syncRemoteAssetsToDb(
users: [owner],
getChangedAssets: (user, since) async => (toUpsert, toDelete),
loadAssets: (user, date) => throw Exception(),
loadAssets: (user, date, firstSync) => throw Exception(),
refreshUsers: () => throw Exception(),
firstSync: false,
);
expect(c, isTrue);
expect(db.assets.countSync(), 6);

View File

@@ -0,0 +1,35 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:immich_mobile/utils/version_compatibility.dart';
void main() {
test('getVersionCompatibilityMessage', () {
String? result;
result = getVersionCompatibilityMessage(1, 0, 2, 0);
expect(
result,
'Your app major version is not compatible with the server!',
);
result = getVersionCompatibilityMessage(1, 106, 1, 105);
expect(
result,
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
);
result = getVersionCompatibilityMessage(1, 107, 1, 105);
expect(
result,
'Your app minor version is not compatible with the server! Please update your server to version v1.106.0 or newer to login',
);
result = getVersionCompatibilityMessage(1, 106, 1, 106);
expect(result, null);
result = getVersionCompatibilityMessage(1, 107, 1, 106);
expect(result, null);
result = getVersionCompatibilityMessage(1, 107, 1, 108);
expect(result, null);
});
}

View File

@@ -6735,7 +6735,7 @@
"info": {
"title": "Immich",
"description": "Immich API",
"version": "1.105.1",
"version": "1.106.3",
"contact": {}
},
"tags": [],
@@ -7432,10 +7432,6 @@
},
"AssetFullSyncDto": {
"properties": {
"lastCreationDate": {
"format": "date-time",
"type": "string"
},
"lastId": {
"format": "uuid",
"type": "string"
@@ -8150,7 +8146,7 @@
"type": "boolean"
},
"maxDistance": {
"format": "float",
"format": "double",
"maximum": 0.1,
"minimum": 0.001,
"type": "number"
@@ -8351,7 +8347,7 @@
"type": "boolean"
},
"maxDistance": {
"format": "float",
"format": "double",
"maximum": 2,
"minimum": 0,
"type": "number"
@@ -8361,7 +8357,7 @@
"type": "integer"
},
"minScore": {
"format": "float",
"format": "double",
"maximum": 1,
"minimum": 0,
"type": "number"
@@ -9801,7 +9797,7 @@
"type": "integer"
},
"diskUsagePercentage": {
"format": "float",
"format": "double",
"type": "number"
},
"diskUse": {

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
/**
* Immich
* 1.105.1
* 1.106.3
* DO NOT MODIFY - This file has been generated using oazapfts.
* See https://www.npmjs.com/package/oazapfts
*/
@@ -904,7 +904,6 @@ export type AssetDeltaSyncResponseDto = {
upserted: AssetResponseDto[];
};
export type AssetFullSyncDto = {
lastCreationDate?: string;
lastId?: string;
limit: number;
updatedUntil: string;

View File

@@ -69,7 +69,7 @@
"schedule": "on tuesday"
}
],
"ignorePaths": ["mobile/openapi/pubspec.yaml"],
"ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"],
"ignoreDeps": ["http", "intl"],
"labels": ["dependencies", "renovate"]
}

View File

@@ -1,5 +1,5 @@
# dev build
FROM ghcr.io/immich-app/base-server-dev:20240604@sha256:bb31fafb1e8fcb4338c2f7a8f424da3d9c5cf6dd6bdb266c54477c795dd07819 as dev
FROM ghcr.io/immich-app/base-server-dev:20240611@sha256:2047ec0f857a800675379c65404dfdf4ac02ab7684c759916e3256d3d9566027 as dev
RUN apt-get install --no-install-recommends -yqq tini
WORKDIR /usr/src/app
@@ -41,7 +41,7 @@ RUN npm run build
# prod build
FROM ghcr.io/immich-app/base-server-prod:20240604@sha256:481ea3ee56fb0e130804fec25c124d28477f10f8a01f7d06fb2e3f85c181bbb9
FROM ghcr.io/immich-app/base-server-prod:20240611@sha256:efd32a2af6e7ace8bcea1e94115fe95a971fe1d1fef7e667ff6e77364ce51c46
WORKDIR /usr/src/app
ENV NODE_ENV=production \

View File

@@ -1,12 +1,12 @@
{
"name": "immich",
"version": "1.105.1",
"version": "1.106.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "immich",
"version": "1.105.1",
"version": "1.106.3",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@nestjs/bullmq": "^10.0.1",
@@ -76,7 +76,6 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",
@@ -5790,15 +5789,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
},
"node_modules/@types/imagemin": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
"integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/inquirer": {
"version": "8.2.6",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",
@@ -20214,15 +20204,6 @@
"resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.2.tgz",
"integrity": "sha512-lPG6KlZs88gef6aD85z3HNkztpj7w2R7HmR3gygjfXCQmsLloWNARFkMuzKiiY8FGdh1XDpgBdrSf4aKDiA7Kg=="
},
"@types/imagemin": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/@types/imagemin/-/imagemin-8.0.5.tgz",
"integrity": "sha512-tah3dm+5sG+fEDAz6CrQ5evuEaPX9K6DF3E5a01MPOKhA2oGBoC+oA5EJzSugB905sN4DE19EDzldT2Cld2g6Q==",
"dev": true,
"requires": {
"@types/node": "*"
}
},
"@types/inquirer": {
"version": "8.2.6",
"resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.2.6.tgz",

View File

@@ -1,6 +1,6 @@
{
"name": "immich",
"version": "1.105.1",
"version": "1.106.3",
"description": "",
"author": "",
"private": true,
@@ -102,7 +102,6 @@
"@types/cookie-parser": "^1.4.3",
"@types/express": "^4.17.17",
"@types/fluent-ffmpeg": "^2.1.21",
"@types/imagemin": "^8.0.1",
"@types/js-yaml": "^4.0.9",
"@types/lodash": "^4.14.197",
"@types/mock-fs": "^4.13.1",

View File

@@ -374,7 +374,8 @@ export const immichAppConfig: ConfigModuleOptions = {
DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
IMMICH_PORT: Joi.number().optional(),
IMMICH_METRICS_PORT: Joi.number().optional(),
IMMICH_API_METRICS_PORT: Joi.number().optional(),
IMMICH_MICROSERVICES_METRICS_PORT: Joi.number().optional(),
IMMICH_METRICS: Joi.boolean().optional().default(false),
IMMICH_HOST_METRICS: Joi.boolean().optional().default(false),

View File

@@ -254,7 +254,7 @@ export class StorageCore {
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
return false;
}
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: true });
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
const { checksum } = assetInfo;
const newChecksum = await this.cryptoRepository.hashFile(newPath);

View File

@@ -42,8 +42,8 @@ export class SystemConfigCore {
instance = null;
}
async getConfig(force = false): Promise<SystemConfig> {
if (force || !this.config) {
async getConfig({ withCache }: { withCache: boolean }): Promise<SystemConfig> {
if (!withCache || !this.config) {
const lastUpdated = this.lastUpdated;
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
if (lastUpdated === this.lastUpdated) {
@@ -74,13 +74,13 @@ export class SystemConfigCore {
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
const config = await this.getConfig(true);
const config = await this.getConfig({ withCache: false });
this.config$.next(config);
return config;
}
async refreshConfig() {
const newConfig = await this.getConfig(true);
const newConfig = await this.getConfig({ withCache: false });
this.config$.next(newConfig);
}

View File

@@ -127,10 +127,10 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
stackParentId: withStack ? entity.stack?.primaryAssetId : undefined,
stack: withStack
? entity.stack?.assets
.filter((a) => a.id !== entity.stack?.primaryAssetId)
.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
?.filter((a) => a.id !== entity.stack?.primaryAssetId)
?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth }))
: undefined,
stackCount: entity.stack?.assets?.length ?? null,
stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null,
isOffline: entity.isOffline,
hasMetadata: true,
duplicateId: entity.duplicateId,

View File

@@ -21,7 +21,7 @@ export class DuplicateDetectionConfig extends TaskConfig {
@Min(0.001)
@Max(0.1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'float' })
@ApiProperty({ type: 'number', format: 'double' })
maxDistance!: number;
}
@@ -30,14 +30,14 @@ export class FacialRecognitionConfig extends ModelConfig {
@Min(0)
@Max(1)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'float' })
@ApiProperty({ type: 'number', format: 'double' })
minScore!: number;
@IsNumber()
@Min(0)
@Max(2)
@Type(() => Number)
@ApiProperty({ type: 'number', format: 'float' })
@ApiProperty({ type: 'number', format: 'double' })
maxDistance!: number;
@IsNumber()

View File

@@ -21,7 +21,7 @@ export class ServerStorageResponseDto {
@ApiProperty({ type: 'integer', format: 'int64' })
diskAvailableRaw!: number;
@ApiProperty({ type: 'number', format: 'float' })
@ApiProperty({ type: 'number', format: 'double' })
diskUsagePercentage!: number;
}

View File

@@ -7,9 +7,6 @@ export class AssetFullSyncDto {
@ValidateUUID({ optional: true })
lastId?: string;
@ValidateDate({ optional: true })
lastCreationDate?: Date;
@ValidateDate()
updatedUntil!: Date;

View File

@@ -16,4 +16,6 @@ export class AssetStackEntity {
@Column({ nullable: false })
primaryAssetId!: string;
assetCount?: number;
}

View File

@@ -122,7 +122,6 @@ export interface AssetExploreOptions extends AssetExploreFieldOptions {
export interface AssetFullSyncOptions {
ownerId: string;
lastCreationDate?: Date;
lastId?: string;
updatedUntil: Date;
limit: number;
@@ -157,7 +156,7 @@ export interface IAssetRepository {
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]>;
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]>;
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
getById(
id: string,

View File

@@ -120,6 +120,10 @@ export interface IEntityJob extends IBaseJob {
source?: 'upload' | 'sidecar-write' | 'copy';
}
export interface IAssetDeleteJob extends IEntityJob {
deleteOnDisk: boolean;
}
export interface ILibraryFileJob extends IEntityJob {
ownerId: string;
assetPath: string;
@@ -246,7 +250,7 @@ export type JobItem =
// Asset Deletion
| { name: JobName.PERSON_CLEANUP; data?: IBaseJob }
| { name: JobName.ASSET_DELETION; data: IEntityJob }
| { name: JobName.ASSET_DELETION; data: IAssetDeleteJob }
| { name: JobName.ASSET_DELETION_CHECK; data?: IBaseJob }
// Library Management

View File

@@ -1049,50 +1049,18 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
"asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)
AND ("asset"."fileCreatedAt", "asset"."id") < ($2, $3)
AND "asset"."updatedAt" <= $4
AND "asset"."id" > $2
AND "asset"."updatedAt" <= $3
ORDER BY
"asset"."fileCreatedAt" DESC,
"asset"."id" DESC
"asset"."id" ASC
LIMIT
10
@@ -1156,42 +1124,11 @@ SELECT
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
"exifInfo"."fps" AS "exifInfo_fps",
"stack"."id" AS "stack_id",
"stack"."primaryAssetId" AS "stack_primaryAssetId",
"stackedAssets"."id" AS "stackedAssets_id",
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
"stackedAssets"."type" AS "stackedAssets_type",
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
"stackedAssets"."checksum" AS "stackedAssets_checksum",
"stackedAssets"."duration" AS "stackedAssets_duration",
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
"stackedAssets"."stackId" AS "stackedAssets_stackId",
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
"stack"."primaryAssetId" AS "stack_primaryAssetId"
FROM
"assets" "asset"
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
AND ("stackedAssets"."deletedAt" IS NULL)
WHERE
"asset"."isVisible" = true
AND "asset"."ownerId" IN ($1)

View File

@@ -155,8 +155,8 @@ export class AssetRepository implements IAssetRepository {
});
}
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]> {
return this.repository.find({
async getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<string[]> {
const assets = await this.repository.find({
select: { deviceAssetId: true },
where: {
deviceAssetId: In(deviceAssetIds),
@@ -165,6 +165,8 @@ export class AssetRepository implements IAssetRepository {
},
withDeleted: true,
});
return assets.map((asset) => asset.deviceAssetId);
}
getByUserId(
@@ -763,36 +765,40 @@ export class AssetRepository implements IAssetRepository {
],
})
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]> {
const { ownerId, lastCreationDate, lastId, updatedUntil, limit } = options;
const { ownerId, lastId, updatedUntil, limit } = options;
const builder = this.getBuilder({
userIds: [ownerId],
exifInfo: true, // also joins stack information
exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit`
withStacked: false, // return all assets individually as expected by the app
});
})
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount');
if (lastCreationDate !== undefined && lastId !== undefined) {
builder.andWhere('(asset.fileCreatedAt, asset.id) < (:lastCreationDate, :lastId)', {
lastCreationDate,
lastId,
});
if (lastId !== undefined) {
builder.andWhere('asset.id > :lastId', { lastId });
}
return builder
builder
.andWhere('asset.updatedAt <= :updatedUntil', { updatedUntil })
.orderBy('asset.fileCreatedAt', 'DESC')
.addOrderBy('asset.id', 'DESC')
.limit(limit)
.withDeleted()
.getMany();
.orderBy('asset.id', 'ASC')
.limit(limit) // cannot use `take` for performance reasons
.withDeleted();
return builder.getMany();
}
@GenerateSql({ params: [{ userIds: [DummyValue.UUID], updatedAfter: DummyValue.DATE }] })
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]> {
const builder = this.getBuilder({ userIds: options.userIds, exifInfo: true, withStacked: false })
const builder = this.getBuilder({
userIds: options.userIds,
exifInfo: false, // need to do this manually because `exifInfo: true` also loads stacked assets messing with `limit`
withStacked: false, // return all assets individually as expected by the app
})
.leftJoinAndSelect('asset.exifInfo', 'exifInfo')
.leftJoinAndSelect('asset.stack', 'stack')
.loadRelationCountAndMap('stack.assetCount', 'stack.assets', 'stackedAssetsCount')
.andWhere({ updatedAt: MoreThan(options.updatedAfter) })
.limit(options.limit)
.limit(options.limit) // cannot use `take` for performance reasons
.withDeleted();
return builder.getMany();
}
}

View File

@@ -45,7 +45,7 @@ export class MediaRepository implements IMediaRepository {
}
async generateThumbnail(input: string | Buffer, output: string, options: ThumbnailOptions): Promise<void> {
const pipeline = sharp(input, { failOn: 'none' })
const pipeline = sharp(input, { failOn: 'none', limitInputPixels: false })
.pipelineColorspace(options.colorspace === Colorspace.SRGB ? 'srgb' : 'rgb16')
.rotate();

View File

@@ -1,6 +1,6 @@
import { Inject, Injectable } from '@nestjs/common';
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
import { DefaultExiftoolArgs, DefaultReadTaskOptions, ExifTool, Tags } from 'exiftool-vendored';
import { DefaultReadTaskOptions, Tags, exiftool } from 'exiftool-vendored';
import geotz from 'geo-tz';
import { DummyValue, GenerateSql } from 'src/decorators';
import { ExifEntity } from 'src/entities/exif.entity';
@@ -21,23 +21,18 @@ export class MetadataRepository implements IMetadataRepository {
) {
this.logger.setContext(MetadataRepository.name);
}
private exiftool: ExifTool = this.initExiftool();
async teardown() {
await this.exiftool.end();
}
private initExiftool() {
// Enable exiftool LFS to parse metadata for files larger than 2GB.
const exiftoolArgs = ['-api', 'largefilesupport=1', ...DefaultExiftoolArgs];
return new ExifTool({ exiftoolArgs });
await exiftool.end();
}
readTags(path: string): Promise<ImmichTags | null> {
return this.exiftool
return exiftool
.read(path, undefined, {
...DefaultReadTaskOptions,
// Enable exiftool LFS to parse metadata for files larger than 2GB.
optionalArgs: ['-api', 'largefilesupport=1'],
defaultVideosToUTC: true,
backfillTimezones: true,
inferTimezoneFromDatestamps: true,
@@ -53,12 +48,12 @@ export class MetadataRepository implements IMetadataRepository {
}
extractBinaryTag(path: string, tagName: string): Promise<Buffer> {
return this.exiftool.extractBinaryTagToBuffer(tagName, path);
return exiftool.extractBinaryTagToBuffer(tagName, path);
}
async writeTags(path: string, tags: Partial<Tags>): Promise<void> {
try {
await this.exiftool.write(path, tags, ['-overwrite_original']);
await exiftool.write(path, tags, ['-overwrite_original']);
} catch (error) {
this.logger.warn(`Error writing exif data (${path}): ${error}`);
}

View File

@@ -277,14 +277,12 @@ export class AssetMediaService {
auth: AuthDto,
checkExistingAssetsDto: CheckExistingAssetsDto,
): Promise<CheckExistingAssetsResponseDto> {
const assets = await this.assetRepository.getByDeviceIds(
const existingIds = await this.assetRepository.getByDeviceIds(
auth.user.id,
checkExistingAssetsDto.deviceId,
checkExistingAssetsDto.deviceAssetIds,
);
return {
existingIds: assets.map((asset) => asset.id),
};
return { existingIds };
}
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {

View File

@@ -389,8 +389,8 @@ describe(AssetService.name, () => {
await sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'], force: true });
expect(jobMock.queueAll).toHaveBeenCalledWith([
{ name: JobName.ASSET_DELETION, data: { id: 'asset1' } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset2' } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset1', deleteOnDisk: true } },
{ name: JobName.ASSET_DELETION, data: { id: 'asset2', deleteOnDisk: true } },
]);
});
@@ -410,7 +410,7 @@ describe(AssetService.name, () => {
assetMock.getById.mockResolvedValue(assetWithFace);
await sut.handleAssetDeletion({ id: assetWithFace.id });
await sut.handleAssetDeletion({ id: assetWithFace.id, deleteOnDisk: true });
expect(jobMock.queue.mock.calls).toEqual([
[
@@ -435,7 +435,7 @@ describe(AssetService.name, () => {
it('should update stack primary asset if deleted asset was primary asset in a stack', async () => {
assetMock.getById.mockResolvedValue(assetStub.primaryImage as AssetEntity);
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id });
await sut.handleAssetDeletion({ id: assetStub.primaryImage.id, deleteOnDisk: true });
expect(assetStackMock.update).toHaveBeenCalledWith({
id: 'stack-1',
@@ -446,10 +446,21 @@ describe(AssetService.name, () => {
it('should delete a live photo', async () => {
assetMock.getById.mockResolvedValue(assetStub.livePhotoStillAsset);
await sut.handleAssetDeletion({ id: assetStub.livePhotoStillAsset.id });
await sut.handleAssetDeletion({
id: assetStub.livePhotoStillAsset.id,
deleteOnDisk: true,
});
expect(jobMock.queue.mock.calls).toEqual([
[{ name: JobName.ASSET_DELETION, data: { id: assetStub.livePhotoMotionAsset.id } }],
[
{
name: JobName.ASSET_DELETION,
data: {
id: assetStub.livePhotoMotionAsset.id,
deleteOnDisk: true,
},
},
],
[
{
name: JobName.DELETE_FILES,
@@ -463,7 +474,7 @@ describe(AssetService.name, () => {
it('should update usage', async () => {
assetMock.getById.mockResolvedValue(assetStub.image);
await sut.handleAssetDeletion({ id: assetStub.image.id });
await sut.handleAssetDeletion({ id: assetStub.image.id, deleteOnDisk: true });
expect(userMock.updateUsage).toHaveBeenCalledWith(assetStub.image.ownerId, -5000);
});
});

View File

@@ -27,7 +27,7 @@ import { IAssetStackRepository } from 'src/interfaces/asset-stack.interface';
import { IAssetRepository } from 'src/interfaces/asset.interface';
import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface';
import {
IEntityJob,
IAssetDeleteJob,
IJobRepository,
ISidecarWriteJob,
JOBS_ASSET_PAGINATION_SIZE,
@@ -245,7 +245,7 @@ export class AssetService {
}
async handleAssetDeletionCheck(): Promise<JobStatus> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
const trashedDays = config.trash.enabled ? config.trash.days : 0;
const trashedBefore = DateTime.now()
.minus(Duration.fromObject({ days: trashedDays }))
@@ -256,15 +256,21 @@ export class AssetService {
for await (const assets of assetPagination) {
await this.jobRepository.queueAll(
assets.map((asset) => ({ name: JobName.ASSET_DELETION, data: { id: asset.id } })),
assets.map((asset) => ({
name: JobName.ASSET_DELETION,
data: {
id: asset.id,
deleteOnDisk: true,
},
})),
);
}
return JobStatus.SUCCESS;
}
async handleAssetDeletion(job: IEntityJob): Promise<JobStatus> {
const { id } = job;
async handleAssetDeletion(job: IAssetDeleteJob): Promise<JobStatus> {
const { id, deleteOnDisk } = job;
const asset = await this.assetRepository.getById(id, {
faces: {
@@ -301,12 +307,14 @@ export class AssetService {
// TODO refactor this to use cascades
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
await this.jobRepository.queue({
name: JobName.ASSET_DELETION,
data: { id: asset.livePhotoVideoId, deleteOnDisk },
});
}
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];
// skip originals if the user deleted the whole library
if (!asset.library?.deletedAt) {
if (deleteOnDisk) {
files.push(asset.sidecarPath, asset.originalPath);
}
@@ -321,7 +329,12 @@ export class AssetService {
await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids);
if (force) {
await this.jobRepository.queueAll(ids.map((id) => ({ name: JobName.ASSET_DELETION, data: { id } })));
await this.jobRepository.queueAll(
ids.map((id) => ({
name: JobName.ASSET_DELETION,
data: { id, deleteOnDisk: true },
})),
);
} else {
await this.assetRepository.softDeleteAll(ids);
this.eventRepository.clientSend(ClientEvent.ASSET_TRASH, auth.user.id, ids);

View File

@@ -77,7 +77,7 @@ export class AuthService {
}
async login(dto: LoginCredentialDto, details: LoginDetails) {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
if (!config.passwordLogin.enabled) {
throw new UnauthorizedException('Password login has been disabled');
}
@@ -174,7 +174,7 @@ export class AuthService {
}
async authorize(dto: OAuthConfigDto): Promise<OAuthAuthorizeResponseDto> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
if (!config.oauth.enabled) {
throw new BadRequestException('OAuth is not enabled');
}
@@ -190,7 +190,7 @@ export class AuthService {
}
async callback(dto: OAuthCallbackDto, loginDetails: LoginDetails) {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
const profile = await this.getOAuthProfile(config, dto.url);
this.logger.debug(`Logging in with OAuth: ${JSON.stringify(profile)}`);
let user = await this.userRepository.getByOAuthId(profile.sub);
@@ -242,7 +242,7 @@ export class AuthService {
}
async link(auth: AuthDto, dto: OAuthCallbackDto): Promise<UserAdminResponseDto> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
const { sub: oauthId } = await this.getOAuthProfile(config, dto.url);
const duplicate = await this.userRepository.getByOAuthId(oauthId);
if (duplicate && duplicate.id !== auth.user.id) {
@@ -264,7 +264,7 @@ export class AuthService {
return LOGIN_URL;
}
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
if (!config.oauth.enabled) {
return LOGIN_URL;
}

View File

@@ -42,25 +42,25 @@ export class CliService {
}
async disablePasswordLogin(): Promise<void> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
config.passwordLogin.enabled = false;
await this.configCore.updateConfig(config);
}
async enablePasswordLogin(): Promise<void> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
config.passwordLogin.enabled = true;
await this.configCore.updateConfig(config);
}
async disableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
config.oauth.enabled = false;
await this.configCore.updateConfig(config);
}
async enableOAuthLogin(): Promise<void> {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
config.oauth.enabled = true;
await this.configCore.updateConfig(config);
}

View File

@@ -43,7 +43,7 @@ export class DuplicateService {
}
async handleQueueSearchDuplicates({ force }: IBaseJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
const { machineLearning } = await this.configCore.getConfig({ withCache: false });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}
@@ -64,7 +64,7 @@ export class DuplicateService {
}
async handleSearchDuplicates({ id }: IEntityJob): Promise<JobStatus> {
const { machineLearning } = await this.configCore.getConfig();
const { machineLearning } = await this.configCore.getConfig({ withCache: true });
if (!isDuplicateDetectionEnabled(machineLearning)) {
return JobStatus.SKIPPED;
}

View File

@@ -150,7 +150,7 @@ export class JobService {
}
async init(jobHandlers: Record<JobName, JobHandler>) {
const config = await this.configCore.getConfig();
const config = await this.configCore.getConfig({ withCache: false });
for (const queueName of Object.values(QueueName)) {
let concurrency = 1;

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