Compare commits

...

409 Commits

Author SHA1 Message Date
github-actions
068904f746 chore: version v1.114.0 2024-09-06 13:49:08 +00:00
Alex
5d8052202e chore(mobile): Translations update (#12392)
chore(mobile): translation update
2024-09-06 13:30:26 +00:00
Ivan Mondragon
2dc95704c5 feat(web): add download shortcut on the timeline & asset viewer (#12339)
feat(web): implement download shortcut
2024-09-06 08:26:58 -05:00
Michel Heusschen
529b7fe748 fix(web): show focus outline for asset thumbnails again (#12382)
* fix(web): show focus outline for asset thumbnails again

* fix e2e test
2024-09-06 08:18:45 -05:00
martin
a653d9d29f feat: optimize copy image to clipboard (#12366)
* feat: optimize copy image to clipboard

* pr feedback

* fix: urlToBlob

Co-authored-by: Jason Rasmussen <jason@rasm.me>

* fix: imgToBlob

Co-authored-by: Jason Rasmussen <jason@rasm.me>

* chore: finish rename

* fix: dimensions

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-09-06 08:16:59 -05:00
Michel Heusschen
ecc85ff6c6 fix(web): ensure shared link covers are full size (#12386) 2024-09-06 08:16:39 -05:00
Michel Heusschen
639bc0c660 fix(web): broken album thumbnail (#12381)
* fix(web): broken album thumbnail

* use properties from thumbnail
2024-09-06 08:16:18 -05:00
Michel Heusschen
9fc30d6bf6 fix(web): auth on navigation from shared link to timeline (#12385) 2024-09-06 08:15:48 -05:00
Mert
aa0097bde2 fix(server): copy video projection metadata for 360 videos (#12376) 2024-09-06 00:30:34 -04:00
Weblate (bot)
02803816f4 chore(web): update translations (#12265)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Adrian M <adimarculescu@gmail.com>
Co-authored-by: Anthony MARGERAND <anthow69@hotmail.fr>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Javier Montón <jmlarraz@gmail.com>
Co-authored-by: Jonathan Jogenfors <jonathan@jogenfors.se>
Co-authored-by: Mathias <mathkot59@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Nicolai Bonde <git@nicolaibonde.dk>
Co-authored-by: S Kutu <spamkutu@mail.ru>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2024-09-05 19:57:27 -04:00
Mert
eb7777639d fix(server): clean face tables after delete (#12375)
clean face tables after delete
2024-09-05 23:09:19 +00:00
Mark
649897f737 docs: Add conditional album storage template information (#12218) 2024-09-05 23:57:12 +02:00
Jason Rasmussen
b0af9be513 fix(web): person asset grid (#12370) 2024-09-05 20:49:23 +00:00
Jason Rasmussen
d6729c50c9 fix: only load rtl plugin once (#12365)
fix(web): only load rtl plugin once
2024-09-05 14:29:41 -04:00
Alex
77904a54d8 fix(mobile): download asset to Camera folder on Android (#12355)
* fix(mobile): download asset to Camera folder on Android

* remove unused import

* better message

* linting
2024-09-05 17:33:55 +00:00
Alex
0148005931 chore: upgrade openapi generator version (#12358) 2024-09-05 11:31:48 -05:00
Alex
dfcdaefa22 fix(web): showing album timeline after adding new assets (#12354) 2024-09-05 10:37:14 -05:00
Min Idzelis
d7d3b8dfec fix: flash bug on tag (#12332)
* fix flash bug on tag

* fix lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-05 09:29:07 -05:00
Lukas
27e283e724 fix(server): search suggestions include partner assets (#12269)
search suggestions now include partner assets

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-05 09:12:46 -05:00
Carsten Otto
259bc8a6b0 fix(web): only show valid time zones/offsets, update list based on date (#12315)
fix(web): only show valid time zones / offsets, update list based on date

this also prefers the local time zone over others with the same offset
2024-09-05 09:12:22 -05:00
Ben
c5848112bb feat(web): add skip link to sidebar (#12330)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-05 08:24:24 -05:00
Jason Rasmussen
ce2349d496 fix(server): asset no longer has tags (#12350) 2024-09-05 08:24:10 -05:00
Alex
f26d47c8d9 fix(mobile): background task crashing on Android (#12314) 2024-09-04 22:39:50 -05:00
Jason Rasmussen
f4ec842577 refactor(web): upload panel (#12326)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-04 23:38:55 -04:00
Zack Pollard
0d6bef2c05 ci: job naming improvements and success job for matrix (#12316)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2024-09-04 23:28:30 +01:00
BugFest
77e6a6d78b feat(server): Import face regions from metadata (#6455)
* feat: faces-from-metadata - Import face regions from metadata

Implements immich-app#1692.
- OpenAPI spec changes to accomodate metadata face import configs. New settings to enable the feature.
- Updates admin UI compoments
- ML faces detection/recognition & Exif/Metadata faces compatibility

Signed-off-by: BugFest <bugfest.dev@pm.me>

* chore(web): remove unused file confirm-enable-import-faces

* chore(web): format metadata-settings

* fix(server): faces-from-metadata tests and format

* fix(server): code refinements, nullable face asset sourceType

* fix(server): Add RegionInfo to ImmichTags interface

* fix(server): deleteAllFaces sourceType param can be undefined

* fix(server): exiftool-vendored 27.0.0 moves readArgs into ExifToolOptions

* fix(server): rename isImportFacesFromMetadataEnabled to isFaceImportEnabled

* fix(server): simplify sourceType conditional

* fix(server): small fixes

* fix(server): handling sourceType

* fix(server): sourceType enum

* fix(server): refactor metadata applyTaggedFaces

* fix(server): create/update signature changes

* fix(server): reduce computational cost of Person.getManyByName

* fix(server): use faceList instead of faceSet

* fix(server): Skip regions without Name defined

* fix(mobile): Update open-api (face assets feature changes)

* fix(server): Face-Person reconciliation with map/index

* fix(server): tags.RegionInfo.AppliedToDimensions must be defined to process face-region

* fix(server): fix shared-link.service.ts format

* fix(mobile): Update open-api after branch update

* simplify

* fix(server): minor fixes

* fix(server): person create/update methods type enforcement

* fix(server): style fixes

* fix(server): remove unused metadata code

* fix(server): metadata faces unit tests

* fix(server): top level config metadata category

* fix(server): rename upsertFaces to replaceFaces

* fix(server): remove sourceType when unnecessary

* fix(server): sourceType as ENUM

* fix(server): format fixes

* fix(server): fix tests after sourceType ENUM change

* fix(server): remove unnecessary JobItem cast

* fix(server): fix asset enum imports

* fix(open-api): add metadata config

* fix(mobile): update open-api after metadata open-api spec changes

* fix(web): update web/api metadata config

* fix(server): remove duplicated sourceType def

* fix(server): update generated sql queries

* fix(e2e): tests for metadata face import feature

* fix(web): Fix check:typescript

* fix(e2e): update subproject ref

* fix(server): revert format changes to pass format checks after ci

* fix(mobile): update open-api

* fix(server,movile,open-api,mobile): sourceType as DB data type

* fix(e2e): upload face asset after enabling metadata face import

* fix(web): simplify metadata admin settings and i18n keys

* Update person.repository.ts

Co-authored-by: Jason Rasmussen <jason@rasm.me>

* fix(server): asset_faces.sourceType column not nullable

* fix(server): simplified syntax

* fix(e2e): use SDK for everything except the endpoint being tested

* fix(e2e): fix test format

* chore: clean up

* chore: clean up

* chore: update e2e/test-assets

---------

Signed-off-by: BugFest <bugfest.dev@pm.me>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-09-04 18:23:58 -04:00
Jason Rasmussen
720412645f feat(web): sort albums in modal (#12331) 2024-09-04 18:21:21 -04:00
Alex
0a8bd7dc66 fix(web): correct color for active tree item (#12318)
* fix(web): correct color for active tree item

* remove white space
2024-09-04 14:07:32 -05:00
renovate[bot]
f8211a128e fix(deps): update machine-learning (#12257) 2024-09-04 14:36:12 -04:00
Jason Rasmussen
12b65e3c24 fix(server): auto-reconnect to database (#12320) 2024-09-04 13:32:43 -04:00
Zack Pollard
1783dfd393 fix(web): handle RTL languages in the map component (#12308) 2024-09-04 17:02:37 +01:00
Alex
d685bc1f34 chore(mobile): handle sync album on duplicated (#12173)
* chore(mobile): handle sync album on duplicated

* remove check for duplicate in manual sync

* linting
2024-09-04 10:39:31 -05:00
Carsten Otto
4bf82fb4c4 fix(web): retain selected time zone offset also for +00:00 (#12310)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-09-04 14:47:40 +00:00
Carsten Otto
cbb0a7f8d4 fix(server): parse time zone with explicit zero offset (#12307)
* fix(server): fix test: use data as returned by exiftool-vendored

* fix(server): retain +00:00 timezone if set explicitly
2024-09-04 09:27:04 -05:00
Jason Rasmussen
ee6550c02c feat(web): add Malay language (#12311)
feat(web): add ms.json
2024-09-04 09:20:45 -04:00
Jason Rasmussen
69cedef772 chore: remove repair sidebar item (#12294) 2024-09-03 22:54:13 -05:00
Ben
1e509d97f6 feat(web): show folder navigation in root directory (#12299) 2024-09-03 22:53:48 -05:00
Jason Rasmussen
c7ddd0b44a fix(web): paste event in input fields (#12297) 2024-09-03 22:53:34 -05:00
Jason Rasmussen
c3a8ddaaf2 fix(server): missing asset files relation (#12295) 2024-09-03 21:23:34 -04:00
Jason Rasmussen
526cf23a9e fix(server): public references in migrations (#12298) 2024-09-04 01:20:21 +00:00
renovate[bot]
e1ed7fa6ed fix(deps): update typescript-projects (#12274)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 19:58:03 -04:00
Gavin Mogan
0b6cd74e4d docs: ioredis link (#12291)
Fix link to ioredis docs

it was docker, now its ioredis!
2024-09-03 23:51:09 +00:00
Jason Rasmussen
7ca53ba507 feat(server): support lightroom tags (#12288) 2024-09-03 18:25:09 -04:00
Alex
a96f41aa11 fix: remove public. reference in migration sql (#12285) 2024-09-03 16:42:55 -05:00
Jason Rasmussen
ddd73b9911 feat(server): prefer tagslist (#12286) 2024-09-03 17:36:27 -04:00
Alex
6f37ab6a9e fix(server): empty trash for archived assets (#12281)
* fix(server): empty trash for archived assets

* use withArchived

* add e2e test
2024-09-03 16:04:35 -05:00
Ben McCann
e5667f09c7 chore(web): upgrade pre-req dependencies for Svelte 5 (#12283) 2024-09-03 16:42:46 -04:00
Zack Pollard
668632c398 ci: split e2e into web / server & cli / linting & run on mich (#12267)
* ci: split e2e tests into web / server & cli / linting

* ci: run e2e on mich
2024-09-03 15:19:47 -04:00
Alex
5d6716d265 chore(mobile): post release task (#12268) 2024-09-03 18:32:20 +01:00
Zack Pollard
b6cad7715f fix: docs oauth formatting issue (#12272) 2024-09-03 15:35:12 +00:00
github-actions
48da4c9317 chore: version v1.113.1 2024-09-03 14:12:24 +00:00
Weblate (bot)
a1d9619a6e chore(web): update translations (#12148)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/kmr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Indrek Haav <IndrekHaav@users.noreply.hosted.weblate.org>
Co-authored-by: Jacek <jacek64@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Mihai Paraipan <paraipanmihai@gmail.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>
Co-authored-by: Oğuz Ersen <oguz@ersen.moe>
Co-authored-by: PanSzelescik <panszelescik@gmail.com>
Co-authored-by: Polla Fattah <polla.fattah@gmail.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Rasmus Sehlin <rasmus@sehl.in>
Co-authored-by: S-H-Y-A <yamada0@hotmail.co.jp>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Thomas <thomas.ceccato.02@gmail.com>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: fmis13 <fmis13@disroot.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: kiwinho <kiwicaja@gmail.com>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: rbasliana <91536894+rbasliana@users.noreply.github.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-09-03 13:52:06 +00:00
Zack Pollard
5dd9a2f850 ci: replace deprecate cloudflare properties on cloudflare_record (#12262) 2024-09-03 09:27:50 -04:00
renovate[bot]
058b5ea5ca chore(deps): update base-image to v20240903 (major) (#12261)
chore(deps): update base-image to v20240903

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 13:58:11 +01:00
Zack Pollard
441b009a0b ci: more path filtering, path filtering happens in pre-job so all jobs can be required (#12260)
ci: don't use gha path filtering, use a pre-job to skip instead, add path filtering to more workflows
2024-09-03 13:23:39 +01:00
renovate[bot]
cb903db308 chore(deps): update grafana/grafana docker tag to v11.2.0 (#12209)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 09:00:54 +01:00
renovate[bot]
03ceca8552 chore(deps): update typescript-projects (#12251)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 08:59:17 +01:00
renovate[bot]
53609d45fe chore(deps): update dependency @types/node to ^20.16.2 (#12250)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-09-03 08:53:35 +01:00
PyKen
4af8433aad fix(server): remove thumbnailAt in asset_job_status for missing thumbnails (#12254)
* Remove thumbnailAt in asset_job_status for missing thumbnails

* fix linter error
2024-09-03 00:19:15 -04:00
Biepa
7c978571e0 docs: fixing example docker compose (#12230)
* Fixing example docker compose

Change needed so the following statement included in the docs a bit below makes sense:
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.

* another fix
2024-09-02 19:49:28 +00:00
martin
efdf1b49f4 fix: hide scrollbar when the asset grid is empty (#12217) 2024-09-02 14:43:36 -05:00
Yun Jiang
f46abbb5b5 fix(mobile): set SSL options properly in background backup process (#11870) (#12206)
Co-authored-by: Yun Jiang <yjiang@pulsesecure.net>
2024-09-02 19:42:51 +00:00
Ben
d8b602f757 feat(web): shared breadcrumbs component for folders and tags (#12215)
* feat(web): shared breadcrumbs component for folders and tags

* chore: revert changes to tree view
2024-09-02 14:42:27 -05:00
Alex
59507e557e fix(web): auto grow area extend when there is no content (#12197)
* fix(web): text area expand when there is no description

* use correct content
2024-09-02 14:41:19 -05:00
Ivan Mondragon
174de979db fix(mobile): Android back gesture closes app (#12221)
fix(mobile): Android back gesture closes app, disable predictive back gestures on Android

Co-authored-by: Ivan Mondragon <ivanmondragon42@gmail.com>
2024-09-02 14:40:11 -05:00
Vietbao Tran
862d6d9abe feat(web): load original panorama image when zoomed in to 75% or above (#12222)
* feat(web): load original panorama image when zoomed in to 75% or above

* add checks that original 360 image is web compatible and better error handling

* fix web compatability check typing

* fix asset type
2024-09-02 14:39:55 -05:00
Alex
bd6c5e1b1c feat(web): tag button in album/shared album (#12172) 2024-09-02 14:39:16 -05:00
Niklas Fischer
b80cc0d90f fix(web): German translation for explorer (#12180)
fix German translation for explorer
2024-09-02 12:33:32 -04:00
PyKen
438344fc8f fix(server): get assetFiles when retrieving assets WithoutProperty.THUMBNAIL (#12225) 2024-09-02 09:31:02 -04:00
Jonathan Jogenfors
39141d3f1c fix(server): remove offline assets from trash (#12199)
* use port not taken by immich-dev for e2e

* remove offline files from trash
2024-09-02 01:06:35 +02:00
Qhilm
28bc7f318e docs: typo - accesible => accessible (#12178)
[typo] accesible => accessible
2024-08-31 14:52:20 -05:00
Marco Malavolti
6bfe54788f docs: update google oauth examples (#12162)
* Small update on oauth.md for Google Authn

* Replace "demo" with "example" to be consistent with other example
2024-08-31 13:33:17 -04:00
Michel Heusschen
67468ea367 fix(web): avoid deleting empty album unexpectedly (#12175) 2024-08-31 12:24:38 -05:00
Alex
40327ad987 chore(mobile): post release tasks (#12157)
* sent to reviewer

* sent to reviewer

* update to app store

* update to app store
2024-08-30 16:35:06 -05:00
Jason Rasmussen
d18bc7007a fix: keyword parsing (#12164) 2024-08-30 21:33:42 +00:00
Ben
4cc11efd04 fix(web): hide tree view text overflow with ellipsis (#12161)
fix(web): hide tree view overflow with ellipsis
2024-08-30 17:32:12 -04:00
kaziu687
18fcc3569f fix(web): unable to scroll timeline after using gesture (#12163) 2024-08-30 21:31:42 +00:00
Alex
fcbc1ba399 fix(web): memory view in timeline href (#12158) 2024-08-30 15:00:31 -04:00
Jason Rasmussen
5e6ac87eaf chore: object shorthand linting rule (#12152)
chore: object shorthand
2024-08-30 14:38:53 -04:00
renovate[bot]
40854f358c chore(deps): update dependency svelte to v4.2.19 [security] (#12147)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-30 14:03:44 -04:00
Bastian Machek
51a11d0cb6 docs(project): lightroom project (#12149)
* Update community-projects.tsx

Added my community project: lrc-immich-plugin

* Update community-projects.tsx

typo
2024-08-30 14:01:50 -04:00
github-actions
cc88cbb456 chore: version v1.113.0 2024-08-30 17:16:21 +00:00
Zack Pollard
860ba78650 ci: fix release script (#12146) 2024-08-30 18:07:02 +01:00
Jason Rasmussen
9b1a985d29 fix(server): tag upsert (#12141) 2024-08-30 12:44:24 -04:00
Pierre Couy
b9e5e40ced docs(guide): nginx caching proxy (#12140)
* docs:Add link to nginx caching proxy guide

Following comments on https://github.com/immich-app/immich/pull/11350

* docs:Fix typo

* docs:Fix typo

* docs:Switch to GitHub link
2024-08-30 12:26:31 -04:00
Alex
3316acb71f chore(web): tag creation hint (#12142)
* chore(web): tag creation hint

* use FormatMessage

* use correct format

* use correct css class

* copywriting
2024-08-30 12:16:56 -04:00
Alex
1736887f96 chore(mobile): translations update (#12144)
chore(mobile): translation update
2024-08-30 12:06:25 -04:00
Weblate (bot)
c40262f3ff chore(web): update translations (#12097)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
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: Bogdan Predi <b@predi.dev>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Indrek Haav <IndrekHaav@users.noreply.hosted.weblate.org>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Mário Victor Ribeiro Silva <mariovictorrs@gmail.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: S-H-Y-A <yamada0@hotmail.co.jp>
Co-authored-by: Samuel Lambert <sam.f.lambert@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: rbasliana <91536894+rbasliana@users.noreply.github.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-08-30 11:02:56 -05:00
Alex
b3b599e071 chore(server): deprecate resized property (#12143)
* chore: add dummy resized value for release

* openapi

* add deprecation life cycle info

* use correct default value
2024-08-30 11:01:50 -05:00
Michel Heusschen
b1e780561d fix(web): reset asset grid after changing album order (#12139) 2024-08-30 09:31:53 -05:00
Jonathan Jogenfors
aa04ded311 chore(e2e): change e2e ports to some not used by immich-dev (#12132)
use port not taken by immich-dev for e2e
2024-08-30 08:04:02 -04:00
Alex
fa9b2219f8 chore(mobile): disable Impeller on Android (#12130)
chore(mobile): disable Impeller
2024-08-29 23:41:07 -05:00
ttzytt
7d0c64b73e fix: README_zh_CN.md link (#12124)
Change `https://immich.app/。` to `<https://immich.app/>。`, so that the period will be excluded in the URL.
2024-08-30 04:09:24 +00:00
Spencer Fasulo
48fb0f309d fix(web): Device list shows Ubuntu as unknown OS (#12127)
Co-authored-by: Spencer Fasulo <spencer.fasulo@icloud.com>
2024-08-30 03:14:05 +00:00
Jason Rasmussen
8c54312c87 docs: update roadmap (#12126) 2024-08-29 22:47:22 +00:00
Jonathan Jogenfors
eb4a291c81 chore(server): log path when generating external thumbnail (#12107)
* feat: log path when generating external thumbnail

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-29 22:16:12 +00:00
Jason Rasmussen
c63f63cc15 fix: user specific fields in asset search (#12125) 2024-08-29 18:07:45 -04:00
immich-tofu[bot]
715ac4c599 chore: modify .github/FUNDING.yml 2024-08-29 21:18:20 +00:00
Alex
6fe011e2d7 feat(web): jump to timeline (#12117)
* feat(web): jump to timeline

* Update web/src/lib/components/memory-page/memory-viewer.svelte

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

* wording and open in new tab

* Use correct wording and icon

* fix: hide on archived and trashed assets

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-29 21:14:52 +00:00
Alex
ebecb60f39 feat: user's features preferences (#12099)
* feat: metadata in UserPreference

* feat: web metadata settings

* feat: web metadata settings

* fix: typo

* patch openapi

* fix: missing translation key

* new organization of preference strucutre

* feature settings on web

* localization

* added and used feature settings

* add default value to response dto

* patch openapi

* format en.json file

* implement helper method

* use tags preference logic

* Fix logic bug and add tests

* fix preference can be null in detail panel
2024-08-29 14:29:04 -05:00
Alex
9bfaa525db fix(mobile): long waiting time for login request when server is unreachable (#12100)
* fix(mobile): long waiting time for login request when server is unreachable

* lint

* increase timeout duration
2024-08-29 13:46:47 -05:00
Michel Heusschen
74f18a4523 fix(server): skip smtp validation if unchanged (#12111)
* fix(server): skip smtp validation if unchanged

* update comparison + convert config to plain object
2024-08-29 14:10:09 -04:00
Jason Rasmussen
d08a20bd57 feat: tags (#11980)
* feat: tags

* fix: folder tree icons

* navigate to tag from detail panel

* delete tag

* Tag position and add tag button

* Tag asset in detail panel

* refactor form

* feat: navigate to tag page from clicking on a tag

* feat: delete tags from the tag page

* refactor: moving tag section in detail panel and add + tag button

* feat: tag asset action in detail panel

* refactor add tag form

* fdisable add tag button when there is no selection

* feat: tag bulk endpoint

* feat: tag colors

* chore: clean up

* chore: unit tests

* feat: write tags to sidecar

* Remove tag and auto focus on tag creation form opened

* chore: regenerate migration

* chore: linting

* add color picker to tag edit form

* fix: force render tags timeline on navigating back from asset viewer

* feat: read tags from keywords

* chore: clean up

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-29 12:14:03 -04:00
src
682adaa334 fix(mobile): allow create empty non-shared albums, add proper button colors (#12103)
* Add proper colors to create album button
Allow creation of empty albums with names, or non-empty albums without names

* Add proper colors to create album button
Allow creation of empty albums with names, or non-empty albums without names

* Small changes

* Revert change

* Simplify logic

* lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-29 15:57:42 +00:00
kaziu687
c008feca63 feat(web): navigate assets with gestures (next/prev) (#11888)
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-29 10:40:17 -05:00
Richard Kojedzinszky
f3e176e192 feat(ml): support dynamic scaling (#12065)
feat(ml): make http keep-alive configurable

Closes #12064
2024-08-29 15:11:49 +00:00
Michel Heusschen
9f5a3f1e84 chore(web): enforce valid translation keys using typescript (#12106) 2024-08-29 08:41:39 -04:00
Jonathan Jogenfors
bab5ad7ebd fix(server): ensure new exclusion patterns work (#12102)
* add test for bug

* find excluded paths when checking offline

* fix filename

* fix unit tests

* bump picomatch

* fix e2e paths

* improve e2e

* add unit tests

* cleanup e2e

* set correct asset count

* fix e2e test

* fix lint
2024-08-28 19:51:25 -04:00
renovate[bot]
c6c7c54fa5 chore(deps): update machine-learning (#12062) 2024-08-28 18:00:47 -04:00
renovate[bot]
f0c86846e0 fix(deps): update machine-learning (major) (#11928) 2024-08-28 17:59:57 -04:00
Geoffrey Frogeye
562fec6e2b feat(server): sort images in duplicate groups by date (#12094)
* feat(server): sort images in duplicate groups by date

* Update server/src/dtos/duplicate.dto.ts

Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-08-28 19:59:09 +00:00
Jonathan Jogenfors
363c558db7 fix(server): don't crash when refreshing large libraries (#7934)
* add job to check for offline files

* fix lint

* only check for offline when using checkForOffline

* improve tests

* remove old test

* wip

* remove trie

* refactor batches

* also check offline status

* fix spelling

* don't do offline scan

* rename scan to check

* fix job statuses

* fix lint

* cleanup

* add test

* open-api

* fix test

* fix spinner

* reset text

* don't double batch

* fix comments from mert

* remove tries

* fix tests

* fix e2e

* fix test

* fix test

* add tests

* fix lint

* fix e2e

* interweave scans

* fix errors

* fix messages

* fix test

* add mock

* fix sql

* fix e2e

* use library batch size

* save -> update

* add file extensions

* update specs

* test for import paths

* check import paths when testing offline

* fix lint

* normalize import path

* remove console logs

* decrease batch size to 1000

* add test for import path

* add test for already-online assets

* fix merge

* fix lint

* add library job back

* add offline job to correct queue

* library spec compiles now

* move one test to new e2e

* fix comments

* fix comments

* fix lint

* refactor path validation

* fix loop bug

* remove logging

* expect responses

* fix asset mock

* take the straightforward approach

* use generator correctly

* fix vitest on file edit

* bump vitest to 1.6.0

* test for offline check

* add e2e tests for offlining assets depending on import path

* cleanup e2e test after finish

* cleanup library service

* paginate the walk generator

* fix tests

* fix typo

* refactoring handleOfflineCheck

* better testing of handleOfflineCheck

* fix lint

* handle large library deletions

* dont check if library is deleted

* fix mock

* add a 100k page size to library

* fix loading animation

* better log messages

* Better logging for offline asset removal

* fix sql and tests

* fix number format

* Remove submodule

* fix format

* chore: cleanup

* chore: fix tests

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-28 13:05:48 -04:00
aviv926
5811025ebd docs: Documentation updates (#11516)
* Documentation updates

* PR feedback

* PR feedback

* Originally implemented using #11880

* add to FAQ

* Remove mTLS

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-28 16:43:51 +00:00
Weblate (bot)
7506eefee3 chore(web): update translations (#11758)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ta/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: - <Cenciak@users.noreply.hosted.weblate.org>
Co-authored-by: Adam <adammarzec2@protonmail.com>
Co-authored-by: Andreas Gammelgaard Damsbo <andreas@gdamsbo.dk>
Co-authored-by: André K <lordikohchang@gmail.com>
Co-authored-by: Bartłomiej Ruk <bartek04041993@gmail.com>
Co-authored-by: Ben <boiben609@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bilguun Ochirbat <bilguun0203@gmail.com>
Co-authored-by: Boštjan Kolar <bostjan.kolar@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Cohinem <twitch9ofe@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Erik Järlestrand <erik.jarlestrand@gmail.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Hozoy <Zrincet@gmail.com>
Co-authored-by: Ignas C <ignusiukas1@gmail.com>
Co-authored-by: Indrek Haav <IndrekHaav@users.noreply.hosted.weblate.org>
Co-authored-by: Indrek Haav <indrek.haav@hotmail.com>
Co-authored-by: JH <weblate@dm2.fi>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: José Rodrigues <j.rodrigues.pcmedic@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Karthik Raja K <2001.3.12kaarthik@gmail.com>
Co-authored-by: Kenneth <kenneth@flugheim.no>
Co-authored-by: Kristoffer Braa <kristoffer@lolandbraa.no>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Leo Bottaro <weblate@leobottaro.com>
Co-authored-by: Lukas Hamm <ideallygrey@tuta.io>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Marius Kavoliunas <kavoliunas.m@gmail.com>
Co-authored-by: Mateusz Kędziak <matizek43@gmail.com>
Co-authored-by: Matteo Morari <matteo.morari04@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: PUFF1N <frkmhyt@gmail.com>
Co-authored-by: Philipp66904 <philippg.pgb@gmail.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: René Dyhr <bazzo39@gmail.com>
Co-authored-by: Rui <rui-costa@users.noreply.hosted.weblate.org>
Co-authored-by: Runskrift <anders@rimfrost.nu>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: TheScientistPT <joao.ed.reis.gomes@gmail.com>
Co-authored-by: Tuomas Tornberg <tuomas.tornberg00@gmail.com>
Co-authored-by: Tyoda <tyoda@pm.me>
Co-authored-by: Ulices <hasecilu@tuta.io>
Co-authored-by: Unn Krigul <unn@arter.studio>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: Wolfgang Schweer <wschweer@arcor.de>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: fabianosan <fabianosan2006@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: lasharor <salih@ergezen.nl>
Co-authored-by: rbasliana <rbasliana@protonmail.com>
Co-authored-by: rondadon <hans.murks@protonmail.com>
Co-authored-by: tw-easy <sinale0611@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Оргил Пүрэвдорж <orgyldinio@proton.me>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-08-28 12:39:27 -04:00
Kenneth Bingham
2297d86569 fix(mobile): use a valid OAuth callback URL (#10832)
* add root resource path '/' to mobile oauth scheme

* chore: add oauth-callback path

* add root resource path '/' to mobile oauth scheme

* chore: add oauth-callback path

* fix: make sure there are three forward slash in callback URL

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-28 11:30:06 -05:00
renovate[bot]
cc4e5298ff fix(deps): update typescript-projects (#11927)
* fix(deps): update typescript-projects

* chore: clean up

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-28 12:00:10 -04:00
Zack Pollard
e705831e67 ci: fix permissions when pr-label-validation runs from fork (#12093) 2024-08-28 16:33:21 +01:00
Lena Tauchner
6867bae770 fix(cli): Update build instructions for CLI (#11874)
Update build instructions for CLI
2024-08-28 13:25:58 +00:00
Alex
c44280a50b chore(web): subtler spinner FOUC animation (#12090) 2024-08-28 08:20:56 -05:00
renovate[bot]
cf272fc7fd chore(deps): update terraform cloudflare to v4.40.0 (#11740)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 14:15:20 +01:00
renovate[bot]
365facfc51 chore(deps): update node (#12063)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 08:52:49 -04:00
renovate[bot]
d8aec81ae0 fix(deps): update dependency react-email to v3 (#12077)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 08:52:24 -04:00
renovate[bot]
1239066ada chore(deps): update base-image to v20240827 (major) (#12073)
chore(deps): update base-image to v20240827

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-28 08:51:02 -04:00
Alex
1fd00d8262 chore(web): resolve timeline flashing temporarily (#12088) 2024-08-27 22:31:32 -05:00
Matthew Momjian
d4cdd590bd docs: sql query for duplicate files (#12086) 2024-08-27 20:48:23 -04:00
Alex
be476d7982 chore(web): ensure goto is awaited for login page (#12087)
* chore(web): ensure goto is await for login page

* ensure server config is updated after onboarding is finished
2024-08-27 22:29:50 +00:00
Zack Pollard
028be6738e ci: use push-o-matic app for release process (#12075)
ci: use push-o-matic for release process
2024-08-27 23:19:04 +01:00
Ben
72ab664936 feat(web): announce notifications to screen readers (#12071) 2024-08-27 17:13:17 -05:00
renovate[bot]
98b3441cb1 chore(deps): update prom/prometheus docker digest to f663933 (#12072)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-27 18:08:01 -04:00
Jason Rasmussen
0be3c4472f refactor(server): event names (#12084) 2024-08-27 18:06:50 -04:00
Alex
aac6a4b052 chore(web): ignore shortcut toggle when entering email and password (#12082) 2024-08-27 16:50:25 -05:00
Matthew Momjian
16d5996f77 docs: external library deletion/edits (#12079)
* external lib

* edit 2

* Update FAQ.mdx

* fixes
2024-08-27 15:30:01 -04:00
Yuvraj P
3e970bc2d3 fix(mobile): Changes in the UI for the image editor pages (#12018)
* Ui enchancements and fixes

* Reruning the github review thing

* conflicts fix, apparently

* conflicts fix, apparently

* Fixed edit.page.dart

* Fixed crop page; localization etc

* Updated es-US.json; for Localization

* Formatting

* Changing the es-US.json back

* Update en-US.json

* localization

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-27 16:06:16 +00:00
Matthew Momjian
f70dcaa6cc docs: mTLS/self signed FAQ entry (#12074)
mTLS/self signed
2024-08-27 10:54:53 -05:00
Mark
b051b29eca feat(server): Storage template support album condition (#12000)
feat(server): Storage template support album condition ([Request](https://github.com/immich-app/immich/discussions/11999))
2024-08-26 20:48:39 -05:00
Ben
9894b9513b fix(web): shared link expiration date accessibility (#12060)
- use native select - shows focus, automatically has keyboard
  navigation, accessible for screen readers
- remove DropdownButton component
- fix dropdown styling in Safari
2024-08-26 21:05:23 -04:00
Alex
6b6d2a6621 feat(mobile): preserve mobile album info on upload (#11965)
* curating assets with albums to upload

* sorting for background backup

* background upload works

* transform fields string array to javascript array

* send json array

* generate sql

* refactor upload callback

* remove albums info from upload payload

* mechanism to create album on album selection

* album creation

* Sync to upload album

* Remove unused service

* unify name changes

* Add mechanism to sync uploaded assets to albums

* Put add to album operation after updating the UI state

* clean up

* background album sync

* add to album in background context

* remove add to album in callback

* refactor

* refactor

* refactor

* fix: make sure all selected albums are selected for building upload candidate

* clean up

* add manual sync button

* lint

* revert server changes

* pr feedback

* revert time filtering

* const

* sync album on manual upload

* linting

* pr feedback and proper time filtering

* wording
2024-08-26 13:21:19 -05:00
Alex
f4371578f5 fix(web): show supporter badge for account less than 14 days (#12058) 2024-08-26 17:20:50 +00:00
Alex
edf47dbbd0 feat(web): restore scroll position on navigating back to search page (#12042)
* feat(web): restore scroll position on navigating back to search page

* set 0 for scroll X

* lint

* simplify
2024-08-26 11:26:23 -05:00
Matt Tyree
3ac42edc74 docs: add Immich Kiosk and Immich Power Tools to Community Projects (#12055)
Add Immich Kiosk and Immich Power Tools

Added Immich Kiosk and Immich Power Tools to Community Projects
2024-08-26 16:06:21 +00:00
Carsten Otto
129e5eae66 fix: do not code format repro steps in issue template (#12054)
issue template: do not use "bash" to render a list of text items
2024-08-26 10:33:01 -05:00
Anil Madhavapeddy
fe672d4f35 feat(format): nrw format (#12048) 2024-08-26 08:16:24 -04:00
renovate[bot]
4f02412493 chore(deps): update dependency node to v20.17.0 (#12040)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-25 22:50:51 -04:00
Ben
96056208fc fix(web): announce current theme to screen reader users (#12039) 2024-08-25 18:50:54 -05:00
Min Idzelis
b2dd5a3152 feat: loading screen, initSDK on bootstrap, fix FOUC for theme (#10350)
* feat: loading screen, initSDK on bootstrap, fix FOUC for theme

* pulsate immich logo, don't set localstorage

* Make it spin

* Rework error handling a bit

* Cleanup

* fix test

* rename, memoize

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-25 17:34:08 -05:00
Alex
b653a20d15 fix(web): sort folders (#12038)
chore(web): sort folders
2024-08-25 16:53:14 -05:00
Thomas Clarke
868aedd212 fix: docs link to breaking changes (#12027)
Fix link to breaking changes
2024-08-25 12:54:12 -05:00
Alex
e457d8d62e chore(mobile): patch download > includeEmbeddedVideos user preferences (#11910)
* chore(mobile): patch download > includeEmbeddedVideos user preferences

* correct patch
2024-08-25 05:09:37 +00:00
Christopher Makarem
b41af65997 fix: align camera model drop down behavior with other drop downs on web and mobile (#11951)
* fix(web): align search filter behavior to show all camera models

* fix(mobile): align search filter behavior to clear camera model when make is set

* (mobile) correctly clear the model controller

* fix(mobile) re-add text controller to dropdown

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-25 05:00:15 +00:00
Snowknight26
7a4fccb1b2 fix(web): show a clearer confirmation message when deleting an unnamed album (#11988)
* fix(web): show a different confirmation message when deleting an unnamed album

* Rename the function

* Fix formatting
2024-08-24 23:59:18 -05:00
Yuvraj P
843345df4f fix(mobile): Fix for incorrectly naming edited files and structure change (#11741)
* Fix null name

* Fix null name and Fix button

* Remove extension correctly

* Refactoring the code and formatting

* formatting

* Fix for the extension name
2024-08-24 15:30:31 -05:00
renovate[bot]
00a7b80184 fix(deps): update machine-learning (#11921) 2024-08-24 17:50:05 +00:00
Daniel Dietzler
da12d5f567 feat(web): my immich shortcut (#12007)
feat: my immich shortcut in web
2024-08-23 23:03:36 +00:00
Ben
c14e2914f8 fix(web): rating stars accessibility (#11966)
* fix(web): exif ratings accessibility

* chore: add tests

* fix: eslint errors

* fix: clean up issues from changes in use:focusOutside
2024-08-23 12:34:12 -04:00
Jason Rasmussen
7fbf50a75e fix: remove asset.resized (#11983)
fix: remove resized
2024-08-22 23:24:49 -04:00
Jason Rasmussen
f69ce6ad8a refactor(web): folder view (#11967)
refactor(web): tree view
2024-08-22 11:38:19 -04:00
Carles Albàs Boix
296bbeb2fc feat(web): Left hand navigation for memories (#11913) 2024-08-22 14:40:15 +00:00
Jason Rasmussen
c24cc8a33b chore: ignore sql queries when building docker (#11933) 2024-08-22 11:48:31 +00:00
Min Idzelis
837b1e4929 feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position (#10646)
* Squashed

* Change strategy - now pre-measure buckets offscreen, so don't need to worry about sub-bucket scroll preservation

* Reduce jank on scroll, delay DOM updates until after scroll

* css opt, log measure time

* Trickle out queue while scrolling, flush when stopped

* yay

* Cleanup cleanup...

* everybody...

* everywhere...

* Clean up cleanup!

* Everybody do their share

* CLEANUP!

* package-lock ?

* dynamic measure, todo

* Fix web test

* type lint

* fix e2e

* e2e test

* Better scrollbar

* Tuning, and more tunables

* Tunable tweaks, more tunables

* Scrollbar dots and viewport events

* lint

* Tweaked tunnables, use requestIdleCallback for garbage tasks, bug fixes

* New tunables, and don't update url by default

* Bug fixes

* Bug fix, with debug

* Fix flickr, fix graybox bug, reduced debug

* Refactor/cleanup

* Fix

* naming

* Final cleanup

* review comment

* Forgot to update this after naming change

* scrubber works, with debug

* cleanup

* Rename scrollbar to scrubber

* rename  to

* left over rename and change to previous album bar

* bugfix addassets, comments

* missing destroy(), cleanup

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-21 21:15:21 -05:00
David Kerr
07538299cf feat: folder view (#11880)
* feat: folder view poc

* fix(folder-view): ui modifications

* fix(folder-view): improves utility return types

* fix(folder-view): update getAssetsByOriginalPath

Endpoint now only returns direct children of the path instead of all images in all subfolders.  Functions renamed and scoped to "folder", endpoints renamed

* fix(folder-view): improve typing

* fix(folder-view): replaces css with tailwind

* fix(folder-view): includes folders in main panel

* feat(folder-view): folder cache implementation

* fix(folder-view): can now search for absolute paths

* fix(folder-view): sets default sort to alphabetical by filename

* refactor/styling the browser view

* double click to navigate

* folder tree

* use correct side bar icon

* styling when selected

* correct open icon

* folder layout

* return assetReponseDto

* it's alive

* update new api

* more styling for folder tree

* use query params and path viewer

* use arrow up left for parent folder backward navigation

* use arrow up left for parent folder backward navigation

* encode URL

* handle long folder name

* refactor to the view controller

* remove unused code

* clear cache when logout

* cleaning up

* cleaning up web

* clean as new

* clean as new

* pr feedback + show asset name

* add tests

* add tests

* remove generated file

* lint

* revert docker-compose.dev file

* Update server/src/services/view.service.ts

Co-authored-by: Jason Rasmussen <jason@rasm.me>

* Update server/src/services/view.service.ts

Co-authored-by: Jason Rasmussen <jason@rasm.me>

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-21 13:49:37 -05:00
Ben
6cf5906813 docs: clarify external domain setting (#11958)
Added information about email notifications and advised users not to include a trailing slash.
2024-08-21 18:00:44 +00:00
Alex
817bd2ee94 fix(server): skip bad e2e test (#11957) 2024-08-21 13:57:37 -04:00
Jason Rasmussen
29d229c5ba fix(server): do not match live photos across libraries (#11952) 2024-08-20 21:23:50 -04:00
renovate[bot]
fd225e7462 chore(deps): update ghcr.io/immich-app/base-server-dev docker tag to v20240820 (#11941)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-20 11:34:43 -05:00
Jason Rasmussen
817f42aef7 fix(web): upload on file paste (#11922) 2024-08-20 08:25:26 -05:00
Jason Rasmussen
3be1aaaaa4 refactor(server): controller cleanup (#11923)
chore(server): controller cleanup
2024-08-20 12:50:14 +00:00
Jason Rasmussen
ef9a06be5c fix(server): album statistics endpoint (#11924) 2024-08-20 07:50:36 -04:00
Jason Rasmussen
cde0458dc8 fix(server): coverage reports (#11925) 2024-08-20 07:50:09 -04:00
Jason Rasmussen
8285803c95 refactor: access core (#11930) 2024-08-20 07:49:56 -04:00
Jason Rasmussen
c7801eae7e fix: random e2e test (#11932) 2024-08-20 07:49:35 -04:00
Jason Rasmussen
b60fa77846 fix: update renovate labels (#11931) 2024-08-20 10:33:43 +01:00
renovate[bot]
8d89eba3a9 fix(deps): update dependency exiftool-vendored to v28.2.1 (#11934)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-20 04:39:57 +00:00
renovate[bot]
2fba9f9547 chore(deps): update dependency @types/node to ^20.14.15 (#11920)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-20 00:30:28 -04:00
renovate[bot]
1d559431ba chore(deps): update grafana/grafana docker tag to v11.1.4 (#11912)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-19 23:35:17 -04:00
Jason Rasmussen
7af6733665 refactor(server): move files to separate table (#11861) 2024-08-19 20:03:33 -04:00
Alex
af3a793fe8 fix(server): create shared album from the mobile app does not trigger send email invite (#11911)
* fix(server): create shared album from the mobile app does not trigger send email invite

* remove unused value
2024-08-19 20:43:57 +00:00
Zack Pollard
2237b7a399 chore: validate every PR has a changelog related label (#11909) 2024-08-19 20:47:27 +01:00
Jason Rasmussen
d9698884bd refactor(server): track thumbnail jobs in job status table (#11908)
refactor: track thumbnail jobs in job status table
2024-08-19 13:50:00 -04:00
Jason Rasmussen
8338657eaa refactor(server): stacks (#11453)
* refactor: stacks

* mobile: get it built

* chore: feedback

* fix: sync and duplicates

* mobile: remove old stack reference

* chore: add primary asset id

* revert change to asset entity

* mobile: refactor mobile api

* mobile: sync stack info after creating stack

* mobile: update timeline after deleting stack

* server: update asset updatedAt when stack is deleted

* mobile: simplify action

* mobile: rename to match dto property

* fix: web test

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-19 12:37:15 -05:00
Carles Albàs Boix
ca52cbace1 feat(web): Left hand navigation with A/D (#11907) 2024-08-19 12:07:18 -05:00
Alex
bc31b7c06c feat(mobile): memories lane with the new CarouselView (#11892)
* feat(mobile): memories lane with the new CarouselView

* tuning

* tuning
2024-08-18 21:27:19 -05:00
immich-tofu[bot]
fa7f1e656f chore: modify .github/FUNDING.yml 2024-08-18 21:46:08 +00:00
Mert
036676d501 fix(ml): tokenization for webli models (#11881) 2024-08-18 11:05:10 -04:00
simkli
5ab92f346a feat(web): drag and drop or paste directories for upload (#11879)
feat(web): support for directories drag and drop

Allows directories to be drag and dropped or pasted for upload.
2024-08-18 09:38:21 -05:00
Snowknight26
bd42e05152 fix(web): correctly populate the camera model search dropdown (#11883) 2024-08-18 08:13:41 -04:00
Michel Heusschen
c9f1304bce fix(web): show camera make in search options after searching (#11884) 2024-08-18 08:12:10 -04:00
Michel Heusschen
5ef9a8ff8d feat(web): pasting coordinates (#11866) 2024-08-17 11:03:34 -04:00
Karthik Raja K
0261f79c72 fix(mobile): show correct notification icon for android (#11863) 2024-08-17 07:03:10 +00:00
Aaron Rodrigues
d61828598b fix(docs): spelling (#11859)
* Update requirements.md

* Update unraid.md

* Update shared-albums.md

* Update shared-albums.md

* Update unraid.md

* Update shared-albums.md
2024-08-16 23:14:53 -04:00
Aaron Rodrigues
e9bfe5418a docs: update mobile screenshot (#11851)
* Delete docs/docs/partials/img/sign-in-phone.jpeg

* Add files via upload

* chore: move image

* Delete docs/docs/partials/img/sign-in-phone.jpeg

* upload updated image

* Delete docs/docs/partials/img/sign-in-phone.jpeg

used the wrong image

* reupload with correct image

* Delete docs/docs/partials/img/sign-in-phone.jpeg

* reupload with correct img

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-16 18:46:29 +00:00
Jason Rasmussen
f230b3aa42 feat(server): granular permissions for api keys (#11824)
feat(server): api auth permissions
2024-08-16 09:48:43 -04:00
waclaw66
a372b56d44 fix(mobile): download translation (#11838)
fix: download translation
2024-08-16 08:19:05 -05:00
Saschl
1c754b60dc chore(mobile): only enable wakelock when backup is running (#11849)
chore: only enable wakelock when backup is running
2024-08-16 08:08:21 -05:00
Carsten Otto
c582a841ba fix(docs): read-only affects XMP writing (#11823)
* mention issue: read-only library vs XMP sidecars

* mention issue: read-only library vs XMP sidecars
chore: rename motionphotos to kebab-case and add new assets (#5)
2024-08-15 15:48:21 -05:00
Jason Rasmussen
433c7ab01d refactor: server emit events (#11780) 2024-08-15 20:12:41 +00:00
Jason Rasmussen
32c05ea950 feat(server): do not automatically download android motion videos (#11774)
feat(server): do not automatically download embedded android motion videos
2024-08-15 20:06:16 +00:00
Alex
ed6971222c chore(mobile): Flutter 3.24 (#11633)
* chore(mobile): Flutter 3.24

* fix lint

* fix rendering issues that lead to log get filled with error messages

* linting

* merge main

* fix isar prod build Android

* fix mismatch icon offset
2024-08-15 14:53:37 -05:00
Alex
00023e387f feat(mobile): enable Impeller rendering engine on Android (#11831) 2024-08-15 14:12:56 -05:00
Alex
e51b581f6e fix(mobile): correct native package naming convention (#11826) 2024-08-15 14:10:13 -05:00
Zack Pollard
f40a4fc1c8 fix(ml): broken openvino builds (#11818)
* fix: install opencl from github releases directly to pin versions

* chore: remove configuration-apt script
2024-08-15 13:27:18 -05:00
Alex
3ab7438036 chore(mobile): post release task (#11791) 2024-08-15 12:38:02 -05:00
Alex
49610de4b3 chore(mobile): update target SDK version (#11719)
* chore(mobile): update target SDK version

* background service

* remove print statements

* remove extra line

* format kotlin

* Correct permission
2024-08-15 11:36:43 -05:00
Jason Rasmussen
a4506758aa refactor: auth service (#11811) 2024-08-15 09:14:23 -04:00
Jason Rasmussen
b288241a5c refactor(server): enums (#11809) 2024-08-15 06:57:01 -04:00
Michel Heusschen
fa64277476 fix(web): focus trap inside portal (#11797)
* fix(web): focus trap inside portal

* fix tests
2024-08-15 04:36:29 -04:00
Alex The Bot
f7bfde6a32 Version v1.112.1 2024-08-15 00:00:22 +00:00
Alex
7d5f07d1c7 fix(mobile): android always prompts permission when accessing backup page (#11790)
Android always prompt permission
2024-08-14 18:55:52 -05:00
dependabot[bot]
a38dd53afd chore(deps): bump docker/build-push-action from 6.6.1 to 6.7.0 (#11768)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.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-08-14 18:23:43 -04:00
Jason Rasmussen
44c26c20b6 chore: update submodule (#11789) 2024-08-14 22:06:11 +00:00
Thariq Shanavas
fcec5f867c chore(docs): Encode db dump in UTF-8 for windows (#11787)
* Encode db dump in UTF-8 for windows

* Update backup-and-restore.md
2024-08-14 18:01:27 -04:00
Alex
7d888106ed fix(mobile): load original (#11786)
* fix(mobile): load original

* revert change to format
2024-08-14 14:52:19 -05:00
Alex
9e21f254cd chore(mobile): post release task (#11776) 2024-08-14 13:50:35 -05:00
Jason Rasmussen
da6f269008 refactor: asset e2e performance (#11779) 2024-08-14 14:42:33 -04:00
Alex The Bot
228a7710e6 Version v1.112.0 2024-08-14 15:51:18 +00:00
Alex
8014b0f86d chore(mobile): Translations update (#11771)
chore(mobile): translation update
2024-08-14 10:29:49 -05:00
Alex
fb962f49ea fix(ml): pydantic dep causes starting up issue (#11773)
* fix(ml): pydantic dep causes starting up issue

* revert import
2024-08-14 10:20:12 -05:00
ilyaChuk
7f7fec2cea feat(web): image editor - panel and cropping (#11074)
* cropping, panel

* fix presets

* types

* prettier

* fix lint

* fix aspect ratio, performance optimization

* improved tool selection, removed placeholder

* fix the mouse's exit from canvas

* fix error

* the "save" button and change tracking

* lint, format

* the mini functionality of the save button

* fix aspect ratio

* hide editor button on mobiles

* strict equality

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

* Use the dollar sign syntax for stores inside components

* unobtrusive grid lines, circles at the corners

* more correct image load, handleError

* more strict equality

* fix styles. unused and tailwind

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

* dont store isShowEditor

* if showEditor - hide navbar & shortcuts

* crop-canvas decomposition (danger)

I could have accidentally broken something.. but I checked the work and it seems ok.

* fix lint

* fix ts

* callback function as props

* correctly disabling shortcuts

* convenient canvas borders

• you can use the mouse to go beyond the boundaries and freely change the crop.
• the circles on the corners of the canvas are not cut off.

* -the editor button for video files, -save button

* hide editor btn if panoramic || gif || live

* corners instead of circles (preview), fix lint&format

* confirm close editor without save

* vertical aspect ratios

* recovery after merge. editor's closing shortcut

* fix format

* move from canvas to html elements

* fix changes detections

* rotation

* hide detail panel if showing editor

* fix aspect ratios near min size

* fix crop area when changing image size when rotate

* fix of fix

* better layout - grouping

https://github.com/user-attachments/assets/48f15172-9666-4588-acb6-3cb5eda873a8

* hide the button

* fix i18n, format

* hide button

* hide button v2

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-14 09:54:50 -05:00
Alex
593f036c0d fix(web): fallback aperture info when there is no locale set (#11770)
* fix(web): fallback aperture info when there is no locale set

* pr feedback
2024-08-14 15:52:44 +02:00
waclaw66
e934e368b3 fix(mobile): trash translations (#11761)
trash translations
2024-08-14 08:21:59 -05:00
renovate[bot]
f331a974ed chore(deps): update dependency @types/picomatch to v3.0.1 (#11755)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 23:06:46 -04:00
renovate[bot]
9d09b95618 chore(deps): update machine-learning (#11739)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 21:41:37 +00:00
Weblate (bot)
a8a63b24d0 chore(web): update translations (#11533)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: AMT AMT <altmimiamt@gmail.com>
Co-authored-by: Adam Uchmanowicz <auchmanowicz@gmail.com>
Co-authored-by: António Santos <antoniomsantos99@gmail.com>
Co-authored-by: Atakan Dulker <atakandulker@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Christoph Auer <Christoph.Auer@pilsheim.de>
Co-authored-by: Cristian Florin Tănase <crissssty@gmail.com>
Co-authored-by: Czerjak N <czerjaknorbert@gmail.com>
Co-authored-by: Dmitry <kittyfriend@mail.ru>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: ElTopo <cameos@gmail.com>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Felipe Silva <dorsal-cobweb-life@duck.com>
Co-authored-by: Fjuro <fjuro@alius.cz>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Furkan Yutup <furkanyutupre@gmail.com>
Co-authored-by: Hugo Cossard <hugococa2004@gmail.com>
Co-authored-by: Ionut <ionutp626@gmail.com>
Co-authored-by: Joachim Klahr <joachim@klahr.se>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Lars Bernstein <lb@setq.de>
Co-authored-by: Laurentiu <laurfb@gmail.com>
Co-authored-by: Lauritz Tieste <lauritz6000000@gmail.com>
Co-authored-by: Luna Kowalik <0skar16.contact@gmail.com>
Co-authored-by: MM <metalmario90@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Oliver Larsson <larsson.e.oliver@gmail.com>
Co-authored-by: Peder Kollenborg <pederkollenborg@gmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Pruthvi Bugidi <bps.21@proton.me>
Co-authored-by: Riccardo <lark-unit-rush@duck.com>
Co-authored-by: Rosu Iulian <rosuiulian@gmail.com>
Co-authored-by: Rıfat Dinç <rafidinc41@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Simmer Lajos <weblate.linguini033@passinbox.com>
Co-authored-by: Simon Zeeck Svärd <simon.svard100@gmail.com>
Co-authored-by: Stan P <g97d6liib@mozmail.com>
Co-authored-by: TheScientistPT <joao.ed.reis.gomes@gmail.com>
Co-authored-by: Tobias Frejo <tobiasfrejo@gmail.com>
Co-authored-by: Tom Niget <zippedfire@free.fr>
Co-authored-by: UTKARSH VISHNOI <utkarshvishnoi25@gmail.com>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Vincent Yeung <yeung_pok_yin_405060@yahoo.com.hk>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: Voinea Laurentiu Gabriel <gabivoinea29@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: anton <reallygud@protonmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dkorecko <reset259@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: jocxfin <joonatan@joonatanh.com>
Co-authored-by: manosrh <manosrh@gmail.com>
Co-authored-by: oopzzozzo <ek3ru8m4@gmail.com>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: sibber5 <ghasjado@gmail.com>
Co-authored-by: thestrudl <rok.vidmar1997@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Åke Amcoff <ake@amcoff.net>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-08-13 20:48:17 +00:00
Jason Rasmussen
ab0ed11778 chore: separate enhancement group in release notes (#11756) 2024-08-13 16:39:25 -04:00
Alex
5ec407b57c chore(mobile): properly patch openapi with custom response dto (#11753) 2024-08-13 14:39:25 -05:00
martin
fdf0b16fe3 feat(web): add privacy step in the onboarding (#11359)
* feat: add privacy step in the onboarding

* fix: remove console.log

* feat:Details the implications of enabling the map on the settings page

Added a link to the guide on customizing map styles as well

* feat: add map implication

* refactor: onboarding style

* fix: tile provider

* fix: remove long explanations

* chore: cleanup

---------

Co-authored-by: pcouy <contact@pierre-couy.dev>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 17:01:30 +00:00
Pierre Couy
c924f6c27c docs: update custom map style guide (#11350)
* docs:Reword "Custom Map Style" guide

- Split setting a style.json in Immich and creating a style with
  Maptiler
- Make it clearer that this is the way to change tile provider

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 16:05:36 +00:00
Carsten Otto
df45ef0e35 fix(server): follow symlinks when zipping assets (#11685)
* follow symlinks when zipping assets

fixes #9335

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-13 11:39:24 -04:00
renovate[bot]
81c813a882 chore(deps): update dependency tailwindcss to v3.4.9 (#11750)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 11:37:06 -04:00
Michel Heusschen
b014162088 refactor(web): add tailwind plugin for repeating grid cols (#11748) 2024-08-13 11:36:46 -04:00
Michel Heusschen
276101ee82 feat(web): improve shared link management on mobile (#11720)
* feat(web): improve shared link management on mobile

* fix format
2024-08-13 09:37:47 -05:00
renovate[bot]
9837d60074 chore(deps): update dependency vite-tsconfig-paths to v5 (#11746)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 08:40:22 -04:00
renovate[bot]
28b7443b92 chore(deps): update base-image to v20240813 (major) (#11747)
chore(deps): update base-image to v20240813

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 12:26:22 +00:00
Michel Heusschen
5acdc958b6 fix(web): single row of items (#11729)
* fix(web): single row of items

* remove filterBoxWidth

* slight size adjustment

* rewrite action as component
2024-08-13 08:20:08 -04:00
renovate[bot]
e384692025 chore(deps): update typescript-projects (#11743)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-13 08:17:17 -04:00
renovate[bot]
54b276c984 chore(deps): update dependency @types/node to ^20.14.14 (#11737)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-12 23:31:57 -04:00
Jason Rasmussen
7eb004bd00 chore: better release notes (#11726)
* chore: better release notes

* chore: remove 'tedious' commits
2024-08-12 14:49:07 -04:00
Michel Heusschen
c2965c4408 fix(web): detail panel out of sync when reopening (#11713)
* fix(web): detail panel out of sync when reopening

* extract event handler
2024-08-12 08:10:43 -04:00
Michel Heusschen
b749a68349 fix(web): hide import json button when using config file (#11714) 2024-08-12 07:40:31 -04:00
Michel Heusschen
30aa2c9b82 fix(web): use fallback image if shared asset isn't resized (#11704)
* fix(web): use fallback image if shared asset isn't resized

* remove test-data index file
2024-08-11 15:43:07 -04:00
Robert Schütz
9ed04588b8 chore(deps): update pydantic to v2 (#11701) 2024-08-11 12:23:11 -04:00
Michel Heusschen
7d320217b9 chore(web): remove unused file (#11696) 2024-08-11 08:01:37 -04:00
Michel Heusschen
efdf8bbca9 refactor(web): simplify some stores (#11695)
* refactor(web): simplify some stores

* make writable
2024-08-11 08:01:16 -04:00
Michel Heusschen
34c4fbf730 fix(web): asset viewer dynamic size (#11697) 2024-08-11 07:59:26 -04:00
Matthew Momjian
ca775ab3e9 docs: Update docs + example.env for DB_PASSWORD (#11678) 2024-08-09 21:36:32 +00:00
renovate[bot]
2dd5514043 chore(deps): update prom/prometheus docker digest to cafe963 (#11673)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-09 14:07:25 -04:00
Christoph Suter
f33dbdfe9a feat(web): add Exif-Rating (#11580)
* Add Exif-Rating

* Integrate star rating as own component

* Add e2e tests for rating and validation

* Rename component and async handleChangeRating

* Display rating can be enabled in app settings

* Correct i18n reference

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

* Star rating: change from slider to buttons

* Star rating for clarity

* Design updates.

* Renaming and code optimization

* chore: clean up

* chore: e2e formatting

* light mode border and default value

---------

Co-authored-by: Christoph Suter <christoph@suter-burri.ch>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-08-09 17:45:52 +00:00
Saschl
b1587a5dee feat(mobile): darken screen on backup page (#11623)
* feat: keep screen active on backup

* show dialog

* improve dialog and use shared timer

* get rid of confirmation dialog

* fix timer logic

* fix: set timeout to 60 seconds

* fix: revert unwanted change

* fix: properly hide status bar

* remove unwanted change

* fix: properly restore status bar when waking up

* clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-09 16:39:33 +00:00
Alex
501485d0b1 fix(mobile): incorrect remove action from the album assets detail view (#11671)
* fix(mobile): incorrect remove action from the album assets detail view

* better data structure
2024-08-09 09:51:08 -05:00
renovate[bot]
ed7f857975 chore(deps): update prom/prometheus docker digest to 497fe92 (#11669)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-09 10:00:50 -04:00
Alex
d346985457 chore(mobile): refactor detail panel (#11662)
* date time component

* rename to info_sheet

* simplify map info

* Edit datetime sheet

* fix janking when scroll on info sheet

* Location refactor

* refactor name

* Update date time after editing

* localize rebuild to smaller component

* restore advanced bottom sheet

* reassign EXIF back to local database

* remove print statements
2024-08-09 13:43:47 +00:00
bo0tzz
a144a1bec3 chore: add warning to media location env var (#11665) 2024-08-09 07:29:55 -04:00
Carsten Otto
9f318a9338 fix(docs): update documentation (#11655)
update documentation
2024-08-08 23:03:43 +00:00
Michel Heusschen
11f41099c3 chore(web): remove font-size of 17px (#11657) 2024-08-08 13:26:53 -05:00
Michel Heusschen
96481aae5d refactor(web): supporter badge (#11656)
* refactor(web): supporter badge

* add style lang
2024-08-08 14:02:44 -04:00
Michel Heusschen
4a42a72bd3 fix(server): use luxon for maxdate validator (#11651) 2024-08-08 09:02:39 -05:00
Michel Heusschen
66f2ac8ce3 fix(web): keep album description in sync (#11652) 2024-08-08 09:02:08 -05:00
dependabot[bot]
6b2de807a7 chore(deps): bump docker/build-push-action from 6.6.0 to 6.6.1 (#11646)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.0 to 6.6.1.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.6.0...v6.6.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-08-08 07:28:40 -04:00
Michel Heusschen
96f8050143 feat(web): improve group-tab accessibility (#11647)
feat(web): improve GroupTab accessibility
2024-08-08 07:28:24 -04:00
Zack Pollard
14689462f8 feat: change web asset detail map to zoom level 12.5 (#11643) 2024-08-07 23:38:02 +01:00
Matthew Mirvish
fb68da2b51 fix(server): avoid transcoding thumbnail streams (#11603)
Co-authored-by: mincrmatt12 <mincrmatt12@users.noreply.github.com>
2024-08-07 18:36:37 -04:00
Alex
720b9a286e chore(mobile): update other dependencies (#11641) 2024-08-07 14:09:56 -05:00
Alex
d93ccb1669 chore(mobile): update maplibre_gl dep (#11640) 2024-08-07 13:47:40 -05:00
Alex
c34fc4f2d1 fix(mobile): iOS crashing when download iCloud content (#11639) 2024-08-07 13:09:15 -05:00
Matthew Momjian
905a062a6e docs: how to decrease Redis logs (#11638) 2024-08-07 18:38:27 +01:00
renovate[bot]
aeed24b5b4 fix(deps): update typescript-projects (#11606)
* fix(deps): update typescript-projects

* fix: type error

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-08-07 15:45:30 +00:00
Johannes Groß
28ba22e8c1 fix(server): handle numeric 'Image Description' and 'Description' values (#11636)
* Made 'Image Description' and 'Description' type safe during exif parsing

* add test + update types

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-08-07 15:23:36 +00:00
Jason Rasmussen
5b64456f48 chore: more cursed knowledge (#11631)
* chore: more cursed knowledge

* chore: more cursed knowledge

* chore: rework footer
2024-08-07 09:54:57 -04:00
Jason Rasmussen
02fd6d22b3 chore: more cursed knowledge (#11630) 2024-08-07 12:36:30 +00:00
dependabot[bot]
10ed31d725 chore(deps): bump docker/build-push-action from 6.5.0 to 6.6.0 (#11629)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.5.0 to 6.6.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v6.5.0...v6.6.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-08-07 08:31:23 -04:00
Mert
23d4314eed chore(server): support pgvecto.rs 0.3.0 (#11624)
relax pgvecto.rs constraint
2024-08-06 23:04:55 -04:00
renovate[bot]
ea135cc310 chore(deps): update dependency @types/node to ^20.14.13 (#11604)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 22:59:26 -04:00
Saschl
745e1b003d feat(mobile): enable wakelock on backup page (#11621) 2024-08-06 17:13:11 -05:00
Alex
1dae622dbc chore(mobile): minor styling fix (#11619) 2024-08-06 14:39:07 -05:00
renovate[bot]
8ca24f0ef2 fix(deps): update dependency auto_route to v9 (#11566)
* fix(deps): update dependency auto_route to v9

* fix dep conflict

* linting

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-06 12:50:20 -05:00
renovate[bot]
f679021f0e fix(deps): update dependency share_plus to v10 (#11550)
* fix(deps): update dependency share_plus to v10

* resolve dep conflict

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-06 17:24:55 +00:00
i-am-a-teapot
65f5118bdd feat(web): Add stacking option to deduplication utilities (#11114)
* feat(web): Add stacking option to deduplication utilities

* Update web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte

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

* Fix prettier

* Draft for server side modifications. Endpoint for stacks (PUT,DELETE)

* Fix error

* Disable stakc button if less or more than one asset selected

* Remove unnecesarry log

* Revert to first commit

* Further Revert

* Actually Revert to Origin

* Only one stack button

* Update +page.svelte

* Fix optional arguments

* Fix Prettier

* Fix Linting

* Add stack information to asset view

* clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-06 17:06:30 +00:00
renovate[bot]
9f4fad2a0f chore(deps): update base-image to v20240806 (major) (#11616)
chore(deps): update base-image to v20240806

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-06 12:57:03 -04:00
Michel Heusschen
325fb4b5d1 fix(server): video duration extraction (#11610) 2024-08-06 11:27:05 -05:00
Alex
f040c9fb38 chore(server): remove get person asset limit (#11597)
* chore(server): remover get person asset limit

* sql

* remove getPersonAsset endpoint

* remove getPersonAsset endpoint

* use search endpoint to get people

* fix: server test

* mobile linter

* fix: server test

* remove debuglog

* deprecated endpoint

* change page size on mobile

* revert max size

* fix test
2024-08-06 16:22:13 +00:00
Pruthvi Bugidi
0eacdf93eb feat(mobile): add support for material themes (#11560)
* feat(mobile): add support for material themes

Added support for custom theming and updated all elements accordingly.

* fix(mobile): Restored immich brand colors to default theme

* fix(mobile): make ListTile titles bold in settings main page

* feat(mobile): update bottom nav and appbar colors

* small tweaks

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-08-06 14:20:27 +00:00
renovate[bot]
20262209ce fix(deps): update dependency setuptools to v70 [security] (#11609) 2024-08-06 10:09:38 -04:00
Michel Heusschen
dd638ac207 fix(web): slideshow on iphone (#11599)
* fix(web): slideshow on iphone

* make requestFullscreen type optional
2024-08-06 08:34:17 -05:00
Mert
d5b23373c7 refactor(server): startup checks for vector extension (#11559)
* update update logic

refactor

* update tests

* get version range through repo method, make tests more static

* move "should work" test
2024-08-05 21:00:25 -04:00
renovate[bot]
9765ccb5a7 chore(deps): update machine-learning (#11605)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-08-05 21:00:00 -04:00
renovate[bot]
82d934d09d chore(deps): update dependency eslint to v9 (#11601)
* chore(deps): update dependency eslint to v9

* chore: migrate to eslint flat config files

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-08-05 16:13:16 -04:00
renovate[bot]
2821e0bf95 chore(deps): update typescript-eslint monorepo to v8 (major) (#11598)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2024-08-05 19:13:00 +00:00
Michel Heusschen
bb3d9b6306 chore(web): remove unused event type definitions (#11600) 2024-08-05 14:50:48 -04:00
Alex
c83df2686a fix(mobile): autofill (#11591) 2024-08-05 12:02:31 -05:00
Jason Rasmussen
94da5942bd feat(web): open in map view (#11592) 2024-08-05 10:25:53 -05:00
Alex
54d2c12fff feat(docs): privacy policy (#11535) 2024-08-05 10:06:01 -05:00
foxit64
64fcb25971 fix: dockerfile linter error (#11590)
fix yamllint

Co-authored-by: sysadmin <sysadmin@localhost>
2024-08-05 09:02:02 -05:00
Jason Rasmussen
7f03bd8440 chore: dockerfile casing (#11589)
chore: docokerfile casing
2024-08-05 07:51:30 -05:00
Jason Rasmussen
2974cdbbee chore: dockerfile casing (#11588) 2024-08-05 12:07:28 +00:00
Yuvraj P
f0677735fd fix(mobile): Naming fix for the edited file (#11503) 2024-08-04 23:48:02 -05:00
Stefan Berggren
bb78eb4c4b Add Immich Distribution to Community Projects page (#11576)
Signed-off-by: Stefan Berggren <nsg@nsg.cc>
2024-08-05 03:36:55 +00:00
Mert
4ed75f2ac9 refactor(server): add config events for clip (#11575)
use config events for clip, add tests

formatting
2024-08-04 21:00:36 +00:00
Mert
3f4b783889 chore: add healthcheck field to server and ml (#11573)
add healthcheck field to server and ml
2024-08-04 13:37:43 -05:00
renovate[bot]
3968d76a57 fix(deps): update machine-learning (#11320) 2024-08-03 09:24:09 -04:00
Zack Pollard
55b31d1ce2 chore(web): fix weblate and other cleanup (#11532) 2024-08-02 13:35:47 +00:00
oidq
37cc6fbf27 fix(web): prevent change-location suggestion race-condition (#11523)
When debouncer activated on deletion, the handleSearchPlaces() function
would fire a request with empty query. UI would then show Immich API error.
2024-08-02 05:52:17 +00:00
Weblate (bot)
899b8a0ce7 chore(web): update translations (#11458)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
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: Atakan Dulker <atakandulker@gmail.com>
Co-authored-by: Czerjak N <czerjaknorbert@gmail.com>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: ElTopo <cameos@gmail.com>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Laurentiu <laurfb@gmail.com>
Co-authored-by: Luna Kowalik <0skar16.contact@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Vladimir Petrov (Vlado) <mr.vlado@gmail.com>
Co-authored-by: Voinea Laurentiu Gabriel <gabivoinea29@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dkorecko <reset259@gmail.com>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: oopzzozzo <ek3ru8m4@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-08-01 23:30:44 -04:00
Justin Forseth
d3a5490e71 feat(server): search unknown place (#10866)
* Allow submission of null country

* Update searchAssetBuilder to handle nulls

andWhere({country:null}) produces `"exifInfo"."country" = NULL`. We want
`"exifInfo"."country" IS NULL`, so we have to treat NULL as a special
case

* Allow null country in frontend

* Make the query code a bit more straightforward

* Remove unused brackets import

* Remove log message

* Don't change whitespace for no reason

* Fix prettier style issue

* Update search.dto.ts validators per @jrasm91's recommendation

* Update api types

* Combine null country and state into one guard clause

* chore: clean up

* chore: add e2e for null/empty city, state, country search

* refactor: server returns suggestion for null values

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-08-02 03:27:40 +00:00
Michel Heusschen
3afb5b497f fix(web): correctly format future timeline dates (#11506) 2024-08-01 07:39:26 -04:00
Michel Heusschen
1f0f880ecb fix(web): websocket over ipv6 (#11508) 2024-08-01 07:36:31 -04:00
martyfuhry
2c05ceaf50 fix(server): external domain url validation (#11493)
* fix(web): Changes externalDomain to IsUrl()

* refactor(web): asset viewer actions (#11449)

* refactor(web): asset viewer actions

* motion photo slot and more refactoring

fix(web): Changes externalDomain to IsUrl()

---------

Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
2024-07-31 14:09:30 -04:00
Yuvraj P
01f8b7e458 fix(mobile): Crop presets break crop rectangle #11462 (#11467)
Fix Issue 11464
2024-07-31 12:19:19 -05:00
Michel Heusschen
b73f7fe16f refactor: deduplicate MemoryType and ReactionType enums (#11479)
* refactor: deduplicate memorytype and reactiontype enums

* fix mobile
2024-07-31 12:08:31 -05:00
Michel Heusschen
281cfc95a4 refactor(web): asset viewer actions (#11449)
* refactor(web): asset viewer actions

* motion photo slot and more refactoring
2024-07-31 12:25:38 -04:00
renovate[bot]
3a3ea6135e chore(deps): update typescript-projects (#11437)
* chore(deps): update typescript-projects

* chore: formatting

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-07-31 15:40:23 +00:00
Jason Rasmussen
c44271e9b2 fix(deps): vitest@2 (#11491) 2024-07-31 11:26:35 -04:00
Jason Rasmussen
86904a8382 feat(web): more languages (#11488) 2024-07-31 10:26:17 -04:00
renovate[bot]
cf54829b3b chore(deps): update dependency eslint-plugin-unicorn to v55 (#11435)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 08:49:35 -04:00
dependabot[bot]
990627e00d chore(deps): bump stumpylog/image-cleaner-action from 0.7.0 to 0.8.0 (#11480)
Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/stumpylog/image-cleaner-action/releases)
- [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: stumpylog/image-cleaner-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-07-31 08:48:06 -04:00
Mert
41580696c7 feat(ml): add more search models (#11468)
* update export code

* add uuid glob, sort model names

* add new models to ml, sort names

* add new models to server, sort by dims and name

* typo in name

* update export dependencies

* onnx save function

* format
2024-07-31 04:34:45 +00:00
renovate[bot]
2423bb36c4 chore(deps): update grafana/grafana docker tag to v11.1.3 (#11451)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-31 00:09:13 -04:00
Ben McCann
82b899649d fix: make HTML valid (#11465) 2024-07-31 00:05:08 -04:00
Alex
8ee8450d18 chore(mobile): post release task (#11456) 2024-07-30 21:41:10 -05:00
dependabot[bot]
6d47d52b3c chore(deps): bump docker/setup-buildx-action from 3.5.0 to 3.6.1 (#11445)
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3.5.0 to 3.6.1.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3.5.0...v3.6.1)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-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-07-30 16:22:11 -04:00
Alex The Bot
919fd7d41f Version v1.111.0 2024-07-30 19:06:39 +00:00
Alex
c2fdb6aab8 chores(mobile): Translations update (#11454)
chore(mobile): translation update
2024-07-30 14:03:04 -05:00
Weblate (bot)
b6c4da37fd chore(web): update translations (#11429)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Atakan Dulker <atakandulker@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Florian Ostertag <florian.kuepper@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Maciek S <maslanypotwor1@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Ultragian <giancarlo.brasil@gmail.com>
Co-authored-by: Unimpeded Lemur <yg7lh0fz3@mozmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: tddaij <xdaint@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-30 14:01:42 -05:00
Alex
17c3e8e8bf fix(mobile): mobile logging out randomly (#11431)
* fix(mobile): refactor splash screen to not require online connection

* chore: bump flutter sdk path for vscode

* refactor: authentication provider always try network calls and only fail if 401 or no local user

* lint

* fix: revert change to lookup serverendpoint from store the isar store implementation is very broken

* fix: clear serverUrl and serverEndpoint on logout, and await logout call

* refactor: remove unneeded extra conditions in splash screen useEffect

* revert change to remove serverEndpoint on logging out

* pr feedback

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
2024-07-30 13:15:48 -05:00
renovate[bot]
21d3f248da chore(deps): update base-image to v20240730 (major) (#11447)
chore(deps): update base-image to v20240730

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-30 08:57:27 -04:00
renovate[bot]
a29660aae3 chore(deps): update dependency exiftool-vendored to v28 (#11440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-30 07:30:25 -04:00
renovate[bot]
6c81fa0f0a fix(deps): update dependency exiftool-vendored to v28.2.0 (#11439)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-30 07:29:31 -04:00
renovate[bot]
7156da502f chore(deps): update node.js to eb8101c (#11436)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 22:53:17 -04:00
Alex
13741410a7 chore(mobile): Add text to bottom gallery bar (#11417) 2024-07-29 21:25:04 -05:00
Matthew Momjian
3408e6b3cb docs: warning to not edit volumes in compose (#11432)
* Update docker-compose.yml

* Update docker-compose.yml

* Update docker-compose.yml
2024-07-29 21:24:47 -05:00
Michel Heusschen
434bcec5cc fix(server): correct person birth date across timezones (#11369)
* fix(server): correct person birth date across timezones

* fix test

* update e2e tests

* use Optional decorator
2024-07-29 19:52:04 -04:00
Jason Rasmussen
ebc71e428d feat(server): reverse geocoding endpoint (#11430)
* feat(server): reverse geocoding endpoint

* chore: rename error message
2024-07-29 18:17:26 -04:00
eleith
a70cd368af fix(server): use fqdn for og:image meta tag value (#11082)
* attempt to use fqdn for og:image

opengraph image specifies that the url contains http or https, thus
implying a fqdn.

this change uses the external domain from the server config to attempt
to make the og:image have both the existing path to the thumbnail along
with the desired domain

if the server setting is empty, the old behavior will persist

please note, some og implementations do work with relative paths, so not
all og image checkers may still pass, but not all implementations have
this fallback and thus will not find the image otherwise

* tests and ssr for og:image value as fqdn

* formatting

* fix test

* formatting

* formatting

* fix tests

getConfig was requiring authentication. using already initiated global stores instead

* load config in shared link service itself

* join host and pathname/params safely

* use origin instead of host for full domain string

also fixes lint and address the imageURL type which is optional

* chore: clean up

---------

Co-authored-by: eleith <eleith@lemon.localdomain>
Co-authored-by: eleith <online-github@eleith.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2024-07-29 21:38:47 +00:00
Jared L
3225e33fc1 feat(server): significantly improve Australian reverse geocoding accuracy (#11370)
chore(geocoding): ingest australia PPLXs
2024-07-29 10:59:53 -04:00
Weblate (bot)
85ab916ecf chore(web): update translations (#11416)
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/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
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: CraftWorks <weblate@craftworks.top>
Co-authored-by: Enoé Mugnaschi <enmuro@gmail.com>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: tddaij <xdaint@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-29 14:48:44 +00:00
Michel Heusschen
7445dad0dd fix(web): timeline group date formatting (#11392)
* fix(web): timeline group date formatting

* add isValid check

* remove duplicate type
2024-07-29 10:42:55 -04:00
Michel Heusschen
0237f9baa3 feat(web): more localized number formatting (#11401) 2024-07-29 10:38:27 -04:00
Michel Heusschen
2e059bfbfd fix(web): avoid nesting buttons inside links (#11425) 2024-07-29 10:36:10 -04:00
renovate[bot]
7bb7f63d57 chore(deps): update dependency node to v20.16.0 (#11421)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 10:33:01 -04:00
renovate[bot]
66a5a5718f chore(deps): update terraform cloudflare to v4.38.0 (#11423)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-29 10:32:27 -04:00
Alex
ddc4d2f927 fix(mobile): client TLS on ios (#11415) 2024-07-28 17:32:53 -05:00
Weblate (bot)
0beeb61f5c chore(web): update translations (#11365)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
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: AlrightIDidIt <fimofuni.igamunu@gotgel.org>
Co-authored-by: AxGD <guillermeaxel@yahoo.fr>
Co-authored-by: Bartłomiej Ruk <bartek04041993@gmail.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: ChoosenMEME <timjankowski259@gmail.com>
Co-authored-by: ConfusedAlex <alex@confusedalex.dev>
Co-authored-by: Coooolfan <coolfan1024@outlook.com>
Co-authored-by: Coxcopi70f00b67b61542fe <hn_vogel@gmx.net>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Eric Cornish <ao475129@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Gilgwath <gilgwath@protonmail.com>
Co-authored-by: Jakub <jakubula.jm@gmail.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: NikiTricky <niki.sto2010@gmail.com>
Co-authored-by: Sabin Oana <sabin.oana@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Victor Sueiro <kiwicaja@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: krzemyk <krzemyk.official@proton.me>
Co-authored-by: nazo6 <git@nazo6.dev>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: yusufbarisk <yusufbarisk2004@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-28 20:53:04 +00:00
waclaw66
a321db9f48 fix(web): translation leftovers (#11412)
fix: new album
2024-07-28 15:43:25 -05:00
Matthew Momjian
827136fc8b docs: file custom location (#11413)
* file custom location

* fix microservices
2024-07-28 15:43:09 -05:00
Matthew Momjian
088eea88e0 docs: how to change PG PW (#11414)
* guide to change PG PW

* fix
2024-07-28 15:42:42 -05:00
Yuvraj P
15503784c8 feat(mobile): adds crop and rotate to mobile (#10989)
* Added Crop Feature

* Using LayoutBuilder Fix

* Using Immich Colors

* Using Immich Text Theme

* Chnaging dynamic datatype to nullable

* Fix for the retrivel of the image from the cropscreen

* Using Hooks State

* Small edits

* Finals edits

* Saving to the mobile

* Commented final code

* Commented final code

* Comments and AutoRoute

* Fix AutoRoute Final

* Naming tools and Action when made no edits

* Updating timeline after edit

* chore: lint

* format

* Light Mode Compatible

* fix duplicate page name

* Fix Routing

* Hiding the Button

* lint

* remove unused code

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-28 15:41:14 -05:00
Jonathan Jogenfors
bc8e236598 chore(server): make vite-tsconfig-paths a dev dependency instead (#11404) 2024-07-27 21:50:35 +02:00
Michel Heusschen
909bd43e65 fix(web): slideshow settings title (#11396) 2024-07-27 10:46:19 -05:00
Alex
3330885bcc chore(server): email template minor styling (#11387) 2024-07-26 21:58:48 -05:00
Jan
e1ac73718c feat(web): Duplicate-Page shortcut changes (#11183)
* duplicate page assign other shortcut keys, add 'open image' shortcut

* add shortcut info page to duplicates with own list of keys

* edit translations, add translationkeys

* format fix

* remove typo

---------

Co-authored-by: Zack Pollard <zackpollard@ymail.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 21:47:51 +00:00
Ben
a78eeb9b9c feat(web): search bar keyboard accessibility (#11323)
* feat(web): search bar keyboard accessibility

* fix: adjust aria attributes

* fix: safari announcing the correct option count

* minor adjustments

- CircleIconButton disabled cursor
- more generic selection handler

* fix: more subtle border color in dark mode

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 16:45:15 -05:00
martin
86b3e3ee13 fix(web): responsive design when selecting assets in an album (#11169)
fix: responsive design when selecting assets in an album
2024-07-26 16:33:20 -05:00
waclaw66
4b2bc8e4ce fix(mobile): search filter translation + fixes (#11141)
translation + fixes
2024-07-26 16:32:19 -05:00
renovate[bot]
f92aee204e chore(deps): update dependency @types/picomatch to v3 (#11096)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:29:25 -05:00
renovate[bot]
7fd2b7965c chore(deps): update docker.io/redis:6.2-alpine docker digest to e3b17ba (#11302)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:28:34 -05:00
renovate[bot]
32ba6e3e3f chore(deps): update dependency byte-size to v9 (#11356)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 16:27:09 -05:00
Jonathan Jogenfors
0a6e5e0ec1 fix(server): make vitest pick up edited files (#11385)
fix vitest on file edit
2024-07-26 16:26:38 -05:00
Jonathan Jogenfors
65a4f86154 chore: bump vitest to 1.6.0 (#11386)
bump vitest to 1.6.0
2024-07-26 16:26:17 -05:00
ayykamp
147c6e3600 chore(web): improve responsiveness in Album and Shared Album pages on small devices (#11055)
* style: better responsiveness on album and shared album pages

* revert right margin changes

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-26 21:06:08 +00:00
Nicolò
ee6f1a010c chore(server): clean mail-templates and add tailwind style (#11296)
With this commit I wanted to complete the react-mail
 structure by properly define the templates styles by
 including tailwind css framework.

The framework is extended by both react-mail and
 tailwindcss-preset-email. Those packages help the rendering
 for various email clients.

If in future there is the necessity to target specific mail
 clients the package `tailwindcss-email-variants` and
 `tailwindcss-mso` can help too. The latter has some
 workarounds for the Ms Outlook that is still lacking
 a lot of the CSS3 funcitonality.
 to target

Signed-off-by: hitech95 <nicveronese@gmail.com>
2024-07-26 15:41:11 -05:00
renovate[bot]
a444ea7361 chore(deps): update dependency flutter to v3.22.3 (#11301)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-26 15:39:33 -05:00
Alex
59b809012f chore(mobile): post release task (#11382) 2024-07-26 15:38:41 -05:00
Ben
c037a8b8fa fix(web): easier alt text translation for other languages (#11124)
* fix(web): alt text translation for non-English languages

* fix: refactor to use full translation key names

* fix: calling the translation function directly
2024-07-26 13:48:40 -05:00
Michel Heusschen
ce15cf6065 fix(web): buy immich translations (#11379) 2024-07-26 13:41:59 -05:00
Alex The Bot
04340b3a62 Version v1.110.0 2024-07-26 15:38:20 +00:00
Alex
ef7a6bb246 chore(web): change license wording and other things (#11309) 2024-07-26 10:34:35 -05:00
Alex
bc20710c6d chore(mobile): Translations update (#11373)
chore(mobile): translation update
2024-07-26 10:31:10 -05:00
Zack Pollard
a63490a23b feat: use immich hosted map tiles (#11332) 2024-07-26 15:41:09 +01:00
Nicolò
a3799b3053 feat(server): add IP trust list for reverse proxy (#11286)
* feat(server): add IP trust list for reverse proxy

Signed-off-by: hitech95 <nicveronese@gmail.com>

* feat(docs): add documentation of `IMMICH_TRUSTED_PROXIES` env

Signed-off-by: hitech95 <nicveronese@gmail.com>

---------

Signed-off-by: hitech95 <nicveronese@gmail.com>
2024-07-26 09:23:58 -05:00
Yun Jiang
ea5d6780f2 feat(mobile): Adding setting in mobile app to TLS client certificate (#10860)
* feat(mobile): Adding setting in mobile app to import TLS client certificate and private key

* Formating dart source code to pass dart format test

* Adding missed required trailing commas to pass dart static analysis

* update lock file

* variable names

---------

Co-authored-by: Yun Jiang <yjiang@roku.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-07-26 13:59:02 +00:00
Saschl
62ac9bb7cd fix(mobile): native share functionality on iPad (#11294)
* pass context to share method

* use correct context

* fix: multiselection and logs sharing

* fix: lint
2024-07-26 08:43:59 -05:00
Michel Heusschen
86a658b891 fix(mobile): negative coordinate input (#11292)
* fix(mobile): negative coordinate input

* format
2024-07-26 08:37:29 -05:00
aviv926
536628ad95 docs: Add missing info to asset types and storage locations (#11358)
first
2024-07-26 08:34:36 -05:00
Jordy
2c7db0122d fix(mobile): changed "x jaren" to "x jaar" in dutch app translations (#11371)
changed "x jaren" to "x jaar"
2024-07-26 08:29:59 -05:00
Stephen Smith
ade2901259 feat(server): Allow activating non-admin user with server license (#11206)
* feat(server): allow server license to activate a user

* feat(web): send server+client licenses to user activation when non-admin

* chore(server): update test to allow server license to activate user

* fix(web): correctly load user to determine where to save license
2024-07-25 23:27:44 -05:00
imakida
d180373ec1 fix: "acess" should be "access" (#11363) 2024-07-26 03:36:01 +00:00
Weblate (bot)
c2a65d8fac chore(web): update translations (#11165)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Alex <alex@guldager.one>
Co-authored-by: Alexandr Zhytnyk <oper.kh@gmail.com>
Co-authored-by: AlrightIDidIt <fimofuni.igamunu@gotgel.org>
Co-authored-by: António Santos <antoniomsantos99@gmail.com>
Co-authored-by: Arkady Titenko <pgp-noreply@rkd.dev>
Co-authored-by: Aurora <arci@anche.no>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: CanbiZ <mickey.leskowitz@gmail.com>
Co-authored-by: Coooolfan <coolfan1024@outlook.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Digital <github@crni.xyz>
Co-authored-by: Eero Jääskeläinen <eero.jaaskelainen@gmail.com>
Co-authored-by: Emerson Guimaraes <emersonrosa13@proton.me>
Co-authored-by: Filip Bredborg <fbredborg@gmail.com>
Co-authored-by: Fredrik Ekdahl <fekdahl@gmail.com>
Co-authored-by: Jaksa <jaks@hotmail.de>
Co-authored-by: Joar von Arndt <joarxpablo@gmail.com>
Co-authored-by: Julien SORIN <julien.sorin@hotmail.fr>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Lauritz Tieste <lauritz6000000@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Lukas Miskovsky <miskovskylukas@gmail.com>
Co-authored-by: MATTENN <at.mattenn@gmail.com>
Co-authored-by: Majid <abtin.php@gmail.com>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Mansour Javaher <info@mansour.co.nz>
Co-authored-by: Marko <anony253@live.com>
Co-authored-by: Martin Dechev <dechev86@gmail.com>
Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com>
Co-authored-by: Mike <mike.tgv@gmail.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Mingyu Kim <mingyu@mingyu.co.kr>
Co-authored-by: Oton <oms.moreira@outlook.com>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pheggas <petko252@gmail.com>
Co-authored-by: PolarisYHNL <polarisyhnl@yeah.net>
Co-authored-by: Ponas <le.slab124@aleeas.com>
Co-authored-by: Quang Dang <dangminhquang.r@gmail.com>
Co-authored-by: RaduTek <radutux13@gmail.com>
Co-authored-by: Riku Viitanen <riku.viitanen@protonmail.com>
Co-authored-by: Rolando Grave <roland@graved.ch>
Co-authored-by: Rookie Nguyễn <nguyenquocthang2004@gmail.com>
Co-authored-by: Sam Smith <ja49619@gmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Sophie <mail@sopht.li>
Co-authored-by: Thế Anh Hoàng <the.anh.ls@gmail.com>
Co-authored-by: Timothy <timothy@benker.cc>
Co-authored-by: Varga Bence Levente <varga.bence.levente@protonmail.com>
Co-authored-by: Vikram Pratap Singh <vicky18189@gmail.com>
Co-authored-by: Vincenzo Nunziata <vinciosdev@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: Yusuf Kenan Demiray <yusken2009@gmail.com>
Co-authored-by: aarhor <aaron.horstmann9916@gmail.com>
Co-authored-by: blomusti <m.f.varkara@gmail.com>
Co-authored-by: chapvic <victor@chapaev.org>
Co-authored-by: dvbthien <dvbthien@dvbthien.onmicrosoft.com>
Co-authored-by: eav5jhl0 <eav5jhl0@users.noreply.hosted.weblate.org>
Co-authored-by: fenix_vd <mrfenixvd@yandex.ru>
Co-authored-by: fuzfyy <egeozce35@gmail.com>
Co-authored-by: gallegonovato <fran-carro@hotmail.es>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: mitakskia <spammitakskia@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Àlex Bravo <alexbravobosch@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: 李奕寯 <eugenelego88@gmail.com>
2024-07-25 19:28:48 -04:00
Michel Heusschen
8e6bc13540 feat: people infinite scroll (#11326)
* feat: people infinite scroll

* add infinite scroll to show & hide modal

* update unit tests

* show total people count instead of currently loaded

* update personsearchdto
2024-07-25 15:59:28 -04:00
renovate[bot]
152421e288 chore(deps): update redis:6.2-alpine docker digest to e3b17ba (#11303)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-25 15:20:11 -04:00
aviv926
72a8bbb874 docs: add product key to roadmap (#11351)
* add license to roadmap

* fix

* Supporter Badge

* fix
2024-07-25 19:16:56 +00:00
Zack Pollard
b8d2d38bd1 chore(docs): compress homepage screenshots, 10x smaller (#11347) 2024-07-25 14:50:00 +01:00
Jason Rasmussen
9f6ef92f0b fix(deps): exiftool-vendored (#11338) 2024-07-24 17:38:22 -04:00
renovate[bot]
9e60c107ca chore(deps): update node (#11322)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-24 16:13:24 -04:00
renovate[bot]
2179f83d63 chore(deps): update machine-learning (#11310)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-23 12:53:47 -04:00
renovate[bot]
b259095899 chore(deps): update node (#11300)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-23 08:22:15 -04:00
renovate[bot]
145ace0fa1 chore(deps): update base-image to v20240723 (major) (#11311)
chore(deps): update base-image to v20240723

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-23 08:21:35 -04:00
Michel Heusschen
7d3db11a5c feat(web): coordinate input for asset location (#11291) 2024-07-23 08:01:10 -04:00
Michel Heusschen
8725656fd2 fix(server): DateTimeOriginal overwrite issue with sidecar file (#11306)
* fix(server): DateTimeOriginal overwrite issue with sidecar file

* update unit test
2024-07-23 07:59:46 -04:00
renovate[bot]
6394b4a9a3 chore(deps): update machine-learning (#11299)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-22 22:05:16 -04:00
Nikhil Taneja
d0b3dd888b docs: corrected container port for immich_microservices (#11170) 2024-07-22 13:55:59 +00:00
Michel Heusschen
849bc6e3aa fix(server): correct openapi response type for getServerLicense() (#11261)
* fix(server): correct openapi response type for getServerLicense()

* return 404 error when license doesn't exist

* update e2e test
2024-07-22 08:50:45 -05:00
dependabot[bot]
3d7a9d79da chore(deps): bump docker/build-push-action from 6.3.0 to 6.5.0 (#11282)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 14:45:25 +01:00
dependabot[bot]
f7cc9517ba chore(deps): bump docker/setup-qemu-action from 3.1.0 to 3.2.0 (#11283)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 14:45:14 +01:00
dependabot[bot]
73305feb5b chore(deps): bump docker/setup-buildx-action from 3.4.0 to 3.5.0 (#11284)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-22 14:45:00 +01:00
Michel Heusschen
950cd5d996 fix(web): use fixed position for download and upload panel (#11279) 2024-07-22 08:40:43 -04:00
renovate[bot]
b53bd8c525 fix(deps): update machine-learning (#10740)
* fix(deps): update machine-learning

* update openvino options, cuda

* update openvino build

* fix indentation

* update minimum nvidia driver

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2024-07-21 19:30:24 -04:00
Michel Heusschen
8b773a2b2e fix(server): exif description whitespace handling (#11249)
* fix(server): exif description whitespace handling

* remove trim optional chaining
2024-07-21 19:01:14 -04:00
Daniel Dietzler
1e8806854d docs: 40k stars! (#11265)
40k stars!
2024-07-21 16:19:17 -05:00
Mert
9d2d556200 feat(server): accepted video containers (#11274)
* add accepted container config

* update api

* mp4 option makes no sense

* add to transcoding settings

* wording

* updated spec config

* formatting
2024-07-21 21:14:23 +00:00
Daniel Dietzler
7ecdcb3bc0 fix(server): static mail attachment extension (#11254)
* fix: static file extension

* chore: unit tests
2024-07-20 19:00:46 -04:00
Fynn Petersen-Frey
54488b1016 feat(ml): improved ARM-NN support (#11233) 2024-07-20 15:59:27 -04:00
Alex
7c3326b662 chore(mobile): post release task (#11220) 2024-07-19 15:10:29 +00:00
Fynn Petersen-Frey
745b16e4b4 feat(mobile): remove asset from album in gallery view (#11184)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2024-07-19 10:07:36 -05:00
Alex
a469fe44a1 chore(docs): change some wording (#11201) 2024-07-18 16:52:45 -04:00
Alex The Bot
b9fc59ca9f Version v1.109.2 2024-07-18 19:33:29 +00:00
Alex
e005a123ba fix(web): user can remove server license (#11199) 2024-07-18 14:26:54 -05:00
renovate[bot]
cd63212118 chore(deps): update base-image to v20240718 (major) (#11194)
chore(deps): update base-image to v20240718

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-07-18 13:58:17 -05:00
Michel Heusschen
a9dd013daf fix(web): hide license popup after mouse leave (#11193) 2024-07-18 13:13:45 -05:00
Alex The Bot
01ba859567 Version v1.109.1 2024-07-18 17:55:58 +00:00
Mert
173c9070c8 fix(ml): re-add worker env (#11192)
re-add worker env
2024-07-18 17:50:52 +00:00
Saschl
d37e8ede3b feat: optionally generate thumbnails for invalid images (#11126) 2024-07-18 12:07:22 -04:00
1039 changed files with 66018 additions and 35769 deletions

View File

@@ -22,6 +22,7 @@ open-api/typescript-sdk/node_modules/
server/coverage/
server/node_modules/
server/upload/
server/src/queries
server/dist/
server/www/

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
custom: ['https://buy.immich.app']

View File

@@ -83,7 +83,6 @@ body:
2.
3.
...
render: bash
validations:
required: true

40
.github/release.yml vendored
View File

@@ -1,41 +1,29 @@
changelog:
categories:
- title: ⚠️ Breaking Changes
- title: 🚨 Breaking Changes
labels:
- breaking-change
- changelog:breaking-change
- title: 🗄️ Server
- title: 🔒 Security
labels:
- 🗄server
- changelog:security
- title: 📱 Mobile
- title: 🚀 Features
labels:
- 📱mobile
- changelog:feature
- title: 🖥️ Web
- title: 🌟 Enhancements
labels:
- 🖥web
- changelog:enhancement
- title: 🧠 Machine Learning
- title: 🐛 Bug fixes
labels:
- 🧠machine-learning
- changelog:bugfix
- title: ⚡ CLI
- title: 📚 Documentation
labels:
- cli
- changelog:documentation
- title: 📓 Documentation
- title: 🌐 Translations
labels:
- documentation
- title: 🔨 Maintenance
labels:
- deployment
- dependencies
- renovate
- maintenance
- tech-debt
- title: Other changes
labels:
- "*"
- changelog:translation

View File

@@ -16,10 +16,28 @@ concurrency:
cancel-in-progress: true
jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- id: found_paths
uses: dorny/paths-filter@v3
with:
filters: |
mobile:
- 'mobile/**'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
build-sign-android:
name: Build and sign Android
needs: pre-job
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
runs-on: macos-14
steps:

View File

@@ -22,7 +22,7 @@ permissions:
jobs:
publish:
name: Publish
name: CLI Publish
runs-on: ubuntu-latest
defaults:
run:
@@ -56,10 +56,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
uses: docker/setup-buildx-action@v3.6.1
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v6.3.0
uses: docker/build-push-action@v6.7.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.7.0
uses: stumpylog/image-cleaner-action/untagged@v0.8.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -17,56 +17,67 @@ permissions:
packages: write
jobs:
build_and_push:
name: Build and Push
pre-job:
runs-on: ubuntu-latest
outputs:
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- id: found_paths
uses: dorny/paths-filter@v3
with:
filters: |
server:
- 'server/**'
- 'openapi/**'
- 'web/**'
machine-learning:
- 'machine-learning/**'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
build_and_push_ml:
name: Build and Push ML
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ubuntu-latest
env:
image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/amd64,linux/arm64
- platforms: linux/amd64,linux/arm64
device: cpu
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/amd64
- platforms: linux/amd64
device: cuda
suffix: -cuda
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/amd64
- platforms: linux/amd64
device: openvino
suffix: -openvino
- image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
platforms: linux/arm64
- platforms: linux/arm64
device: armnn
suffix: -armnn
- image: immich-server
context: .
file: server/Dockerfile
platforms: linux/amd64,linux/arm64
device: cpu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.1.0
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.4.0
uses: docker/setup-buildx-action@v3.6.1
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
@@ -93,8 +104,8 @@ jobs:
# Disable latest tag
latest=false
images: |
name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
@@ -111,18 +122,18 @@ jobs:
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v6.3.0
uses: docker/build-push-action@v6.7.0
with:
context: ${{ matrix.context }}
file: ${{ matrix.file }}
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
@@ -132,3 +143,107 @@ jobs:
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
build_and_push_server:
name: Build and Push Server
runs-on: ubuntu-latest
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
env:
image: immich-server
context: .
file: server/Dockerfile
strategy:
fail-fast: false
matrix:
include:
- platforms: linux/amd64,linux/arm64
device: cpu
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.6.1
- name: Login to Docker Hub
# Only push to Docker Hub when making a release
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Generate docker image tags
id: metadata
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
# Tag with pr-number
type=ref,event=pr,suffix=${{ matrix.suffix }}
# Tag with git tag on release
type=ref,event=tag,suffix=${{ matrix.suffix }}
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
- name: Determine build cache output
id: cache-target
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v6.7.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
success-check:
name: Docker Build & Push Success
needs: [build_and_push_ml, build_and_push_server]
runs-on: ubuntu-latest
if: always()
steps:
- name: Any jobs failed?
if: ${{ contains(needs.*.result, 'failure') }}
run: exit 1
- name: All jobs passed or skipped
if: ${{ !(contains(needs.*.result, 'failure')) }}
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"

View File

@@ -2,12 +2,8 @@ name: Docs build
on:
push:
branches: [main]
paths:
- "docs/**"
pull_request:
branches: [main]
paths:
- "docs/**"
release:
types: [published]
@@ -16,7 +12,27 @@ concurrency:
cancel-in-progress: true
jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- id: found_paths
uses: dorny/paths-filter@v3
with:
filters: |
docs:
- 'docs/**'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
build:
name: Docs Build
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
defaults:
run:

View File

@@ -7,13 +7,32 @@ on:
jobs:
checks:
name: Docs Deploy Checks
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow failed' && exit 1
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact
id: get-artifact
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
if (!matchArtifact) {
console.log("No artifact found with the name docs-build-output, build job was skipped")
return { found: false };
}
return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@v7
@@ -73,9 +92,10 @@ jobs:
return parameters;
deploy:
name: Docs Deploy
runs-on: ubuntu-latest
needs: checks
if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -98,18 +118,11 @@ jobs:
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
let artifact = ${{ needs.checks.outputs.artifact }};
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
artifact_id: artifact.id,
archive_format: 'zip',
});
let fs = require('fs');

View File

@@ -5,6 +5,7 @@ on:
jobs:
deploy:
name: Docs Destroy
runs-on: ubuntu-latest
steps:
- name: Checkout code

View File

@@ -0,0 +1,21 @@
name: PR Label Validation
on:
pull_request_target:
types: [opened, labeled, unlabeled, synchronize]
jobs:
validate-release-label:
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
steps:
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
use_regex: true
labels: "changelog:.*"
add_comment: true

View File

@@ -29,10 +29,17 @@ jobs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.ORG_RELEASE_TOKEN }}
token: ${{ steps.generate-token.outputs.token }}
- name: Install Poetry
run: pipx install poetry
@@ -44,10 +51,8 @@ jobs:
id: push-tag
uses: EndBug/add-and-commit@v9
with:
author_name: Alex The Bot
author_email: alex.tran1502@gmail.com
default_author: user_info
message: 'Version ${{ env.IMMICH_VERSION }}'
default_author: github_actions
message: 'chore: version ${{ env.IMMICH_VERSION }}'
tag: ${{ env.IMMICH_VERSION }}
push: true

View File

@@ -10,8 +10,27 @@ concurrency:
cancel-in-progress: true
jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- id: found_paths
uses: dorny/paths-filter@v3
with:
filters: |
mobile:
- 'mobile/**'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
mobile-dart-analyze:
name: Run Dart Code Analysis
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest

View File

@@ -10,8 +10,47 @@ concurrency:
cancel-in-progress: true
jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_mobile: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- id: found_paths
uses: dorny/paths-filter@v3
with:
filters: |
web:
- 'web/**'
- 'open-api/typescript-sdk/**'
server:
- 'server/**'
cli:
- 'cli/**'
- 'open-api/typescript-sdk/**'
e2e:
- 'e2e/**'
mobile:
- 'mobile/**'
machine-learning:
- 'machine-learning/**'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
server-unit-tests:
name: Server
name: Test & Lint Server
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -46,7 +85,9 @@ jobs:
if: ${{ !cancelled() }}
cli-unit-tests:
name: CLI
name: Unit Test CLI
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -85,7 +126,9 @@ jobs:
if: ${{ !cancelled() }}
cli-unit-tests-win:
name: CLI (Windows)
name: Unit Test CLI (Windows)
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: windows-latest
defaults:
run:
@@ -117,7 +160,9 @@ jobs:
if: ${{ !cancelled() }}
web-unit-tests:
name: Web
name: Test & Lint Web
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -159,13 +204,54 @@ jobs:
run: npm run test:cov
if: ${{ !cancelled() }}
e2e-tests:
name: End-to-End Tests
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './e2e/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
if: ${{ !cancelled() }}
- name: Run linter
run: npm run lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
if: ${{ !cancelled() }}
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
runs-on: mich
defaults:
run:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -191,16 +277,41 @@ jobs:
run: npm ci
if: ${{ !cancelled() }}
- name: Run linter
run: npm run lint
- name: Docker build
run: docker compose build
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
- name: Run e2e tests (api & cli)
run: npm run test
if: ${{ !cancelled() }}
- name: Run tsc
run: npm run check
e2e-tests-web:
name: End-to-End Tests (Web)
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
runs-on: mich
defaults:
run:
working-directory: ./e2e
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: 'recursive'
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version-file: './e2e/.nvmrc'
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
@@ -211,16 +322,14 @@ jobs:
run: docker compose build
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
run: npm run test
if: ${{ !cancelled() }}
- name: Run e2e tests (web)
run: npx playwright test
if: ${{ !cancelled() }}
mobile-unit-tests:
name: Mobile
name: Unit Test Mobile
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -234,7 +343,9 @@ jobs:
run: flutter test -j 1
ml-unit-tests:
name: Machine Learning
name: Unit Test ML
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ubuntu-latest
defaults:
run:

2
.gitmodules vendored
View File

@@ -1,6 +1,6 @@
[submodule "mobile/.isar"]
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "server/test/assets"]
[submodule "e2e/test-assets"]
path = e2e/test-assets
url = https://github.com/immich-app/test-assets

View File

@@ -1 +0,0 @@
/dist

View File

@@ -1,28 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
root: true,
env: {
node: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
},
};

View File

@@ -1 +1 @@
20.15.1
20.17.0

View File

@@ -1,4 +1,4 @@
FROM node:20.15.1-alpine3.20@sha256:34b7aa411056c85dbf71d240d26516949b3f72b318d796c26b57caaa1df5639a as core
FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

View File

@@ -4,8 +4,18 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
# For developers
Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
$ npm install
$ npm run build
Then, to build the open-api client run the following in the open-api folder:
$ ./bin/generate-open-api.sh
To run the Immich CLI from source, run the following in the cli folder:
$ npm install
$ npm run build
$ ts-node .
@@ -17,3 +27,4 @@ You can also build and install the CLI using
$ npm run build
$ npm install -g .
****

61
cli/eslint.config.mjs Normal file
View File

@@ -0,0 +1,61 @@
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ['eslint.config.mjs', 'dist'],
},
...compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:unicorn/recommended',
),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
},
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/prevent-abbreviations': 'off',
'unicorn/no-process-exit': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
'object-shorthand': ['error', 'always'],
},
},
];

1813
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.2.9",
"version": "2.2.18",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -13,30 +13,33 @@
"cli"
],
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^20.14.10",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^7.0.0",
"@vitest/coverage-v8": "^1.2.2",
"byte-size": "^8.1.1",
"@types/node": "^20.16.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"eslint": "^8.56.0",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^54.0.0",
"eslint-plugin-unicorn": "^55.0.0",
"globals": "^15.9.0",
"mock-fs": "^5.2.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite-tsconfig-paths": "^4.3.2",
"vitest": "^1.2.2",
"vitest-fetch-mock": "^0.2.2",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5",
"vitest-fetch-mock": "^0.3.0",
"yaml": "^2.3.1"
},
"scripts": {
@@ -64,6 +67,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.15.1"
"node": "20.17.0"
}
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.37.0"
constraints = "4.37.0"
version = "4.40.0"
constraints = "4.40.0"
hashes = [
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=",
"h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=",
"h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=",
"h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=",
"h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=",
"h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=",
"h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=",
"h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=",
"h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=",
"h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=",
"h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=",
"h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=",
"h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=",
"h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=",
"zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd",
"zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f",
"zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396",
"zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb",
"zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c",
"zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2",
"zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce",
"zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
"zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f",
"zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d",
"zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62",
"zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e",
"zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6",
"zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e",
]
}

View File

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

View File

@@ -9,6 +9,6 @@ resource "cloudflare_record" "immich_app_release_domain" {
proxied = true
ttl = 1
type = "CNAME"
value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
content = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.37.0"
constraints = "4.37.0"
version = "4.40.0"
constraints = "4.40.0"
hashes = [
"h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=",
"h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=",
"h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=",
"h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=",
"h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=",
"h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=",
"h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=",
"h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=",
"h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=",
"h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=",
"h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=",
"h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=",
"h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=",
"h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=",
"zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4",
"zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d",
"zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3",
"zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1",
"zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9",
"zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710",
"zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b",
"zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e",
"zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c",
"h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=",
"h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=",
"h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=",
"h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=",
"h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=",
"h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=",
"h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=",
"h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=",
"h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=",
"h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=",
"h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=",
"h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=",
"h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=",
"h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=",
"zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd",
"zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f",
"zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396",
"zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb",
"zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c",
"zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2",
"zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce",
"zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8",
"zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107",
"zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f",
"zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5",
"zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369",
"zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f",
"zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d",
"zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62",
"zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e",
"zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6",
"zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e",
]
}

View File

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

View File

@@ -9,7 +9,7 @@ resource "cloudflare_record" "immich_app_branch_subdomain" {
proxied = true
ttl = 1
type = "CNAME"
value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
content = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}

View File

@@ -46,6 +46,8 @@ services:
depends_on:
- redis
- database
healthcheck:
disable: false
immich-web:
container_name: immich_web
@@ -91,10 +93,12 @@ services:
depends_on:
- database
restart: unless-stopped
healthcheck:
disable: false
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -21,6 +21,8 @@ services:
- redis
- database
restart: always
healthcheck:
disable: false
immich-machine-learning:
container_name: immich_machine_learning
@@ -33,15 +35,19 @@ services:
dockerfile: Dockerfile
args:
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
ports:
- 3003:3003
volumes:
- model-cache:/cache
env_file:
- .env
restart: always
healthcheck:
disable: false
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -65,7 +71,7 @@ services:
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
# set IMMICH_METRICS=true in .env to enable metrics
@@ -73,7 +79,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:f20d3127bf2876f4a1df76246fca576b41ddf1125ed1c546fbd8b16ea55117e6
image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -85,7 +91,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107
image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -16,6 +16,7 @@ services:
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
volumes:
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
env_file:
@@ -26,6 +27,8 @@ services:
- redis
- database
restart: always
healthcheck:
disable: false
immich-machine-learning:
container_name: immich_machine_learning
@@ -40,10 +43,12 @@ services:
env_file:
- .env
restart: always
healthcheck:
disable: false
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -57,13 +62,14 @@ services:
POSTGRES_DB: ${DB_DATABASE_NAME}
POSTGRES_INITDB_ARGS: '--data-checksums'
volumes:
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
healthcheck:
test: pg_isready --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' || exit 1; Chksum="$$(psql --dbname='${DB_DATABASE_NAME}' --username='${DB_USERNAME}' --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
interval: 5m
start_interval: 30s
start_period: 5m
command: ["postgres", "-c" ,"shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
command: ["postgres", "-c", "shared_preload_libraries=vectors.so", "-c", 'search_path="$$user", public, vectors', "-c", "logging_collector=on", "-c", "max_wal_size=2GB", "-c", "shared_buffers=512MB", "-c", "wal_compression=on"]
restart: always
volumes:

View File

@@ -12,6 +12,7 @@ DB_DATA_LOCATION=./postgres
IMMICH_VERSION=release
# Connection secret for postgres. You should change it to a random password
# Please use only the characters `A-Za-z0-9`, without special characters or spaces
DB_PASSWORD=postgres
# The values below this line do not need to be changed

View File

@@ -1 +1 @@
20.15.1
20.17.0

View File

@@ -80,7 +80,7 @@ If your instance has more than 4 users, it is more cost-effective to buy the **S
### 3. What do I do if I don't pay?
You can continue using Immich for an unlimited trial period.
You can continue using Immich without any restriction.
### 4. Will there be any paywalled features?

View File

@@ -1,6 +1,7 @@
---
title: Immich Update - July 2024
authors: [alextran]
date: 2024-07-01T00:00
tags: [update, v1.106.0]
---

View File

@@ -52,14 +52,25 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca
- Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich.
- Use the Immich app more often.
### Why are features not working with a self-signed cert or mTLS?
Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background).
We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/).
---
## Assets
### Does Immich change the file?
No, Immich does not touch the original file under any circumstances,
all edited metadata are saved in the companion sidecar file and the database.
No, Immich does not modify the original files.
All edited metadata is saved in companion `.xmp` sidecar files and the database.
However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI.
### Why do my file names appear as a random string in the file manager?
When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
### Can I add my existing photo library?
@@ -157,6 +168,19 @@ We haven't implemented an official mechanism for creating albums from external l
Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries.
### Why are my edits to files not being saved in read-only external libraries?
Images in read-write external libraries (the default) can be edited as normal.
In read-only libraries (`:ro` in the `docker-compose.yml`), Immich is unable to create the `.xmp` sidecar files to store edited file metadata.
For this reason, the metadata (timestamp, location, description, star rating, etc.) cannot be edited for files in read-only external libraries.
### How are deletions of files handled in external libraries?
Immich will attempt to delete original files that have been trashed when the trash is emptied.
In read-write external libraries (the default), Immich will delete the original file.
In read-only libraries (`:ro` in the `docker-compose.yml`), files can still be trashed in the UI.
However, when the trash is emptied, the files will re-appear in the main timeline since Immich is unable to delete the original file.
---
## Machine Learning
@@ -294,6 +318,12 @@ You need to enable WebSockets on your reverse proxy.
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
### How can I reduce the log verbosity of Redis?
To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`:
` command: redis-server --loglevel warning`
### How can I run Immich as a non-root user?
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.

View File

@@ -45,7 +45,7 @@ docker compose up -d # Start remainder of Immich apps
<TabItem value="Windows system (PowerShell)" label="Windows system (PowerShell)">
```powershell title='Backup'
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "\path\to\backup\dump.sql"
docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | Set-Content -Encoding utf8 "C:\path\to\backup\dump.sql"
```
```powershell title='Restore'
@@ -149,9 +149,21 @@ for more info read the [release notes](https://github.com/immich-app/immich/rele
- Preview images (small thumbnails and large previews) for each asset and thumbnails for recognized faces.
- Stored in `UPLOAD_LOCATION/thumbs/<userID>`.
- **Encoded Assets:**
- Videos that have been re-encoded from the original for wider compatibility. The original is not removed.
- Stored in `UPLOAD_LOCATION/encoded-video/<userID>`.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `UPLOAD_LOCATION/postgres`.
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>
<TabItem value="Storage Template On" label="Storage Template On">
@@ -187,8 +199,19 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
- Files uploaded through mobile apps.
- Temporarily located in `UPLOAD_LOCATION/upload/<userID>`.
- Transferred to `UPLOAD_LOCATION/library/<userID>` upon successful upload.
- **Postgres**
- The Immich database containing all the information to allow the system to function properly.
**Note:** This folder will only appear to users who have made the changes mentioned in [v1.102.0](https://github.com/immich-app/immich/discussions/8930) (an optional, non-mandatory change) or who started with this version.
- Stored in `UPLOAD_LOCATION/postgres`.
:::danger
A backup of this folder does not constitute a backup of your database!
Follow the instructions listed [here](/docs/administration/backup-and-restore#database) to learn how to perform a proper backup.
:::
</TabItem>
</Tabs>
:::danger

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

View File

@@ -52,4 +52,4 @@ Additionally, some jobs run on a schedule, which is every night at midnight. Thi
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />
<img src={require('./img/admin-jobs.webp').default} width="60%" title="Admin jobs" />

View File

@@ -3,7 +3,7 @@
This page contains details about using OAuth in Immich.
:::tip
Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution.
Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution.
:::
## Overview
@@ -30,7 +30,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured
The **Sign-in redirect URIs** should include:
- `app.immich:/` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
- `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
@@ -38,7 +38,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Mobile
- `app.immich:/` (You **MUST** include this for iOS and Android mobile apps to work properly)
- `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
Localhost
@@ -96,16 +96,16 @@ When Auto Launch is enabled, the login page will automatically redirect the user
## Mobile Redirect URI
The redirect URI for the mobile app is `app.immich:/`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following:
The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following:
1. Configure an http(s) endpoint to forwards requests to `app.immich:/`
1. Configure an http(s) endpoint to forwards requests to `app.immich:///oauth-callback`
2. Whitelist the new endpoint as a valid redirect URI with your provider.
3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings.
With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI.
:::info
Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:/`, and can be used for step 1.
Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1.
:::
## Example Configuration
@@ -154,21 +154,21 @@ Configuration of Authorised redirect URIs (Google Console)
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
| Setting | Value |
| ---------------------------- | ---------------------------------------------------------------------------- |
| Issuer URL | `https://accounts.google.com` |
| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Google (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled |
| Mobile Redirect URI Override | Enabled (required) |
| Mobile Redirect URI | `https://example.immich.app/api/oauth/mobile-redirect` |
</details>

View File

@@ -104,7 +104,7 @@ You can choose to disable a certain type of machine learning, for example smart
### Smart Search
The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the
The [smart search](/docs/features/smart-search) settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the
Smart Search job on all images to fully apply the change.
:::info Internet connection
@@ -113,15 +113,23 @@ After downloading, there is no need for Immich to connect to the network
Unless version checking has been enabled in the settings.
:::
### Duplicate Detection
Use CLIP embeddings to find likely duplicates. The maximum detection distance can be configured in order to improve / reduce the level of accuracy.
- **Maximum detection distance -** Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives.
### Facial Recognition
Under these settings, you can change the facial recognition settings
Editable settings:
- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model.
- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.
- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible.
- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.
- **Facial Recognition Model**
- **Min Detection Score**
- **Max Recognition Distance**
- **Min Recognized Faces**
You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works)
:::info
When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces.
@@ -153,7 +161,7 @@ SMTP server setup, for user creation notifications, new albums, etc. More inform
### External Domain
When set, will override the domain name used when viewing and copying a shared link.
Overrides the domain name in shared links and email notifications. The URL should not include a trailing slash.
### Welcome Message

View File

@@ -24,7 +24,7 @@ This environment includes the services below. Additional details are available i
- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web)
- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning)
- Redis
- PostgreSQL development database with exposed port `5432` so you can use any database client to acess it
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
All the services are packaged to run as with single Docker Compose command.

View File

@@ -4,7 +4,8 @@
### Unit tests
Unit are run by calling `npm run test` from the `server` directory.
Unit are run by calling `npm run test` from the `server/` directory.
You need to run `npm install` (in `server/`) before _once_.
### End to end tests
@@ -14,6 +15,11 @@ The e2e tests can be run by first starting up a test production environment via:
make e2e
```
Before you can run the tests, you need to run the following commands _once_:
- `npm install` (in `e2e/`)
- `make open-api` (in the project root `/`)
Once the test environment is running, the e2e tests can be run via:
```bash

View File

@@ -36,7 +36,7 @@ Face detection sends the generated preview image to the machine learning service
## How Facial Recognition Works
The facial recognition algorithm we use is derived from DBSCAN, a popular clustering algorithm. It essentially treats each detected face as a point in a graph and aims to group points that are close to each other.
The facial recognition algorithm we use is derived from [DBSCAN](https://www.youtube.com/watch?v=RDZUdRSDOok), a popular clustering algorithm. It essentially treats each detected face as a point in a graph and aims to group points that are close to each other.
:::note
An important concept is whether something is a _core point_. A core point has a minimum number of points around it within a certain distance. A non-core point can only be assigned to a cluster if it can reach a core point; a non-core point can't be used to extend a cluster even if it's part of one. In Immich, the _Minimum Recognized Faces_ setting controls the threshold to be considered a core point.

View File

@@ -104,15 +104,16 @@ The `immich-server` container will need access to the gallery. Modify your docke
immich-server:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/nas/christmas-trip:ro
+ - /home/user/old-pics:/home/user/old-pics:ro
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - /mnt/media/videos2:/mnt/media/videos2 # the files in this folder can be deleted, as it does not end with :ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
```
:::tip
The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI.
The `ro` flag at the end only gives read-only access to the volumes.
This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)).
:::
:::info

View File

@@ -32,12 +32,13 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- Where and how you can get this file depends on device and vendor, but typically, the device vendor also supplies these
- The `hwaccel.ml.yml` file assumes the path to it is `/usr/lib/libmali.so`, so update accordingly if it is elsewhere
- The `hwaccel.ml.yml` file assumes an additional file `/lib/firmware/mali_csffw.bin`, so update accordingly if your device's driver does not require this file
- Optional: Configure your `.env` file, see [environment variables](/docs/install/environment-variables) for ARM NN specific settings
#### CUDA
- The GPU must have compute capability 5.2 or greater.
- The server must have the official NVIDIA driver installed.
- The installed driver must be >= 535 (it must support CUDA 12.2).
- The installed driver must be >= 545 (it must support CUDA 12.3.2).
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
#### OpenVINO

View File

@@ -66,7 +66,7 @@ The provided file is just a starting point. There are a ton of ways to configure
After bringing down the containers with `docker compose down` and back up with `docker compose up -d`, a Prometheus instance will now collect metrics from the immich server and microservices containers. Note that we didn't need to expose any new ports for these containers - the communication is handled in the internal Docker network.
:::note
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8081` to the microservices container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports. Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
:::
### Usage

View File

@@ -16,7 +16,7 @@ When sharing shared albums, whats shared is:
- Download all assets as zip file (Web only).
:::info Archive size limited.
If the size of the album exceeds 4GB, the archive files will be divided into 4GB each.
If the size of the album exceeds 4GB, the archive files will by default be divided into 4GB each. This can be changed on the user settings page.
:::
- Add a description to the album (Web only).
- Slideshow view (Web only).
@@ -73,14 +73,14 @@ You can edit the link properties, options include;
- **Allow public user to download -** whether to allow whoever has the link to download all the images or a certain image (optional).
- **Allow public user to upload -** whether to allow whoever has the link to upload assets to the album (optional).
:::info
whoever has the link and have uploaded files cannot delete them.
Whoever has the link and have uploaded files cannot delete them.
:::
- **Expire after -** adding an expiration date to the link (optional).
## Share Specific Assets
A user can share specific assets without linking them to a specific album.
in order to do so;
In order to do this:
1. Go to the timeline
2. Select the assets (Shift can be used for multiple selection)
@@ -152,7 +152,7 @@ Some of the features are not available on mobile, to understand what the full fe
## Sharing Between Users
#### Add or remove users from the album.
#### Add or remove users from the album
:::info remove user(s)
When a user is removed from the album, the photos he uploaded will still appear in the album.

View File

@@ -13,14 +13,14 @@ In our `.env` file, we will define variables that will help us in the future whe
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library
+ UPLOAD_LOCATION=/custom/location/on/your/system/immich/immich_files
+ THUMB_LOCATION=/custom/location/on/your/system/immich/thumbs
+ ENCODED_VIDEO_LOCATION=/custom/location/on/your/system/immich/encoded-video
+ PROFILE_LOCATION=/custom/location/on/your/system/immich/profile
+ UPLOAD_LOCATION=/custom/path/immich/immich_files
+ THUMB_LOCATION=/custom/path/immich/thumbs
+ ENCODED_VIDEO_LOCATION=/custom/path/immich/encoded-video
+ PROFILE_LOCATION=/custom/path/immich/profile
...
```
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers.
After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container.
```diff title="docker-compose.yml"
services:
@@ -29,16 +29,6 @@ services:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile
- /etc/localtime:/etc/localtime:ro
...
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs
+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video
+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile
- /etc/localtime:/etc/localtime:ro
```
@@ -46,7 +36,6 @@ services:
Restart Immich to register the changes.
```
docker compose down
docker compose up -d
```

View File

@@ -1,8 +1,22 @@
# Create Custom Map Styles for Immich Using Maptiler
# Custom Map Styles
You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
You may decide that you'd like to modify the style document which is used to
draw the maps in Immich. In addition to visual customization, this also allows
you to pick your own map tile provider instead of the default one. The default
`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json)
and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json)
can be used as a basis for creating your own style.
## Steps
There are several sources for already-made `style.json` map themes, as well as
online generators you can use.
1. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.)
3. Save your selections. Reload the map, and enjoy your custom map style!
## Use Maptiler to build a custom style
Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand.
1. Create a free account at https://cloud.maptiler.com
2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there.
@@ -11,6 +25,3 @@ You may decide that you'd like to modify the style document which is used to dra
5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.<br/>![Maptiler Publication Settings](img/immich_map_styles_publish.png)
6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay.
7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler.
8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection.
9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.
10. Save your selections. Reload the map, and enjoy your custom map style!

View File

@@ -5,7 +5,7 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo
:::
:::tip
Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to the database via the container directly.
Run `docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME>` to connect to the database via the container directly.
(Replace `<DB_USERNAME>` with the value from your [`.env` file](/docs/install/environment-variables#database)).
:::
@@ -23,7 +23,7 @@ SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files wi
```
```sql title="Find by path"
SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_20230903_232542848.jpg';
SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg';
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
```
@@ -37,6 +37,12 @@ SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e
SELECT * FROM "assets" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation
```
```sql title="Find duplicate assets with identical checksum (SHA-1) (excluding trashed files)"
SELECT T1."checksum", array_agg(T2."id") ids FROM "assets" T1
INNER JOIN "assets" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL
WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum";
```
```sql title="Live photos"
SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL;
```
@@ -79,8 +85,7 @@ SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type";
```sql title="Count by type (per user)"
SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets"
JOIN "users" ON "assets"."ownerId" = "users"."id"
GROUP BY "assets"."type", "users"."email"
ORDER BY "users"."email";
GROUP BY "assets"."type", "users"."email" ORDER BY "users"."email";
```
```sql title="Failed file movements"
@@ -106,3 +111,9 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config';
```sql title="Delete person and unset it for the faces it was associated with"
DELETE FROM "person" WHERE "name" = 'PersonNameHere';
```
## Postgres internal
```sql title="Change DB_PASSWORD"
ALTER USER <DB_USERNAME> WITH ENCRYPTED PASSWORD 'newpasswordhere';
```

View File

@@ -7,7 +7,7 @@ in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`.
If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point.
If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point.
```diff
immich-server:

View File

@@ -11,13 +11,13 @@ Never forward port 2283 directly to the internet without additional configuratio
You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/)
### Pros:
### Pros
- Simple to set up and very secure.
- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk.
- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal.
### Cons:
### Cons
- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider.
- VPN software needs to be installed and active on both server-side and client-side.
@@ -27,6 +27,10 @@ You may use a VPN service to open an encrypted connection to your Immich instanc
If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
:::tip Video toturial
You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created.
:::
### Pros
- Minimal configuration needed on server and client sides.
@@ -44,7 +48,7 @@ A reverse proxy is a service that sits between web servers and clients. A revers
If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/docs/administration/reverse-proxy.md).
You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder.

View File

@@ -11,6 +11,10 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
:::
:::danger
When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud.
:::
```yaml
name: immich_remote_ml

View File

@@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
cd /tmp/immich-mountpoint
```
You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`

View File

@@ -56,7 +56,8 @@ Optionally, you can enable hardware acceleration for machine learning and transc
- Populate custom database information if necessary.
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets.
- Consider changing `DB_PASSWORD` to something randomly generated
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`.
### Step 3 - Start the containers
@@ -108,7 +109,7 @@ Immich is currently under heavy development, which means you can expect [breakin
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
[watchtower]: https://containrrr.dev/watchtower/
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created
[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
[releases]: https://github.com/immich-app/immich/releases
[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository

View File

@@ -38,19 +38,23 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*1</sup>⚠️ | `./upload`<sup>\*2</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
\*1: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*2: 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.
:::tip
@@ -121,7 +125,7 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis][redis-api] documentation.
More info can be found in the upstream [ioredis] documentation.
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
:::
@@ -155,23 +159,29 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Containers |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
| Variable | Description | Default | Containers |
| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
\*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
\*3: For scenarios like HPA in K8S. https://github.com/immich-app/immich/discussions/12064
:::info
Other machine learning parameters can be tuned from the admin UI.
@@ -216,4 +226,4 @@ to use use a Docker secret for the password in the Redis container.
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis

View File

@@ -4,7 +4,7 @@ sidebar_position: 10
# Requirements
Hardware and software requirements for Immich
Hardware and software requirements for Immich:
## Software

View File

@@ -45,7 +45,7 @@ width="70%"
alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
/>
3. Select the cog ⚙️ next to Immich then click "**Edit Stack**"
3. Select the cogwheel ⚙️ next to Immich and click "**Edit Stack**"
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
<details >
@@ -130,7 +130,7 @@ For more information on how to use the application once installed, please refer
## Updating Steps
Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman ui, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager.
Updating is extremely easy however it's important to be aware that containers managed via the Docker Compose Manager plugin do not integrate with Unraid's native dockerman UI, the label "_update ready_" will always be present on containers installed via the Docker Compose Manager.
<img
src={require('./img/unraid09.png').default}

View File

@@ -27,3 +27,9 @@ If an asset is in multiple albums, `{{album}}` will be set to the name of the al
:::
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album:
```
{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 404 KiB

After

Width:  |  Height:  |  Size: 1.5 MiB

View File

@@ -145,28 +145,36 @@ const config = {
label: 'Installation',
to: '/docs/install/requirements',
},
{
label: 'Contributing',
to: '/docs/overview/support-the-project',
},
{
label: 'Privacy Policy',
to: '/privacy-policy',
},
],
},
{
title: 'Community',
title: 'Documentation',
items: [
{
label: 'Discord',
href: 'https://discord.immich.app',
label: 'Roadmap',
to: '/roadmap',
},
{
label: 'Reddit',
href: 'https://www.reddit.com/r/immich/',
label: 'API',
to: '/docs/api',
},
{
label: 'Cursed Knowledge',
to: '/cursed-knowledge',
},
],
},
{
title: 'Links',
items: [
// {
// label: "Blog",
// to: "/blog",
// },
{
label: 'GitHub',
href: 'https://github.com/immich-app/immich',
@@ -175,6 +183,14 @@ const config = {
label: 'YouTube',
href: 'https://www.youtube.com/@immich-app',
},
{
label: 'Discord',
href: 'https://discord.immich.app',
},
{
label: 'Reddit',
href: 'https://www.reddit.com/r/immich/',
},
],
},
],

575
docs/package-lock.json generated
View File

@@ -2155,9 +2155,9 @@
}
},
"node_modules/@docusaurus/core": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz",
"integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz",
"integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.3",
@@ -2170,12 +2170,12 @@
"@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8",
"@docusaurus/cssnano-preset": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/cssnano-preset": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2236,14 +2236,15 @@
"node": ">=18.0"
},
"peerDependencies": {
"@mdx-js/react": "^3.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/cssnano-preset": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz",
"integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz",
"integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==",
"license": "MIT",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
@@ -2256,9 +2257,9 @@
}
},
"node_modules/@docusaurus/logger": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz",
"integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz",
"integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
@@ -2269,14 +2270,14 @@
}
},
"node_modules/@docusaurus/mdx-loader": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz",
"integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz",
"integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/logger": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@@ -2308,12 +2309,12 @@
}
},
"node_modules/@docusaurus/module-type-aliases": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz",
"integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz",
"integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==",
"license": "MIT",
"dependencies": {
"@docusaurus/types": "3.4.0",
"@docusaurus/types": "3.5.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2326,52 +2327,21 @@
"react-dom": "*"
}
},
"node_modules/@docusaurus/plugin-content-blog": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz",
"integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"cheerio": "^1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"srcset": "^4.0.0",
"tslib": "^2.6.0",
"unist-util-visit": "^5.0.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/plugin-content-docs": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz",
"integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz",
"integrity": "sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/module-type-aliases": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
@@ -2389,38 +2359,15 @@
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/plugin-content-pages": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz",
"integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/plugin-debug": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz",
"integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz",
"integrity": "sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0"
@@ -2434,14 +2381,14 @@
}
},
"node_modules/@docusaurus/plugin-google-analytics": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz",
"integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz",
"integrity": "sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2453,14 +2400,14 @@
}
},
"node_modules/@docusaurus/plugin-google-gtag": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz",
"integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz",
"integrity": "sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},
@@ -2473,14 +2420,14 @@
}
},
"node_modules/@docusaurus/plugin-google-tag-manager": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz",
"integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz",
"integrity": "sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2492,17 +2439,17 @@
}
},
"node_modules/@docusaurus/plugin-sitemap": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz",
"integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz",
"integrity": "sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"
@@ -2516,24 +2463,81 @@
}
},
"node_modules/@docusaurus/preset-classic": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz",
"integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz",
"integrity": "sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/plugin-debug": "3.4.0",
"@docusaurus/plugin-google-analytics": "3.4.0",
"@docusaurus/plugin-google-gtag": "3.4.0",
"@docusaurus/plugin-google-tag-manager": "3.4.0",
"@docusaurus/plugin-sitemap": "3.4.0",
"@docusaurus/theme-classic": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-search-algolia": "3.4.0",
"@docusaurus/types": "3.4.0"
"@docusaurus/core": "3.5.2",
"@docusaurus/plugin-content-blog": "3.5.2",
"@docusaurus/plugin-content-docs": "3.5.2",
"@docusaurus/plugin-content-pages": "3.5.2",
"@docusaurus/plugin-debug": "3.5.2",
"@docusaurus/plugin-google-analytics": "3.5.2",
"@docusaurus/plugin-google-gtag": "3.5.2",
"@docusaurus/plugin-google-tag-manager": "3.5.2",
"@docusaurus/plugin-sitemap": "3.5.2",
"@docusaurus/theme-classic": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/theme-search-algolia": "3.5.2",
"@docusaurus/types": "3.5.2"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-blog": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz",
"integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"cheerio": "1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"srcset": "^4.0.0",
"tslib": "^2.6.0",
"unist-util-visit": "^5.0.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"@docusaurus/plugin-content-docs": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-pages": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz",
"integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
@@ -2544,27 +2548,27 @@
}
},
"node_modules/@docusaurus/theme-classic": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz",
"integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz",
"integrity": "sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.4.0",
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/types": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/module-type-aliases": "3.5.2",
"@docusaurus/plugin-content-blog": "3.5.2",
"@docusaurus/plugin-content-docs": "3.5.2",
"@docusaurus/plugin-content-pages": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/theme-translations": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
"infima": "0.2.0-alpha.43",
"infima": "0.2.0-alpha.44",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"postcss": "^8.4.26",
@@ -2583,19 +2587,73 @@
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/theme-common": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz",
"integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==",
"node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-blog": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz",
"integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==",
"license": "MIT",
"dependencies": {
"@docusaurus/mdx-loader": "3.4.0",
"@docusaurus/module-type-aliases": "3.4.0",
"@docusaurus/plugin-content-blog": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/plugin-content-pages": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"cheerio": "1.0.0-rc.12",
"feed": "^4.2.2",
"fs-extra": "^11.1.1",
"lodash": "^4.17.21",
"reading-time": "^1.5.0",
"srcset": "^4.0.0",
"tslib": "^2.6.0",
"unist-util-visit": "^5.0.0",
"utility-types": "^3.10.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"@docusaurus/plugin-content-docs": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-pages": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz",
"integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==",
"license": "MIT",
"dependencies": {
"@docusaurus/core": "3.5.2",
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/types": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"fs-extra": "^11.1.1",
"tslib": "^2.6.0",
"webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
},
"peerDependencies": {
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/theme-common": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz",
"integrity": "sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew==",
"license": "MIT",
"dependencies": {
"@docusaurus/mdx-loader": "3.5.2",
"@docusaurus/module-type-aliases": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2609,24 +2667,25 @@
"node": ">=18.0"
},
"peerDependencies": {
"@docusaurus/plugin-content-docs": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/theme-search-algolia": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
"integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz",
"integrity": "sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA==",
"license": "MIT",
"dependencies": {
"@docsearch/react": "^3.5.2",
"@docusaurus/core": "3.4.0",
"@docusaurus/logger": "3.4.0",
"@docusaurus/plugin-content-docs": "3.4.0",
"@docusaurus/theme-common": "3.4.0",
"@docusaurus/theme-translations": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-validation": "3.4.0",
"@docusaurus/core": "3.5.2",
"@docusaurus/logger": "3.5.2",
"@docusaurus/plugin-content-docs": "3.5.2",
"@docusaurus/theme-common": "3.5.2",
"@docusaurus/theme-translations": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-validation": "3.5.2",
"algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0",
@@ -2645,9 +2704,9 @@
}
},
"node_modules/@docusaurus/theme-translations": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz",
"integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz",
"integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==",
"license": "MIT",
"dependencies": {
"fs-extra": "^11.1.1",
@@ -2658,9 +2717,9 @@
}
},
"node_modules/@docusaurus/types": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz",
"integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz",
"integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==",
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
@@ -2679,13 +2738,13 @@
}
},
"node_modules/@docusaurus/utils": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz",
"integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz",
"integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/logger": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0",
@@ -2718,9 +2777,9 @@
}
},
"node_modules/@docusaurus/utils-common": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz",
"integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz",
"integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.0"
@@ -2738,14 +2797,14 @@
}
},
"node_modules/@docusaurus/utils-validation": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz",
"integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==",
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz",
"integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==",
"license": "MIT",
"dependencies": {
"@docusaurus/logger": "3.4.0",
"@docusaurus/utils": "3.4.0",
"@docusaurus/utils-common": "3.4.0",
"@docusaurus/logger": "3.5.2",
"@docusaurus/utils": "3.5.2",
"@docusaurus/utils-common": "3.5.2",
"fs-extra": "^11.2.0",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
@@ -4237,9 +4296,9 @@
}
},
"node_modules/autoprefixer": {
"version": "10.4.19",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz",
"integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==",
"version": "10.4.20",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
"integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
"funding": [
{
"type": "opencollective",
@@ -4254,12 +4313,13 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"browserslist": "^4.23.0",
"caniuse-lite": "^1.0.30001599",
"browserslist": "^4.23.3",
"caniuse-lite": "^1.0.30001646",
"fraction.js": "^4.3.7",
"normalize-range": "^0.1.2",
"picocolors": "^1.0.0",
"picocolors": "^1.0.1",
"postcss-value-parser": "^4.2.0"
},
"bin": {
@@ -4531,9 +4591,9 @@
}
},
"node_modules/browserslist": {
"version": "4.23.0",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz",
"integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==",
"version": "4.23.3",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
"integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
"funding": [
{
"type": "opencollective",
@@ -4548,11 +4608,12 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"caniuse-lite": "^1.0.30001587",
"electron-to-chromium": "^1.4.668",
"node-releases": "^2.0.14",
"update-browserslist-db": "^1.0.13"
"caniuse-lite": "^1.0.30001646",
"electron-to-chromium": "^1.5.4",
"node-releases": "^2.0.18",
"update-browserslist-db": "^1.1.0"
},
"bin": {
"browserslist": "cli.js"
@@ -4699,9 +4760,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001614",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz",
"integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==",
"version": "1.0.30001651",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
"integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
"funding": [
{
"type": "opencollective",
@@ -4715,7 +4776,8 @@
"type": "github",
"url": "https://github.com/sponsors/ai"
}
]
],
"license": "CC-BY-4.0"
},
"node_modules/ccount": {
"version": "2.0.1",
@@ -6342,9 +6404,10 @@
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
},
"node_modules/electron-to-chromium": {
"version": "1.4.751",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz",
"integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw=="
"version": "1.5.6",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
"integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
"license": "ISC"
},
"node_modules/emoji-regex": {
"version": "9.2.2",
@@ -8763,9 +8826,10 @@
}
},
"node_modules/infima": {
"version": "0.2.0-alpha.43",
"resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz",
"integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==",
"version": "0.2.0-alpha.44",
"resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.44.tgz",
"integrity": "sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ==",
"license": "MIT",
"engines": {
"node": ">=12"
}
@@ -11958,9 +12022,10 @@
}
},
"node_modules/node-releases": {
"version": "2.0.14",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw=="
"version": "2.0.18",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
"integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
"license": "MIT"
},
"node_modules/nopt": {
"version": "1.0.10",
@@ -12754,9 +12819,9 @@
}
},
"node_modules/postcss": {
"version": "8.4.39",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz",
"integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==",
"version": "8.4.40",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz",
"integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==",
"funding": [
{
"type": "opencollective",
@@ -13600,9 +13665,9 @@
}
},
"node_modules/prettier": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz",
"integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==",
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz",
"integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==",
"dev": true,
"license": "MIT",
"bin": {
@@ -13633,9 +13698,10 @@
}
},
"node_modules/prism-react-renderer": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz",
"integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==",
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz",
"integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==",
"license": "MIT",
"dependencies": {
"@types/prismjs": "^1.26.0",
"clsx": "^2.0.0"
@@ -13747,9 +13813,10 @@
}
},
"node_modules/qs": {
"version": "6.12.1",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz",
"integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==",
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
"license": "BSD-3-Clause",
"dependencies": {
"side-channel": "^1.0.6"
},
@@ -16014,9 +16081,9 @@
}
},
"node_modules/tailwindcss": {
"version": "3.4.4",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz",
"integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==",
"version": "3.4.10",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
"integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
@@ -16376,9 +16443,9 @@
}
},
"node_modules/typescript": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz",
"integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==",
"version": "5.5.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz",
"integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==",
"license": "Apache-2.0",
"bin": {
"tsc": "bin/tsc",
@@ -16607,9 +16674,9 @@
}
},
"node_modules/update-browserslist-db": {
"version": "1.0.13",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
"integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==",
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz",
"integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==",
"funding": [
{
"type": "opencollective",
@@ -16624,9 +16691,10 @@
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
"picocolors": "^1.0.0"
"escalade": "^3.1.2",
"picocolors": "^1.0.1"
},
"bin": {
"update-browserslist-db": "cli.js"
@@ -16714,12 +16782,16 @@
}
},
"node_modules/url": {
"version": "0.11.3",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz",
"integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==",
"version": "0.11.4",
"resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz",
"integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==",
"license": "MIT",
"dependencies": {
"punycode": "^1.4.1",
"qs": "^6.11.2"
"qs": "^6.12.3"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/url-loader": {
@@ -16783,7 +16855,8 @@
"node_modules/url/node_modules/punycode": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz",
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ=="
"integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==",
"license": "MIT"
},
"node_modules/util": {
"version": "0.10.4",

View File

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

View File

@@ -43,6 +43,11 @@ const guides: CommunityGuidesProps[] = [
description: 'Access your local Immich installation over the internet using your own domain',
url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md',
},
{
title: 'Nginx caching map server',
description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server',
url: 'https://github.com/pcouy/pcouy.github.io/blob/main/_posts/2024-08-30-proxying-a-map-tile-server-for-increased-privacy.md',
},
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {

View File

@@ -28,11 +28,6 @@ const projects: CommunityProjectProps[] = [
description: 'A simple way to remove orphaned offline assets from the Immich database',
url: 'https://github.com/Thoroslives/immich_remove_offline_files',
},
{
title: 'Create albums from folders',
description: 'A Python script to create albums based on the folder structure of an external library.',
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
},
{
title: 'Immich-Tools',
description: 'Provides scripts for handling problems on the repair page.',
@@ -43,6 +38,11 @@ const projects: CommunityProjectProps[] = [
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
url: 'https://github.com/midzelis/mi.Immich.Publisher',
},
{
title: 'Lightroom Immich Plugin: lrc-immich-plugin',
description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.',
url: 'https://github.com/bmachek/lrc-immich-plugin',
},
{
title: 'Immich Duplicate Finder',
description: 'Webapp that uses machine learning to identify near-duplicate images.',
@@ -58,11 +58,31 @@ const projects: CommunityProjectProps[] = [
description: 'Unofficial Immich Android TV app.',
url: 'https://github.com/giejay/Immich-Android-TV',
},
{
title: 'Create albums from folders',
description: 'A Python script to create albums based on the folder structure of an external library.',
url: 'https://github.com/Salvoxia/immich-folder-album-creator',
},
{
title: 'Powershell Module PSImmich',
description: 'Powershell Module for the Immich API',
url: 'https://github.com/hanpq/PSImmich',
},
{
title: 'Immich Distribution',
description: 'Snap package for easy install and zero-care auto updates of Immich. Self-hosted photo management.',
url: 'https://immich-distribution.nsg.cc',
},
{
title: 'Immich Kiosk',
description: 'Lightweight slideshow to run on kiosk devices and browsers.',
url: 'https://github.com/damongolding/immich-kiosk',
},
{
title: 'Immich Power Tools',
description: 'Power tools for organizing your immich library.',
url: 'https://github.com/varun-raj/immich-power-tools',
},
];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {

View File

@@ -1,4 +1,3 @@
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';

View File

@@ -1,4 +1,13 @@
import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js';
import {
mdiCalendarToday,
mdiCrosshairsOff,
mdiLeadPencil,
mdiLockOff,
mdiLockOutline,
mdiSpeedometerSlow,
mdiWeb,
mdiWrap,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item as TimelineItem, Timeline } from '../components/timeline';
@@ -8,6 +17,41 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
const items: Item[] = [
{
icon: mdiWrap,
iconColor: 'gray',
title: 'Carriage returns in bash scripts are cursed',
description: 'Git can be configured to automatically convert LF to CRLF on checkout and CRLF breaks bash scripts.',
link: {
url: 'https://github.com/immich-app/immich/pull/11613',
text: '#11613',
},
date: new Date(2024, 7, 7),
},
{
icon: mdiLockOff,
iconColor: 'red',
title: 'Fetch inside Cloudflare Workers is cursed',
description:
'Fetch requests in Cloudflare Workers use http by default, even if you explicitly specify https, which can often cause redirect loops.',
link: {
url: 'https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/5',
text: 'Cloudflare',
},
date: new Date(2024, 7, 7),
},
{
icon: mdiCrosshairsOff,
iconColor: 'gray',
title: 'GPS sharing on mobile is cursed',
description:
'Some phones will silently strip GPS data from images when apps without location permission try to access them.',
link: {
url: 'https://github.com/immich-app/immich/discussions/11268',
text: '#11268',
},
date: new Date(2024, 6, 21),
},
{
icon: mdiLeadPencil,
iconColor: 'gold',

View File

@@ -41,7 +41,7 @@ function HomepageHeader() {
Discord
</Link>
</div>
<img src="/img/immich-screenshots.png" alt="screenshots" width={'70%'} />
<img src="/img/immich-screenshots.webp" alt="screenshots" width={'70%'} />
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
<div className="h-24">
<a href="https://play.google.com/store/apps/details?id=app.alextran.immich">

View File

@@ -0,0 +1,114 @@
import React from 'react';
import Link from '@docusaurus/Link';
import Layout from '@theme/Layout';
import { useColorMode } from '@docusaurus/theme-common';
function HomepageHeader() {
const { isDarkTheme } = useColorMode();
return (
<header>
<section className="max-w-[900px] m-4 p-4 md:p-6 md:m-auto md:my-12 border border-red-400 rounded-2xl bg-slate-200 dark:bg-immich-dark-gray">
<section>
<h1>Privacy Policy</h1>
<p>Last updated: July 31st 2024</p>
<p>
Welcome to Immich. We are committed to respecting your privacy. This Privacy Policy sets out how we collect,
use, and share information when you use our Immich app.
</p>
</section>
{/* 1. Scope of This Policy */}
<section>
<h2>1. Scope of This Policy</h2>
<p>
This Privacy Policy applies to the Immich app ("we", "our", or "us") and covers our collection, use, and
disclosure of your information. This Policy does not cover any third-party websites, services, or
applications that can be accessed through our app, or third-party services you may access through Immich.
</p>
</section>
{/* 2. Information We Collect */}
<section>
<h2>2. Information We Collect</h2>
<div>
<p>
<strong>Locally Stored Data</strong>: Immich stores all your photos, albums, settings, and locally on your
device. We do not have access to this data, nor do we transmit or store it on any of our servers.
</p>
</div>
<div>
<p>
<strong>Purchase Information:</strong> When you make a purchase within the{' '}
<a href="https://buy.immich.app">https://buy.immich.app</a>, we collect the following information for tax
calculation purposes:
</p>
<ul>
<li>Country of origin</li>
<li>Postal code (if the user is from Canada or the United States)</li>
</ul>
</div>
</section>
{/* 3. Use of Your Information */}
<section>
<h2>3. Use of Your Information</h2>
<p>
<strong>Tax Calculation:</strong> The country of origin and postal code (for users from Canada or the United
States) are collected solely for determining the applicable tax rates on your purchase.
</p>
</section>
{/* 4. Sharing of Your Information */}
<section>
<h2>4. Sharing of Your Information</h2>
<ul>
<li>
<strong>Tax Authorities:</strong> The purchase information may be shared with tax authorities as required
by law.
</li>
<li>
<strong>Payment Providers:</strong> The purchase information may be shared with payment providers where
required.
</li>
</ul>
</section>
{/* 5. Changes to This Policy */}
<section>
<h2>5. Changes to This Policy</h2>
<p>
We may update our Privacy Policy from time to time. If we make any changes, we will notify you by revising
the "Last updated" date at the top of this policy. It's encouraged that users frequently check this page for
any changes to stay informed about how we are helping to protect the personal information we collect.
</p>
</section>
{/* 6. Contact Us */}
<section>
<h2>6. Contact Us</h2>
<p>
If you have any questions about this Privacy Policy, please contact us at{' '}
<a href="mailto:immich@futo.org">immich@futo.org</a>
</p>
</section>
</section>
</header>
);
}
export default function Home(): JSX.Element {
return (
<Layout
title="Home"
description="immich Self-hosted photo and video backup solution directly from your mobile phone "
noFooter={true}
>
<HomepageHeader />
<div className="flex flex-col place-items-center place-content-center">
<p>This project is available under GNU AGPL v3 license.</p>
<p className="text-xs">Privacy should not be a luxury</p>
</div>
</Layout>
);
}

View File

@@ -15,6 +15,7 @@ import {
mdiCloudUploadOutline,
mdiCollage,
mdiContentDuplicate,
mdiCrop,
mdiDevices,
mdiEmailOutline,
mdiExpansionCard,
@@ -26,6 +27,7 @@ import {
mdiFileSearch,
mdiFlash,
mdiFolder,
mdiFolderMultiple,
mdiForum,
mdiHandshakeOutline,
mdiHeart,
@@ -36,6 +38,7 @@ import {
mdiImageMultipleOutline,
mdiImageSearch,
mdiKeyboardSettingsOutline,
mdiLicense,
mdiLockOutline,
mdiMagnify,
mdiMagnifyScan,
@@ -55,11 +58,14 @@ import {
mdiScaleBalance,
mdiSecurity,
mdiServer,
mdiShare,
mdiShareAll,
mdiShareCircle,
mdiStar,
mdiStarOutline,
mdiTableKey,
mdiTag,
mdiTagMultiple,
mdiText,
mdiThemeLightDark,
mdiTrashCanOutline,
@@ -72,6 +78,11 @@ import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
'v1.113.0': new Date(2024, 7, 30),
'v1.112.0': new Date(2024, 7, 14),
'v1.111.0': new Date(2024, 6, 26),
'v1.110.0': new Date(2024, 5, 11),
'v1.109.0': new Date(2024, 6, 18),
'v1.106.1': new Date(2024, 5, 11),
'v1.104.0': new Date(2024, 4, 13),
'v1.103.0': new Date(2024, 3, 29),
@@ -220,6 +231,67 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
withRelease({
icon: mdiTagMultiple,
iconColor: 'orange',
title: 'Tags',
description: 'Tag your photos and videos',
release: 'v1.113.0',
}),
withRelease({
icon: mdiFolderMultiple,
iconColor: 'brown',
title: 'Folders',
description: 'View your photos and videos in folders',
release: 'v1.113.0',
}),
withRelease({
icon: mdiPalette,
title: 'Theming (mobile)',
description: 'Pick a primary color for the mobile app',
release: 'v1.112.0',
}),
withRelease({
icon: mdiStarOutline,
iconColor: 'gold',
title: 'Star rating',
description: 'Rate your photos and videos',
release: 'v1.112.0',
}),
withRelease({
icon: mdiCrop,
iconColor: 'royalblue',
title: 'Editor (mobile)',
description: 'Crop and rotate on mobile',
release: 'v1.111.0',
}),
withRelease({
icon: mdiMap,
iconColor: 'green',
title: 'Deploy tiles.immich.cloud',
description: 'Dedicated tile server for Immich',
release: 'v1.111.0',
}),
{
icon: mdiStar,
iconColor: 'gold',
title: '40,000 Stars',
description: 'Reached 40K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2024, 6, 21)),
},
withRelease({
icon: mdiShare,
title: 'Deploy my.immich.app',
description: 'Url router for immich links',
release: 'v1.109.0',
}),
withRelease({
icon: mdiLicense,
iconColor: 'gold',
title: 'Supporter Badge',
description: 'The option to buy Immich to support its development!',
release: 'v1.109.0',
}),
withRelease({
icon: mdiHistory,
title: 'Versioned documentation',
@@ -236,7 +308,7 @@ const milestones: Item[] = [
withRelease({
icon: mdiContentDuplicate,
title: 'Similar image detection',
description: 'Detect duplicate assets that arent exactly identical',
description: "Detect duplicate assets that aren't exactly identical",
release: 'v1.106.1',
}),
withRelease({

View File

@@ -1,4 +1,40 @@
[
{
"label": "v1.114.0",
"url": "https://v1.114.0.archive.immich.app"
},
{
"label": "v1.113.1",
"url": "https://v1.113.1.archive.immich.app"
},
{
"label": "v1.113.0",
"url": "https://v1.113.0.archive.immich.app"
},
{
"label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app"
},
{
"label": "v1.112.0",
"url": "https://v1.112.0.archive.immich.app"
},
{
"label": "v1.111.0",
"url": "https://v1.111.0.archive.immich.app"
},
{
"label": "v1.110.0",
"url": "https://v1.110.0.archive.immich.app"
},
{
"label": "v1.109.2",
"url": "https://v1.109.2.archive.immich.app"
},
{
"label": "v1.109.1",
"url": "https://v1.109.1.archive.immich.app"
},
{
"label": "v1.109.0",
"url": "https://v1.109.0.archive.immich.app"

BIN
docs/static/img/immich-screenshots.webp vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@@ -4,7 +4,7 @@ module.exports = {
corePlugins: {
preflight: false, // disable Tailwind's reset
},
content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src
content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
theme: {
extend: {

View File

@@ -1,32 +0,0 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
tsconfigRootDir: __dirname,
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
root: true,
env: {
node: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/prevent-abbreviations': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prefer-event-target': 'off',
'unicorn/no-thenable': 'off',
},
};

View File

@@ -1 +1 @@
20.15.1
20.17.0

View File

@@ -1,5 +1,3 @@
version: '3.8'
name: immich-e2e
services:
@@ -32,10 +30,10 @@ services:
- redis
- database
ports:
- 2283:3001
- 2285:3001
redis:
image: redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
@@ -45,7 +43,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_DB: immich
ports:
- 5433:5432
- 5435:5432
volumes:
model-cache:

65
e2e/eslint.config.mjs Normal file
View File

@@ -0,0 +1,65 @@
import { FlatCompat } from '@eslint/eslintrc';
import js from '@eslint/js';
import typescriptEslint from '@typescript-eslint/eslint-plugin';
import tsParser from '@typescript-eslint/parser';
import globals from 'globals';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: js.configs.recommended,
allConfig: js.configs.all,
});
export default [
{
ignores: ['eslint.config.mjs'],
},
...compat.extends(
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
'plugin:unicorn/recommended',
),
{
plugins: {
'@typescript-eslint': typescriptEslint,
},
languageOptions: {
globals: {
...globals.node,
},
parser: tsParser,
ecmaVersion: 5,
sourceType: 'module',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir: __dirname,
},
},
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-floating-promises': 'error',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
'unicorn/prevent-abbreviations': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-null': 'off',
'unicorn/prefer-top-level-await': 'off',
'unicorn/prefer-event-target': 'off',
'unicorn/no-thenable': 'off',
'object-shorthand': ['error', 'always'],
},
},
];

1899
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "1.109.0",
"version": "1.114.0",
"description": "",
"main": "index.js",
"type": "module",
@@ -19,23 +19,26 @@
"author": "",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
"@eslint/js": "^9.8.0",
"@immich/cli": "file:../cli",
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^20.14.10",
"@types/node": "^20.16.2",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.0",
"eslint": "^8.57.0",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
"eslint": "^9.0.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^54.0.0",
"exiftool-vendored": "^27.0.0",
"eslint-plugin-unicorn": "^55.0.0",
"exiftool-vendored": "^28.0.0",
"globals": "^15.9.0",
"jose": "^5.6.3",
"luxon": "^3.4.4",
"oidc-provider": "^8.5.1",
@@ -47,9 +50,9 @@
"supertest": "^7.0.0",
"typescript": "^5.3.3",
"utimes": "^5.2.1",
"vitest": "^1.3.0"
"vitest": "^2.0.5"
},
"volta": {
"node": "20.15.1"
"node": "20.17.0"
}
}

View File

@@ -8,7 +8,7 @@ export default defineConfig({
workers: 1,
reporter: 'html',
use: {
baseURL: 'http://127.0.0.1:2283',
baseURL: 'http://127.0.0.1:2285',
trace: 'on-first-retry',
},
@@ -54,7 +54,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'docker compose up --build -V --remove-orphans',
url: 'http://127.0.0.1:2283',
url: 'http://127.0.0.1:2285',
reuseExistingServer: true,
},
});

View File

@@ -344,16 +344,16 @@ describe('/albums', () => {
});
});
describe('GET /albums/count', () => {
describe('GET /albums/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/albums/count');
const { status, body } = await request(app).get('/albums/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should return total count of albums the user has access to', async () => {
const { status, body } = await request(app)
.get('/albums/count')
.get('/albums/statistics')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);

View File

@@ -0,0 +1,258 @@
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const create = (accessToken: string, permissions: Permission[]) =>
createApiKey({ apiKeyCreateDto: { name: 'api key', permissions } }, { headers: asBearerAuth(accessToken) });
describe('/api-keys', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
beforeEach(async () => {
await utils.resetDatabase(['api_keys']);
});
describe('POST /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not work without permission', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('apiKey.create'));
});
it('should work with apiKey.create', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({
name: 'API Key',
permissions: [Permission.ApiKeyRead],
});
expect(body).toEqual({
secret: expect.any(String),
apiKey: {
id: expect.any(String),
name: 'API Key',
permissions: [Permission.ApiKeyRead],
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
});
expect(status).toBe(201);
});
it('should not create an api key with all permissions', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({ name: 'API Key', permissions: [Permission.All] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have'));
});
it('should not create an api key with more permissions', async () => {
const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]);
const { status, body } = await request(app)
.post('/api-keys')
.set('x-api-key', secret)
.send({ name: 'API Key', permissions: [Permission.ApiKeyRead] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have'));
});
it('should create an api key', async () => {
const { status, body } = await request(app)
.post('/api-keys')
.send({ name: 'API Key', permissions: [Permission.All] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual({
apiKey: {
id: expect.any(String),
name: 'API Key',
permissions: [Permission.All],
createdAt: expect.any(String),
updatedAt: expect.any(String),
},
secret: expect.any(String),
});
expect(status).toEqual(201);
});
});
describe('GET /api-keys', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/api-keys');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should start off empty', async () => {
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
expect(status).toEqual(200);
});
it('should return a list of api keys', async () => {
const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([
create(admin.accessToken, [Permission.All]),
create(admin.accessToken, [Permission.All]),
create(admin.accessToken, [Permission.All]),
]);
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(3);
expect(body).toEqual(expect.arrayContaining([apiKey1, apiKey2, apiKey3]));
expect(status).toEqual(200);
});
});
describe('GET /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should get api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.get(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
id: expect.any(String),
name: 'api key',
permissions: [Permission.All],
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
});
describe('PUT /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`)
.send({ name: 'new name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.put(`/api-keys/${uuidDto.invalid}`)
.send({ name: 'new name' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should update api key details', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.put(`/api-keys/${apiKey.id}`)
.send({ name: 'new name' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
id: expect.any(String),
name: 'new name',
permissions: [Permission.All],
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
});
describe('DELETE /api-keys/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app)
.delete(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('API Key not found'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/api-keys/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should delete an api key', async () => {
const { apiKey } = await create(user.accessToken, [Permission.All]);
const { status } = await request(app)
.delete(`/api-keys/${apiKey.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(204);
});
});
describe('authentication', () => {
it('should work as a header', async () => {
const { secret } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret);
expect(body).toHaveLength(1);
expect(status).toBe(200);
});
it('should work as a query param', async () => {
const { secret } = await create(user.accessToken, [Permission.All]);
const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`);
expect(body).toHaveLength(1);
expect(status).toBe(200);
});
});
});

View File

@@ -6,8 +6,9 @@ import {
LoginResponseDto,
SharedLinkType,
getAssetInfo,
getConfig,
getMyUser,
updateAssets,
updateConfig,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
@@ -43,6 +44,10 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -66,25 +71,24 @@ describe('/asset', () => {
let timeBucketUser: LoginResponseDto;
let quotaUser: LoginResponseDto;
let statsUser: LoginResponseDto;
let stackUser: LoginResponseDto;
let user1Assets: AssetMediaResponseDto[];
let user2Assets: AssetMediaResponseDto[];
let stackAssets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto;
let ratingAsset: AssetMediaResponseDto;
let facesAsset: AssetMediaResponseDto;
const setupTests = async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[websocket, user1, user2, statsUser, quotaUser, timeBucketUser, stackUser] = await Promise.all([
[websocket, user1, user2, statsUser, quotaUser, timeBucketUser] = await Promise.all([
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.create('1')),
utils.userSetup(admin.accessToken, createUserDto.create('2')),
utils.userSetup(admin.accessToken, createUserDto.create('stats')),
utils.userSetup(admin.accessToken, createUserDto.userQuota),
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
utils.userSetup(admin.accessToken, createUserDto.create('stack')),
]);
await utils.createPartner(user1.accessToken, user2.userId);
@@ -99,6 +103,16 @@ describe('/asset', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: locationAsset.id });
// asset rating
ratingAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'mongolels.jpg',
bytes: await readFile(ratingAssetFilepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: ratingAsset.id });
user1Assets = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
@@ -137,20 +151,6 @@ describe('/asset', () => {
}),
]);
// stacks
stackAssets = await Promise.all([
utils.createAsset(stackUser.accessToken),
utils.createAsset(stackUser.accessToken),
utils.createAsset(stackUser.accessToken),
utils.createAsset(stackUser.accessToken),
utils.createAsset(stackUser.accessToken),
]);
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const person1 = await utils.createPerson(user1.accessToken, {
name: 'Test Person',
});
@@ -214,6 +214,80 @@ describe('/asset', () => {
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('should get the asset rating', async () => {
await utils.waitForWebsocketEvent({
event: 'assetUpload',
id: ratingAsset.id,
});
const { status, body } = await request(app)
.get(`/assets/${ratingAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: ratingAsset.id,
exifInfo: expect.objectContaining({ rating: 3 }),
});
});
it('should get the asset faces', async () => {
const config = await getSystemConfig(admin.accessToken);
config.metadata.faces.import = true;
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
// asset faces
facesAsset = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'portrait.jpg',
bytes: await readFile(facesAssetFilepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
const { status, body } = await request(app)
.get(`/assets/${facesAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body.id).toEqual(facesAsset.id);
expect(body.people).toMatchObject([
{
name: 'Marie Curie',
birthDate: null,
thumbnailPath: '',
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 261,
boundingBoxX2: 356,
boundingBoxY1: 146,
boundingBoxY2: 284,
sourceType: 'exif',
},
],
},
{
name: 'Pierre Curie',
birthDate: null,
thumbnailPath: '',
isHidden: false,
faces: [
{
imageHeight: 700,
imageWidth: 840,
boundingBoxX1: 536,
boundingBoxX2: 618,
boundingBoxY1: 83,
boundingBoxY2: 252,
sourceType: 'exif',
},
],
},
]);
});
it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
@@ -353,6 +427,8 @@ describe('/asset', () => {
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
});
it('should require authentication', async () => {
@@ -389,17 +465,14 @@ describe('/asset', () => {
}
});
it.each(TEN_TIMES)(
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
async () => {
const { status, body } = await request(app)
.get('/assets/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => {
const { status, body } = await request(app)
.get('/assets/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
},
);
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
});
it('should return error', async () => {
const { status } = await request(app)
@@ -472,6 +545,44 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it('should update date time original when sidecar file contains DateTimeOriginal', async () => {
const sidecarData = `<?xpacket begin='?' id='W5M0MpCehiHzreSzNTczkc9d'?>
<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='Image::ExifTool 12.40'>
<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#'>
<rdf:Description rdf:about=''
xmlns:exif='http://ns.adobe.com/exif/1.0/'>
<exif:ExifVersion>0220</exif:ExifVersion> <exif:DateTimeOriginal>2024-07-11T10:32:52Z</exif:DateTimeOriginal>
<exif:GPSVersionID>2.3.0.0</exif:GPSVersionID>
</rdf:Description>
</rdf:RDF>
</x:xmpmeta>
<?xpacket end='w'?>`;
const { id } = await utils.createAsset(user1.accessToken, {
sidecarData: {
bytes: Buffer.from(sidecarData),
filename: 'example.xmp',
},
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const assetInfo = await utils.getAssetInfo(user1.accessToken, id);
expect(assetInfo.exifInfo?.dateTimeOriginal).toBe('2024-07-11T10:32:52.000Z');
const { status, body } = await request(app)
.put(`/assets/${id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
expect(body).toMatchObject({
id,
exifInfo: expect.objectContaining({
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
}),
});
expect(status).toEqual(200);
});
it('should reject invalid gps coordinates', async () => {
for (const test of [
{ latitude: 12 },
@@ -537,6 +648,31 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it('should set the rating', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ rating: 2 });
expect(body).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({
rating: 2,
}),
});
expect(status).toEqual(200);
});
it('should reject invalid rating', async () => {
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
}
});
it('should return tagged people', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
@@ -735,145 +871,8 @@ describe('/asset', () => {
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
});
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should add stack children', async () => {
const { status } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
});
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).toBeUndefined();
});
it('should merge stack children', async () => {
// create stack after previous test removed stack children
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
{ headers: asBearerAuth(stackUser.accessToken) },
);
const { status } = await request(app)
.put('/assets')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
expect(status).toBe(204);
const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[0].id }),
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
]),
);
});
});
describe('PUT /assets/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/assets/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/assets/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const { status, body } = await request(app)
.put('/assets/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/assets/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
expect(status).toBe(200);
const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// new parent
expect(asset.stack).not.toBeUndefined();
expect(asset.stack).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: stackAssets[1].id }),
expect.objectContaining({ id: stackAssets[2].id }),
expect.objectContaining({ id: stackAssets[3].id }),
]),
);
});
});
describe('POST /assets', () => {
beforeAll(setupTests, 30_000);
@@ -902,13 +901,12 @@ describe('/asset', () => {
expect(body).toEqual(errorDto.badRequest());
});
it.each([
const tests = [
{
input: 'formats/avif/8bit-sRGB.avif',
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.avif',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
@@ -924,7 +922,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'el_torcal_rocks.jpg',
resized: true,
exifInfo: {
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
exifImageWidth: 512,
@@ -948,7 +945,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.jxl',
resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
@@ -964,7 +960,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'IMG_2682.heic',
resized: true,
fileCreatedAt: '2019-03-21T16:04:22.348Z',
exifInfo: {
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
@@ -989,7 +984,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'density_plot.png',
resized: true,
exifInfo: {
exifImageWidth: 800,
exifImageHeight: 800,
@@ -1004,7 +998,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'glarus.nef',
resized: true,
fileCreatedAt: '2010-07-20T17:27:12.000Z',
exifInfo: {
make: 'NIKON CORPORATION',
@@ -1026,7 +1019,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'philadelphia.nef',
resized: true,
fileCreatedAt: '2016-09-22T22:10:29.060Z',
exifInfo: {
make: 'NIKON CORPORATION',
@@ -1049,7 +1041,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '4_3.rw2',
resized: true,
fileCreatedAt: '2018-05-10T08:42:37.842Z',
exifInfo: {
make: 'Panasonic',
@@ -1073,7 +1064,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '12bit-compressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-09-27T10:51:44.000Z',
exifInfo: {
make: 'SONY',
@@ -1098,8 +1088,7 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '14bit-uncompressed-(3_2).arw',
resized: true,
fileCreatedAt: '2016-01-08T15:08:01.000Z',
fileCreatedAt: '2016-01-08T14:08:01.000Z',
exifInfo: {
make: 'SONY',
model: 'ILCE-7M2',
@@ -1111,28 +1100,39 @@ describe('/asset', () => {
iso: 100,
lensModel: 'E 25mm F2',
fileSizeInByte: 49_512_448,
dateTimeOriginal: '2016-01-08T15:08:01.000Z',
dateTimeOriginal: '2016-01-08T14:08:01.000Z',
latitude: null,
longitude: null,
orientation: '1',
},
},
},
])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
const filepath = join(testAssetDir, input);
const { id, status } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
];
expect(status).toBe(AssetMediaStatus.Created);
it(`should upload and generate a thumbnail for different file types`, async () => {
// upload in parallel
const assets = await Promise.all(
tests.map(async ({ input }) => {
const filepath = join(testAssetDir, input);
return utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
}),
);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
for (const { id, status } of assets) {
expect(status).toBe(AssetMediaStatus.Created);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
}
const asset = await utils.getAssetInfo(admin.accessToken, id);
for (const [i, { id }] of assets.entries()) {
const { expected } = tests[i];
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
expect(asset).toMatchObject(expected);
}
});
it('should handle a duplicate', async () => {

View File

@@ -353,7 +353,7 @@ describe('/libraries', () => {
expect(assets.count).toBe(2);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 });
@@ -361,11 +361,11 @@ describe('/libraries', () => {
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
});
it('should offline missing files', async () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
it('should offline a file missing from disk', async () => {
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
@@ -374,7 +374,40 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(3);
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
expect(newAssets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: true,
originalFileName: 'assetC.png',
}),
]),
);
});
it('should offline a file outside of import paths', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -383,6 +416,45 @@ describe('/libraries', () => {
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: false,
originalFileName: 'assetA.png',
}),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
}),
]),
);
});
it('should offline a file covered by an exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/directoryB/**'] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(2);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
isOffline: false,
originalFileName: 'assetA.png',
}),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
@@ -434,6 +506,8 @@ describe('/libraries', () => {
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
});
it('should scan new files', async () => {
@@ -445,14 +519,14 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(3);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -460,6 +534,8 @@ describe('/libraries', () => {
}),
]),
);
utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
});
describe('with refreshModifiedFiles=true', () => {
@@ -559,10 +635,11 @@ describe('/libraries', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline2`],
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -570,9 +647,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
expect(initialAssets.count).toBe(1);
expect(initialAssets.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -593,7 +670,54 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(0);
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should remove offline files from trash', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
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.count).toBe(2);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.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.count).toBe(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/removeOffline`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
expect(assets.items[0].isOffline).toBe(false);
expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`);
utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should not remove online files', async () => {

View File

@@ -159,4 +159,75 @@ describe('/map', () => {
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
});
describe('GET /map/reverse-geocode', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/reverse-geocode');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw an error if a lat is not provided', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
});
it('should throw an error if a lat is not a number', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
});
it('should throw an error if a lat is out of range', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
});
it('should throw an error if a lon is not provided', async () => {
const { status, body } = await request(app)
.get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180']));
});
const reverseGeocodeTestCases = [
{
name: 'Vaucluse',
lat: -33.858_977_058_663_13,
lon: 151.278_490_730_270_48,
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
},
{
name: 'Ravenhall',
lat: -37.765_732_399_174_75,
lon: 144.752_453_164_883_3,
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
},
{
name: 'Scarborough',
lat: -31.894_346_156_789_997,
lon: 115.757_617_103_904_64,
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
},
];
it.each(reverseGeocodeTestCases)(`should resolve to $name`, async ({ lat, lon, results }) => {
const { status, body } = await request(app)
.get(`/map/reverse-geocode?lat=${lat}&lon=${lon}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(Array.isArray(body)).toBe(true);
expect(body.length).toBe(results.length);
expect(body).toEqual(results);
});
});
});

View File

@@ -92,14 +92,14 @@ describe(`/oauth`, () => {
it('should return a redirect uri', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2283/auth/login' });
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) });
const params = new URL(body.url).searchParams;
expect(params.get('client_id')).toBe('client-default');
expect(params.get('response_type')).toBe('code');
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2283/auth/login');
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
expect(params.get('state')).toBeDefined();
});
});

View File

@@ -6,10 +6,19 @@ import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const invalidBirthday = [
{ birthDate: 'false', response: 'birthDate must be a date string' },
{ birthDate: '123567', response: 'birthDate must be a date string' },
{ birthDate: 123_567, response: 'birthDate must be a date string' },
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
{
birthDate: 'false',
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{
birthDate: '123567',
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{
birthDate: 123_567,
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
},
{ birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] },
];
describe('/people', () => {
@@ -65,6 +74,7 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 3,
hidden: 1,
people: [
@@ -80,6 +90,7 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: false,
total: 3,
hidden: 1,
people: [
@@ -88,6 +99,21 @@ describe('/people', () => {
],
});
});
it('should support pagination', async () => {
const { status, body } = await request(app)
.get('/people')
.set('Authorization', `Bearer ${admin.accessToken}`)
.query({ withHidden: true, page: 2, size: 1 });
expect(status).toBe(200);
expect(body).toEqual({
hasNextPage: true,
total: 3,
hidden: 1,
people: [expect.objectContaining({ name: 'visible_person' })],
});
});
});
describe('GET /people/:id', () => {
@@ -168,13 +194,13 @@ describe('/people', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
name: 'New Person',
birthDate: '1990-01-01T05:00:00.000Z',
birthDate: '1990-01-01',
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
name: 'New Person',
birthDate: '1990-01-01T05:00:00.000Z',
birthDate: '1990-01-01',
});
});
});
@@ -216,7 +242,7 @@ describe('/people', () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01T05:00:00.000Z' });
.send({ birthDate: '1990-01-01' });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: '1990-01-01' });
});

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
@@ -32,9 +32,6 @@ describe('/search', () => {
let assetOneJpg5: AssetMediaResponseDto;
let assetSprings: AssetMediaResponseDto;
let assetLast: AssetMediaResponseDto;
let cities: string[];
let states: string[];
let countries: string[];
beforeAll(async () => {
await utils.resetDatabase();
@@ -85,7 +82,7 @@ describe('/search', () => {
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
const coordinates = [
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
{ latitude: 63.0695, longitude: -151.0074 }, // denali
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
@@ -101,16 +98,15 @@ describe('/search', () => {
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
];
const updates = assets.map((asset, i) =>
updateAsset({ id: asset.id, updateAssetDto: coordinates[i] }, { headers: asBearerAuth(admin.accessToken) }),
const updates = coordinates.map((dto, i) =>
updateAsset({ id: assets[i].id, updateAssetDto: dto }, { headers: asBearerAuth(admin.accessToken) }),
);
await Promise.all(updates);
for (const asset of assets) {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
for (const [i] of coordinates.entries()) {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
}
[
@@ -137,12 +133,6 @@ describe('/search', () => {
assetLast = assets.at(-1) as AssetMediaResponseDto;
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });
const mapMarkers = await getMapMarkers({}, { headers: asBearerAuth(admin.accessToken) });
const nonTrashed = mapMarkers.filter((mark) => mark.id !== assetSilver.id);
cities = [...new Set(nonTrashed.map((mark) => mark.city).filter((entry): entry is string => !!entry))].sort();
states = [...new Set(nonTrashed.map((mark) => mark.state).filter((entry): entry is string => !!entry))].sort();
countries = [...new Set(nonTrashed.map((mark) => mark.country).filter((entry): entry is string => !!entry))].sort();
}, 30_000);
afterAll(async () => {
@@ -321,23 +311,120 @@ describe('/search', () => {
},
{
should: 'should search by city',
deferred: () => ({ dto: { city: 'Accra' }, assets: [assetHeic] }),
deferred: () => ({
dto: {
city: 'Accra',
includeNull: true,
},
assets: [assetHeic],
}),
},
{
should: "should search city ('')",
deferred: () => ({
dto: {
city: '',
isVisible: true,
includeNull: true,
},
assets: [assetLast],
}),
},
{
should: 'should search city (null)',
deferred: () => ({
dto: {
city: null,
isVisible: true,
includeNull: true,
},
assets: [assetLast],
}),
},
{
should: 'should search by state',
deferred: () => ({ dto: { state: 'New York' }, assets: [assetDensity] }),
deferred: () => ({
dto: {
state: 'New York',
includeNull: true,
},
assets: [assetDensity],
}),
},
{
should: "should search state ('')",
deferred: () => ({
dto: {
state: '',
isVisible: true,
withExif: true,
includeNull: true,
},
assets: [assetLast, assetNotocactus],
}),
},
{
should: 'should search state (null)',
deferred: () => ({
dto: {
state: null,
isVisible: true,
includeNull: true,
},
assets: [assetLast, assetNotocactus],
}),
},
{
should: 'should search by country',
deferred: () => ({ dto: { country: 'France' }, assets: [assetFalcon] }),
deferred: () => ({
dto: {
country: 'France',
includeNull: true,
},
assets: [assetFalcon],
}),
},
{
should: "should search country ('')",
deferred: () => ({
dto: {
country: '',
isVisible: true,
includeNull: true,
},
assets: [assetLast],
}),
},
{
should: 'should search country (null)',
deferred: () => ({
dto: {
country: null,
isVisible: true,
includeNull: true,
},
assets: [assetLast],
}),
},
{
should: 'should search by make',
deferred: () => ({ dto: { make: 'Canon' }, assets: [assetFalcon, assetDenali] }),
deferred: () => ({
dto: {
make: 'Canon',
includeNull: true,
},
assets: [assetFalcon, assetDenali],
}),
},
{
should: 'should search by model',
deferred: () => ({ dto: { model: 'Canon EOS 7D' }, assets: [assetDenali] }),
deferred: () => ({
dto: {
model: 'Canon EOS 7D',
includeNull: true,
},
assets: [assetDenali],
}),
},
{
should: 'should allow searching the upload library (libraryId: null)',
@@ -450,32 +537,79 @@ describe('/search', () => {
it('should get suggestions for country', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=country')
.get('/search/suggestions?type=country&includeNull=true')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(countries);
expect(body).toEqual([
'Cuba',
'France',
'Georgia',
'Germany',
'Ghana',
'Japan',
'Morocco',
"People's Republic of China",
'Russian Federation',
'Singapore',
'Spain',
'Switzerland',
'United States of America',
null,
]);
expect(status).toBe(200);
});
it('should get suggestions for state', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=state')
.get('/search/suggestions?type=state&includeNull=true')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(states.length);
expect(body).toEqual(expect.arrayContaining(states));
expect(body).toEqual([
'Andalusia',
'Berlin',
'Glarus',
'Greater Accra',
'Havana',
'Île-de-France',
'Marrakesh-Safi',
'Mississippi',
'New York',
'Shanghai',
'St.-Petersburg',
'Tbilisi',
'Tokyo',
'Virginia',
null,
]);
expect(status).toBe(200);
});
it('should get suggestions for city', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=city')
.get('/search/suggestions?type=city&includeNull=true')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(cities);
expect(body).toEqual([
'Accra',
'Berlin',
'Glarus',
'Havana',
'Marrakesh',
'Montalbán de Córdoba',
'New York City',
'Novena',
'Paris',
'Philadelphia',
'Saint Petersburg',
'Shanghai',
'Stanley',
'Tbilisi',
'Tokyo',
null,
]);
expect(status).toBe(200);
});
it('should get suggestions for camera make', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=camera-make')
.get('/search/suggestions?type=camera-make&includeNull=true')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Apple',
@@ -485,13 +619,14 @@ describe('/search', () => {
'PENTAX Corporation',
'samsung',
'SONY',
null,
]);
expect(status).toBe(200);
});
it('should get suggestions for camera model', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=camera-model')
.get('/search/suggestions?type=camera-model&includeNull=true')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Canon EOS 7D',
@@ -506,6 +641,7 @@ describe('/search', () => {
'SM-F711N',
'SM-S906U',
'SM-T970',
null,
]);
expect(status).toBe(200);
});

View File

@@ -102,6 +102,7 @@ describe('/server-info', () => {
configFile: false,
duplicateDetection: false,
facialRecognition: false,
importFaces: false,
map: true,
reverseGeocoding: true,
oauth: false,

View File

@@ -110,6 +110,7 @@ describe('/server', () => {
facialRecognition: false,
map: true,
reverseGeocoding: true,
importFaces: false,
oauth: false,
oauthAutoLaunch: false,
passwordLogin: true,
@@ -254,7 +255,7 @@ describe('/server', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send(serverLicense);
const { status } = await request(app).get('/server/license').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(status).toBe(404);
});
});

View File

@@ -112,6 +112,13 @@ describe('/shared-links', () => {
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`);
});
it('should have fqdn og:image meta tag for shared asset', async () => {
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
expect(resp.status).toBe(200);
expect(resp.header['content-type']).toContain('text/html');
expect(resp.text).toContain(`<meta property="og:image" content="http://`);
});
});
describe('GET /shared-links', () => {

View File

@@ -0,0 +1,211 @@
import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/stacks', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let asset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[user1, user2] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
]);
asset = await utils.createAsset(user1.accessToken);
});
describe('POST /stacks', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/stacks')
.send({ assetIds: [asset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require at least two assets', async () => {
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should require access', async () => {
const user2Asset = await utils.createAsset(user2.accessToken);
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset.id, user2Asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should create a stack', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
primaryAssetId: asset1.id,
assets: [expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })],
});
});
it('should merge an existing stack', async () => {
const [asset1, asset2, asset3] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
const response1 = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(response1.status).toBe(201);
const stacksBefore = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) });
const { status, body } = await request(app)
.post('/stacks')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ assetIds: [asset1.id, asset3.id] });
expect(status).toBe(201);
expect(body).toEqual({
id: expect.any(String),
primaryAssetId: asset1.id,
assets: expect.arrayContaining([
expect.objectContaining({ id: asset1.id }),
expect.objectContaining({ id: asset2.id }),
expect.objectContaining({ id: asset3.id }),
]),
});
const stacksAfter = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) });
expect(stacksAfter.length).toBe(stacksBefore.length);
});
// it('should require a valid parent id', async () => {
// const { status, body } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${user1.accessToken}`)
// .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
// });
});
// it('should require access to the parent', async () => {
// const { status, body } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${user1.accessToken}`)
// .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.noPermission);
// });
// it('should add stack children', async () => {
// const { status } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
// .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
// expect(status).toBe(204);
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// expect(asset.stack).not.toBeUndefined();
// expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
// });
// it('should remove stack children', async () => {
// const { status } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
// .send({ removeParent: true, ids: [stackAssets[1].id] });
// expect(status).toBe(204);
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// expect(asset.stack).not.toBeUndefined();
// expect(asset.stack).toEqual(
// expect.arrayContaining([
// expect.objectContaining({ id: stackAssets[2].id }),
// expect.objectContaining({ id: stackAssets[3].id }),
// ]),
// );
// });
// it('should remove all stack children', async () => {
// const { status } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
// .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
// expect(status).toBe(204);
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// expect(asset.stack).toBeUndefined();
// });
// it('should merge stack children', async () => {
// // create stack after previous test removed stack children
// await updateAssets(
// { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
// { headers: asBearerAuth(stackUser.accessToken) },
// );
// const { status } = await request(app)
// .put('/assets')
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
// .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
// expect(status).toBe(204);
// const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
// expect(asset.stack).not.toBeUndefined();
// expect(asset.stack).toEqual(
// expect.arrayContaining([
// expect.objectContaining({ id: stackAssets[0].id }),
// expect.objectContaining({ id: stackAssets[1].id }),
// expect.objectContaining({ id: stackAssets[2].id }),
// ]),
// );
// });
});

View File

@@ -0,0 +1,603 @@
import {
AssetMediaResponseDto,
LoginResponseDto,
Permission,
TagCreateDto,
TagResponseDto,
createTag,
getAllTags,
tagAssets,
upsertTags,
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
const create = (accessToken: string, dto: TagCreateDto) =>
createTag({ tagCreateDto: dto }, { headers: asBearerAuth(accessToken) });
const upsert = (accessToken: string, tags: string[]) =>
upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) });
describe('/tags', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let userAsset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
user = await utils.userSetup(admin.accessToken, createUserDto.user1);
userAsset = await utils.createAsset(user.accessToken);
});
beforeEach(async () => {
// tagging assets eventually triggers metadata extraction which can impact other tests
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.resetDatabase(['tags']);
});
describe('POST /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/tags').send({ name: 'TagA' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.create'));
});
it('should work with tag.create', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]);
const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(body).toEqual({
id: expect.any(String),
name: 'TagA',
value: 'TagA',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(status).toBe(201);
});
it('should create a tag', async () => {
const { status, body } = await request(app)
.post('/tags')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'TagA' });
expect(body).toEqual({
id: expect.any(String),
name: 'TagA',
value: 'TagA',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(status).toBe(201);
});
it('should allow multiple users to create tags with the same value', async () => {
await create(admin.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.post('/tags')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ name: 'TagA' });
expect(body).toEqual({
id: expect.any(String),
name: 'TagA',
value: 'TagA',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(status).toBe(201);
});
it('should create a nested tag', async () => {
const parent = await create(admin.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.post('/tags')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'TagB', parentId: parent.id });
expect(body).toEqual({
id: expect.any(String),
parentId: parent.id,
name: 'TagB',
value: 'TagA/TagB',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
expect(status).toBe(201);
});
});
describe('GET /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/tags');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).get('/tags').set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});
it('should start off empty', async () => {
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([]);
expect(status).toEqual(200);
});
it('should return a list of tags', async () => {
const [tagA, tagB, tagC] = await Promise.all([
create(admin.accessToken, { name: 'TagA' }),
create(admin.accessToken, { name: 'TagB' }),
create(admin.accessToken, { name: 'TagC' }),
]);
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(3);
expect(body).toEqual([tagA, tagB, tagC]);
expect(status).toEqual(200);
});
it('should return a nested tags', async () => {
await upsert(admin.accessToken, ['TagA/TagB/TagC', 'TagD']);
const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toHaveLength(4);
expect(status).toEqual(200);
const tags = body as TagResponseDto[];
const tagA = tags.find((tag) => tag.value === 'TagA') as TagResponseDto;
const tagB = tags.find((tag) => tag.value === 'TagA/TagB') as TagResponseDto;
const tagC = tags.find((tag) => tag.value === 'TagA/TagB/TagC') as TagResponseDto;
const tagD = tags.find((tag) => tag.value === 'TagD') as TagResponseDto;
expect(tagA).toEqual(expect.objectContaining({ name: 'TagA', value: 'TagA' }));
expect(tagB).toEqual(expect.objectContaining({ name: 'TagB', value: 'TagA/TagB', parentId: tagA.id }));
expect(tagC).toEqual(expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC', parentId: tagB.id }));
expect(tagD).toEqual(expect.objectContaining({ name: 'TagD', value: 'TagD' }));
});
});
describe('PUT /tags', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.create'));
});
it('should upsert tags', async () => {
const { status, body } = await request(app)
.put(`/tags`)
.send({ tags: ['TagA/TagB/TagC/TagD'] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ name: 'TagD', value: 'TagA/TagB/TagC/TagD' })]);
});
it('should upsert tags in parallel without conflicts', async () => {
const [[tag1], [tag2], [tag3], [tag4]] = await Promise.all([
upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
]);
const { id, parentId, createdAt } = tag1;
for (const tag of [tag1, tag2, tag3, tag4]) {
expect(tag).toMatchObject({
id,
parentId,
createdAt,
name: 'TagD',
value: 'TagA/TagB/TagC/TagD',
});
}
});
});
describe('PUT /tags/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put('/tags/assets')
.set('x-api-key', secret)
.send({ assetIds: [], tagIds: [] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should skip assets that are not owned by the user', async () => {
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
create(user.accessToken, { name: 'TagA' }),
create(user.accessToken, { name: 'TagB' }),
create(user.accessToken, { name: 'TagC' }),
utils.createAsset(user.accessToken),
utils.createAsset(admin.accessToken),
]);
const { status, body } = await request(app)
.put(`/tags/assets`)
.send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 3 });
});
it('should skip tags that are not owned by the user', async () => {
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
create(user.accessToken, { name: 'TagA' }),
create(user.accessToken, { name: 'TagB' }),
create(admin.accessToken, { name: 'TagC' }),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
const { status, body } = await request(app)
.put(`/tags/assets`)
.send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 4 });
});
it('should bulk tag assets', async () => {
const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
create(user.accessToken, { name: 'TagA' }),
create(user.accessToken, { name: 'TagB' }),
create(user.accessToken, { name: 'TagC' }),
utils.createAsset(user.accessToken),
utils.createAsset(user.accessToken),
]);
const { status, body } = await request(app)
.put(`/tags/assets`)
.send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({ count: 6 });
});
});
describe('GET /tags/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.get(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.get(`/tags/${uuidDto.notFound}`)
.set('x-api-key', secret)
.send({ assetIds: [], tagIds: [] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.read'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should get tag details', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.get(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
id: expect.any(String),
name: 'TagA',
value: 'TagA',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
it('should get nested tag details', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
const tagC = await create(user.accessToken, { name: 'TagC', parentId: tagB.id });
const tagD = await create(user.accessToken, { name: 'TagD', parentId: tagC.id });
const { status, body } = await request(app)
.get(`/tags/${tagD.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
id: expect.any(String),
parentId: tagC.id,
name: 'TagD',
value: 'TagA/TagB/TagC/TagD',
createdAt: expect.any(String),
updatedAt: expect.any(String),
});
});
});
describe('PUT /tags/:id', () => {
it('should require authentication', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(admin.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '#000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.set('x-api-key', secret)
.send({ color: '#000000' });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.update'));
});
it('should update a tag', async () => {
const tag = await create(user.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '#000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ color: `#000000` }));
});
it('should update a tag color without a # prefix', async () => {
const tag = await create(user.accessToken, { name: 'tagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}`)
.send({ color: '000000' })
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ color: `#000000` }));
});
});
describe('DELETE /tags/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.delete(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app).delete(`/tags/${tag.id}`).set('x-api-key', secret);
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.delete'));
});
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
});
it('should delete a tag', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status } = await request(app)
.delete(`/tags/${tag.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(204);
});
it('should delete a nested tag (root)', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
const { status } = await request(app)
.delete(`/tags/${tagA.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(204);
const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) });
expect(tags.length).toBe(0);
});
it('should delete a nested tag (leaf)', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
const { status } = await request(app)
.delete(`/tags/${tagB.id}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(204);
const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) });
expect(tags.length).toBe(1);
expect(tags[0]).toEqual(tagA);
});
});
describe('PUT /tags/:id/assets', () => {
it('should require authentication', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.send({ ids: [userAsset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.put(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should be able to tag own asset', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
});
it("should not be able to add assets to another user's tag", async () => {
const tagA = await create(admin.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Not found or no tag.asset access'));
});
it('should add duplicate assets only once', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.put(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ids: [userAsset.id, userAsset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: userAsset.id, success: true }),
expect.objectContaining({ id: userAsset.id, success: false, error: 'duplicate' }),
]);
});
});
describe('DELETE /tags/:id/assets', () => {
it('should require authentication', async () => {
const tagA = await create(admin.accessToken, { name: 'TagA' });
const { status, body } = await request(app)
.delete(`/tags/${tagA}/assets`)
.send({ ids: [userAsset.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require authorization', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
{ headers: asBearerAuth(user.accessToken) },
);
const { status, body } = await request(app)
.delete(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
it('should require authorization (api key)', async () => {
const tag = await create(user.accessToken, { name: 'TagA' });
const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
const { status, body } = await request(app)
.delete(`/tags/${tag.id}/assets`)
.set('x-api-key', secret)
.send({ ids: [userAsset.id] });
expect(status).toBe(403);
expect(body).toEqual(errorDto.missingPermission('tag.asset'));
});
it('should be able to remove own asset from own tag', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
{ headers: asBearerAuth(user.accessToken) },
);
const { status, body } = await request(app)
.delete(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ids: [userAsset.id] });
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
});
it('should remove duplicate assets only once', async () => {
const tagA = await create(user.accessToken, { name: 'TagA' });
await tagAssets(
{ id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
{ headers: asBearerAuth(user.accessToken) },
);
const { status, body } = await request(app)
.delete(`/tags/${tagA.id}/assets`)
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ ids: [userAsset.id, userAsset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: userAsset.id, success: true }),
expect.objectContaining({ id: userAsset.id, success: false, error: 'not_found' }),
]);
});
});
});

View File

@@ -42,6 +42,23 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
});
it('should empty the trash with archived assets', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.archiveAssets(admin.accessToken, [assetId]);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
});
});
describe('POST /trash/restore', () => {

View File

@@ -1,11 +1,11 @@
import {
LoginResponseDto,
createStack,
deleteUserAdmin,
getMyUser,
getUserAdmin,
getUserPreferencesAdmin,
login,
updateAssets,
} from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
@@ -321,8 +321,8 @@ describe('/admin/users', () => {
utils.createAsset(user.accessToken),
]);
await updateAssets(
{ assetBulkUpdateDto: { stackParentId: asset1.id, ids: [asset2.id] } },
await createStack(
{ stackCreateDto: { assetIds: [asset1.id, asset2.id] } },
{ headers: asBearerAuth(user.accessToken) },
);

View File

@@ -236,6 +236,32 @@ describe('/users', () => {
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } });
});
it('should require a boolean for download include embedded videos', async () => {
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { includeEmbeddedVideos: 1_234_567.89 } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value']));
});
it('should update download include embedded videos', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ download: { includeEmbeddedVideos: true } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } });
});
});
describe('GET /users/:id', () => {

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