Compare commits

..

332 Commits

Author SHA1 Message Date
Jason Rasmussen
8b0684ee9c feat: show shared link view count 2025-02-18 17:39:54 -05:00
renovate[bot]
7bf142dc43 chore(deps): update prom/prometheus docker digest to 5888c18 (#16171)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:02:56 -05:00
renovate[bot]
d8cda6ee40 chore(deps): update base-image to v20250218 (major) (#16204)
chore(deps): update base-image to v20250218

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 16:02:33 -05:00
renovate[bot]
a31bc94460 fix(deps): update typescript-projects (#16203)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 21:35:50 +01:00
renovate[bot]
516709ffe1 chore(deps): update dependency @types/node to ^22.13.2 (#16200)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-18 15:10:44 -05:00
renovate[bot]
425cf62482 fix(deps): update typescript-projects (#16178)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-02-18 20:40:09 +01:00
Jason Anderson
58242b3b4a chore(docs): Synology set-up guide (#16179)
* Addition of Synology set-up guide

* fix: format

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-18 13:39:42 -06:00
Alex
9d4aee36e2 refactor(mobile): asset provider (#16159)
* refactor(mobile): asset provider

* wip

* wip: delete local assets

* wip: delete remote assets

* wip: deletion logic

* refactor

* pr feedback
2025-02-18 13:10:55 -06:00
shenlong
70d08a2b2a chore(mobile): lint (#16182)
* lint - convert path to lowercase for finding index

* update dcm lint rules

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2025-02-18 09:34:19 -06:00
Zack Pollard
f1b98d5f45 ci: docker cleanup, cleanup (#16194) 2025-02-18 14:56:58 +00:00
bo0tzz
749eff03d5 fix: pgvectors docs link (#16187)
Fixes #16184
2025-02-18 08:38:07 -05:00
bo0tzz
5f257b9a84 fix: don't write cache on fork PRs (#16189) 2025-02-18 12:47:20 +01:00
Jonathan Jogenfors
0cae20033c fix(server): more e2e library flakiness cleanup (#16176) 2025-02-17 19:04:38 -05:00
Jonathan Jogenfors
115ee0d6cc fix(server): remove unused readme (#16175)
fix(server): remove readme
2025-02-17 19:03:43 -05:00
Jonathan Jogenfors
bfdd6eac01 fix(server): flaky library e2e tests (#16174) 2025-02-17 18:26:44 -05:00
bo0tzz
9eab770e79 fix: don't push on forks (#16165) 2025-02-17 20:13:56 +00:00
João Paulo Ros
efd8d8b884 fix(mobile): Server endpoint on the login screen. (#16149)
Fixing the server endpoint on the login screen. It added the "/api" suffix instead of using the default method getServerUrl, which takes care of sanitizing the URL.

Co-authored-by: Joao Paulo Ros <ros@voxit.ai>
2025-02-17 19:12:48 +00:00
Alessandro Craciun
25e1c8cc7f chore(web): update italian translations (#15695) 2025-02-17 13:09:55 -06:00
Jason Rasmussen
7c26663013 chore: removed unused endpoint (#16167) 2025-02-17 13:07:50 -06:00
bo0tzz
2c88ce8559 chore: run full jobs on workflow file change (#16166) 2025-02-17 12:09:38 -06:00
Nick Overacker
50b072803d fix: limit width of logo in emails to 100% (#16164)
Limit width of logo in emails to 100%

The current live version breaks Yahoo Mail (at least in Firefox). It appears far too large and makes the email unreadable by pushing the text outside of the reading pane.
2025-02-17 17:46:14 +00:00
Mangat Singh Toor | ਮੰਗਤ ਸਿੰਘ ਤੂਰ
1689cecaf7 fix: include live images in person view count (#16116)
* fix: include live images in person view count

Fixed an issue where the total image count in the person view excluded live images.
The query now correctly accounts for all relevant assets by removing the condition
that filtered out assets with a livePhotoVideoId.

Issue:
- Image count under a person’s name was inaccurate, showing only static images.

Fix:
- Removed `.on('assets.livePhotoVideoId', 'is', null)` from the LEFT JOIN condition.

Tested on:
- Web

Ran PR checklist

* chore: run make sql.

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-17 15:49:30 +00:00
Pablo P Varela
5cd1018db3 fix(mobile): failed to load gl-ES locale (#16123) 2025-02-17 08:48:55 -06:00
renovate[bot]
31e6270a28 chore(deps): update docker.io/redis:6.2-alpine docker digest to 148bb54 (#16113)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 14:23:28 +00:00
renovate[bot]
b3fbd0809b chore(deps): update redis:6.2-alpine docker digest to 148bb54 (#16140)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-17 14:23:03 +00:00
Zack Pollard
129a4a82e0 ci: docker build cache (#16156) 2025-02-17 13:55:22 +00:00
Zack Pollard
924d11a913 ci: copy image layers from ghcr to dockerhub on release (#16155) 2025-02-17 13:41:45 +00:00
Zack Pollard
425c87bce4 ci: machine learning separate native docker image builds (#16102) 2025-02-17 11:56:28 +00:00
bo0tzz
25fcda6eeb chore: add warning to all compose files (#16146) 2025-02-16 21:28:59 -06:00
Jason Rasmussen
f386b4d377 feat(web): use thumbhash as a cache key (#16106)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-16 03:34:13 +00:00
renovate[bot]
c524fcf084 chore(deps): update node.js to v22.14.0 (#16132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 21:29:33 -06:00
renovate[bot]
194c567a45 chore(deps): update redis:6.2-alpine docker digest to 785233c (#16114)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-15 12:10:44 +00:00
Zack Pollard
411f96ef49 fix: place suggestions not clickable in asset set location modal (#16104)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-15 09:44:11 +00:00
Alex
4f912de018 refactor(mobile): album provider (#16099) 2025-02-14 19:27:39 -06:00
Alex
47203d2760 refactor(mobile): asset stack provider (#16100)
* refactor(mobile): asset stack provider

* remove file from ignore list
2025-02-14 13:23:14 -06:00
Zack Pollard
8ab87a8803 ci: retag commit hash unset outside of PRs (#16103) 2025-02-14 19:18:49 +01:00
Zack Pollard
5b4f894211 ci: docker images sha commit tag (#16098) 2025-02-14 16:08:41 +00:00
Mangat Singh Toor | ਮੰਗਤ ਸਿੰਘ ਤੂਰ
b1f05fc18b fix(web): properly project profile picture (#16095)
* fix(profile-image-cropper): ensure correct image area is saved after transparency check

Fixed an issue where users could not set a profile picture due to incorrect transparency detection.
After addressing transparency detection by passing explicit dimensions, another issue arose where the
generated blob did not represent the correct cropped image area. To fix this, a new cropped blob was generated using the canvas that was used to check for transparent pixels.

- Pass image width and height explicitly to `hasTransparentPixels` for accurate processing.
- Return both transparency status and the correctly cropped image blob.
- Ensure the final uploaded image is taken from `croppedImageBlob` to reflect user adjustments.

* chore: run pr web checklist. No issues in the changed file.

* fix(profile-image-cropper): ensure correct image area is saved after transparency check

Fixed an issue where users could not set a profile picture due to incorrect transparency detection.
To fix this, a new cropped blob was generated using the height and width of the imgElement.

Note: this is a simpler fix than the one in the previous commit.

* lint

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-02-14 15:49:22 +00:00
Zack Pollard
dbbefde98d feat: native arm and amd64 server builds (#15408) 2025-02-14 15:55:18 +01:00
Jonathan Jogenfors
5407a28533 feat(server): Nullable asset dates (#15669)
* nullable dates

* wip

* don't search for null dates

* Add placeholder type

* cleanup
2025-02-13 15:30:12 -06:00
bo0tzz
f5edc87e4d feat: comment URL on previewed PRs (#16085) 2025-02-13 21:10:00 +00:00
HelloMihai
bf16b61d43 fix: broken html id (#16084)
ids cannot have spaces

relative should not be in the ID of the element
2025-02-13 14:46:12 -05:00
Joren Guillaume
8c882b54cd docs: put Windows restore command on one line (#16074)
Lots of 'unexpected newline' comments when restoring from other users, this should fix that.
2025-02-13 05:44:33 -05:00
Jason Rasmussen
2d7c333c8c refactor(server): narrow auth types (#16066) 2025-02-12 15:23:08 -05:00
Yaros
7c821dd205 feat(mobile): Made Map Bottom Sheet extendable higher (#16056)
Made Map Bottom Sheet extendable higher
2025-02-12 14:56:50 +00:00
renovate[bot]
703361da1a chore(deps): update dependency svelte to v5.19.9 (#16043)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 17:24:39 -06:00
Jason Rasmussen
fa5aeaf539 refactor: last repository (#16042) 2025-02-11 22:15:56 +00:00
Jason Rasmussen
5f3a42a132 refactor: repositories (#16038) 2025-02-11 15:12:31 -05:00
Jason Rasmussen
9d85272c2b refactor: repositories (#16036) 2025-02-11 14:08:13 -05:00
renovate[bot]
d2575d8f00 fix(deps): update typescript-projects (#16023)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-02-11 18:50:18 +00:00
renovate[bot]
f0a4c945bd chore(deps): update github-actions (#16032)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 17:24:47 +00:00
renovate[bot]
a3766b879e fix(deps): update machine-learning (#16012)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 11:23:54 -06:00
Alex
1a190c33a0 chore(mobile): post release task (#16004) 2025-02-11 11:23:02 -06:00
renovate[bot]
17a63e37b2 chore(deps): update base-image to v20250211 (major) (#16025)
chore(deps): update base-image to v20250211

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 11:21:25 -06:00
renovate[bot]
bf1f8da884 chore(deps): update docker/build-push-action action to v6.13.0 (#16022)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 14:16:10 +01:00
renovate[bot]
2271984dbd chore(deps): update dependency @types/node to ^22.13.1 (#16013)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-11 00:19:02 +00:00
Snowknight26
b40963ec52 fix(web): Update shared link Exif capitalization to match existing capitalization (#16010)
Update shared link Exif capitalization to match existing capitalization
2025-02-10 19:00:37 -05:00
Jason Rasmussen
735f8d661e refactor: test mocks (#16008) 2025-02-11 00:47:42 +01:00
github-actions
8794c84e9d chore: version v1.126.1 2025-02-10 17:54:02 +00:00
Alex
cef19eed97 chore(mobile): patch openapi preference (#16000) 2025-02-10 17:39:43 +00:00
Alex
90c607c1a6 chore(mobile): post release task (#15998) 2025-02-10 11:12:36 -06:00
Daniel Dietzler
52b650093d fix: merch link (#15999) 2025-02-10 16:56:40 +00:00
Parsa Poorshikhian
fe4c49c8e3 chore: update of the persian translation (#15972)
* chore: update of the persian translation

* chore: update of the persian translation

* chore: update of the persian translation

* chore: update of the persian translation
2025-02-10 16:47:53 +00:00
Nicholas Flamy
4cad23aaa3 docs: add-hash #15860 follow-up (#15988)
add-hash
2025-02-10 10:46:47 -06:00
github-actions
feba590de7 chore: version v1.126.0 2025-02-10 16:10:06 +00:00
renovate[bot]
64f0333306 chore(deps): update grafana/grafana docker tag to v11.5.1 (#15963)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-09 07:00:37 -05:00
Jason Rasmussen
758bcd1e97 fix(server): validate oauth profile has a sub (#15967) 2025-02-08 17:01:28 -05:00
Alex
fb21950ad8 chore(web): shared links style tweaks (#15960) 2025-02-07 20:53:12 -05:00
Jason Rasmussen
758449e9f0 refactor: session repository (#15957) 2025-02-07 23:16:40 +00:00
Jason Rasmussen
d7d4d22fe0 refactor: process repository (#15956) 2025-02-07 18:04:04 -05:00
Jason Rasmussen
03948a69e2 refactor: system metadata repository (#15954) 2025-02-07 17:26:49 -05:00
Jason Rasmussen
61b8eb85b5 feat: view album shared links (#15943) 2025-02-07 16:38:20 -05:00
Jason Rasmussen
c5360e78c5 feat(web): shared link filters (#15948) 2025-02-07 13:05:15 -05:00
Jason Rasmussen
23014c263b feat(api): set person color (#15937) 2025-02-07 10:06:58 -05:00
Mert
2e5007adef docs: soften wording for openvino igpu (#15941) 2025-02-07 06:44:22 -05:00
Nicholas Flamy
c4531fc4d3 fix(docs): show version selection dropdown on mobile (#15894)
change-className-and-add-css-to-show-versions-on-mobile
2025-02-06 16:00:52 -05:00
renovate[bot]
252d3f5f2c chore(deps): update grafana/grafana docker tag to v11.5.0 (#15930)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 15:59:47 -05:00
renovate[bot]
ef6c2bf547 chore(deps): update base-image to v20250204 (major) (#15931)
chore(deps): update base-image to v20250204

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 15:59:29 -05:00
Krassimir Valev
6aad9fae8e feat(web): revamp places (#12219)
* revamp places

* add english translations

* migrate places page and components to svelte 5

* fix lint

* chore: cleanup

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-02-06 20:54:01 +00:00
Daniel Dietzler
45f7401513 chore: nestjs 11 (#15542) 2025-02-06 13:56:26 -05:00
renovate[bot]
3c7edba388 chore(deps): update terraform cloudflare to v4.52.0 (#15526)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-02-06 13:52:27 -05:00
renovate[bot]
76a70703a5 chore(deps): update base-image to v20250128 (major) (#15796)
chore(deps): update base-image to v20250128

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-06 13:51:52 -05:00
Ridvan
f78066d4b9 Update setup.md to include FVM dependency (#15927) 2025-02-06 18:50:55 +00:00
Jason Rasmussen
48d421e28c fix(server): always get UTC dates from postgres (#15920) 2025-02-05 18:47:27 +00:00
defooster
1492b55c07 fix(docs): typo in unraid.md (#15913)
Update unraid.md

fixed wrong word
2025-02-05 09:35:55 -06:00
bo0tzz
1d6a4e9318 fix: call hexOrBufferToBase64 for stripMetadata thumbhash (#15917)
Fixes #15916 (I think)
2025-02-05 09:20:46 -06:00
Alex
fe42e7410b chore(server): follow up on #15899 (#15907) 2025-02-04 16:57:11 -06:00
Jason Rasmussen
58bf58b393 refactor: get map markers database query (#15899) 2025-02-04 09:07:41 -06:00
Nicholas Flamy
99de52479e fix: pr template not being used and make some changes (#15893)
fix-pr-template-and-make-some-changes-with-suggestions
2025-02-04 09:06:54 -06:00
André Ventura
97574d7296 fix(web): prevent accidental modal closures on mouseup outside (#15900) 2025-02-04 13:43:19 +00:00
Nicholas Flamy
5015210f37 docs: add-current-path-to-version-switcher (#15860)
add-current-path-to-version-switcher
2025-02-04 04:09:07 -05:00
Lukas
0bb1219b5f fix(server): for individual shares not showing thumbnails (#15895)
* Fix for individual shares not showing thumbnails

* synced sql

* chore: add e2e test

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2025-02-04 09:07:50 +00:00
Jonathan Jogenfors
b730aa60ed fix(server): queue missing metadata (#15864)
fix: queue missing metadata
2025-02-04 04:00:39 -05:00
Arno
7ec3610753 feat: Mark people as favorite (#14866)
* feat: added ability to mark people as favorite, which get sorted to the front of the people list

* feat(server): added unit test for favorite people

* feat(server): refactored for better readability

* fixed person service unit tests

* fixed open-api and sql checks

* fixed bad codegen and removed unnecessary type assertion again

* chore: clean up

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2025-02-04 08:52:17 +00:00
Tom Graham
69e88ef985 fix(mobile): #15182 Video memories no longer play (#15210)
* Update current asset to play video.

* Updated location of currentAssetProvider update per feedback.

* Added a playbackDelayFactor to the video viewer to resolve an issue in memories.

Also adjusted the scale of the memory preview image to match the ratio of the video. This still appears to jump because the video preview doesn't seem to be the first frame for some reason :\

* add video indicator

---------

Co-authored-by: Tom graham <tomg@questps.com.au>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-03 22:43:23 +00:00
jtkmckenna
9358b4dc7e fix: bash install.sh script for mac os (#15874)
fix: bash script for mac os

Fix the displayed IP address in bash script if hostname fails to return a string

Co-authored-by: Joseph McKenna <dev@jtkmckenna.com>
2025-02-03 16:41:42 -06:00
Alex
06f077bac2 fix(server): memory lane assets order (#15882)
* fix(server): memory lane assets order

* fix: sql

* pr feedback

* sql
2025-02-03 16:29:41 -06:00
Meesam
47f6181d42 fix(mobile): improved the visibility of backup cloud icon on lighter images (#15886)
* fix(mobile): improved the visibility of backup cloud icon on lighter images

* refactor(mobile): add 'const' keyword to Offset constructor for improved performance
2025-02-03 20:30:39 +00:00
André Ventura
aac029d92b feat(web): merge suggestion modal: focus on Yes button by default. (#15827)
* feat(web): merge suggestion modal: focus on Yes button by default.

* refactor(web): merge suggestion modal: use Button from @immich/ui.

---------

Co-authored-by: André Ventura <afv@users.noreply.github.com>
2025-02-03 14:01:05 -06:00
Damiano Ferrari
ef245ea2d2 feat(mobile): Use NavigationRail when the screen is in landscape mode (#15885) 2025-02-03 13:49:55 -06:00
Stark
e8d05e78ad feat(web): Updated Onboarding page (#15880)
Updated Onboarding page

the "previous" button on the Storage Template page now points to privacy instead of theme
2025-02-03 17:36:25 +00:00
Matthew Momjian
52c9fbea5f fix(docs): query DB by ID (#15863)
* db query for id

* format

* backticks

* Update database-queries.md
2025-02-02 22:55:47 -06:00
bo0tzz
882163f545 chore: build metadata for ML container (#15831)
* chore: build metadata for ML container

* fix: build_image_url
2025-02-02 23:45:58 +01:00
Damiano Ferrari
96a6cc20b7 refactor(mobile): Use switch expression when possible (#15852)
refactor: Use `switch` expression when possible

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-02-02 15:46:46 -06:00
Alex
4efacfbb91 feat: search by description (#15818)
* feat: search by description

* wip: mobile

* wip: mobile ui

* wip: mobile search logic

* feat: using f_unaccent

* icon to fit with text search
2025-02-02 15:18:13 -06:00
Matthew Momjian
a808a840c8 fix(mobile): title of custom proxy headers (#15859)
fix title
2025-02-02 20:43:14 +00:00
Nicholas Flamy
3f18acdb1a docs: TrueNAS: add danger message to external libraries (#15857)
Add danger message to external libraries in truenas.md (Format fix included)
2025-02-02 12:07:39 -06:00
Zack Pollard
2b41b5efe1 feat: merch links (#15843) 2025-02-02 00:26:23 +01:00
David Wolff
9ac95d6845 feat: add searching by tags (#15395)
* feat: add searching by tags

* fix: fix merge

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-31 21:37:22 +00:00
Mangat Singh Toor | ਮੰਗਤ ਸਿੰਘ ਤੂਰ
221e197633 fix(mobile): retain edited title when album updates (#15806)
* fix(album-viewer): retain edited title when album updates

ensure `AlbumViewerEditableTitle` keeps user input while editing,
even when the album updates from another provider. fall back to
`albumName` only when not in edit mode.

* linting

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-31 09:24:53 -06:00
David Wolff
1b141d5ca9 refactor(server): filter assets by people using a subquery instead of a cte (#15768) 2025-01-31 09:06:45 -06:00
Alex
098bab7c9b fix(mobile): search page issues (#15804)
* fix: don't repeat search

* fix: show snackbar for no result

* fix: do not search on empty filter

* chore: syling
2025-01-31 03:12:57 +00:00
Felix Eckhofer
4fccc09fc1 chore: fix typo in libraries.md (#15800)
Fix typo in libraries.md
2025-01-30 20:34:12 -06:00
Jason Rasmussen
c016b65ef2 fix(web): shared link date range (#15802) 2025-01-30 18:36:45 -05:00
preeperkiller
844eed8707 fix(web): HelpAndFeedback button the same size as Theme button in navbar (#15791)
fix(server): HelpAndFeedback button the same size as Theme button in navbar
2025-01-30 12:43:35 -05:00
Justin Forseth
6e31ac4c75 feat(mobile): Add filter to people_picker.dart (#15771)
* Add filter to people_picker.dart

* feat: styling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-29 21:02:54 +00:00
Jirapan.
b287c0cbe8 chore: update of the Thai translation (#15758) 2025-01-29 20:29:50 +00:00
Jason Rasmussen
1fcc75fb44 docs: update server arch (#15775) 2025-01-29 13:42:38 -06:00
Jonathan Jogenfors
ca79e25a6e feat(server): synology exclusion patterns (#15773)
feat: add synology exclusion patterns
2025-01-29 13:42:21 -06:00
github-actions
4fd8c1b3c1 chore: version v1.125.7 2025-01-29 17:41:38 +00:00
Antonio Sarro
f3ba994186 fix(web): update recent album after edit (#15762)
* fix(web): update recent album after edit

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2025-01-29 17:27:30 +00:00
Ben Cochran
b4a4abbf51 fix(docs): move a few API doc comments to descriptions (#15381)
Previously, the comments were being used as the summaries, and thus were
displayed as the “title” of these endpoints
2025-01-29 11:58:10 -05:00
Jason Rasmussen
a0aea021a1 fix(server): restore user (#15763) 2025-01-29 16:49:08 +00:00
Joren Guillaume
9033a99587 fix(server): Update vaapi-wsl to include dxg (#15759) 2025-01-29 16:39:02 +01:00
ayykamp
cc0cbd705e feat: add support for JPEG 2000 (#15710)
* chore(server): add support for .jp2

* docs: add support for .jp2

* chore: fix tests

* fix formatting

* unify sorting
2025-01-28 23:27:28 +00:00
Carsten Otto
da580d4685 fix: show local dates for range in album summary (#15654)
* fix(web): show local dates for range in album summary

* fix(server): show local dates for range in album summary
2025-01-28 14:33:38 -06:00
Simon
cb6d94c7a7 chore: update of the Ukrainian translation (#15751)
Update uk-UA.json

Update of the Ukrainian translation for the Immich app
2025-01-28 20:32:57 +00:00
André Ventura
060300de8a fix(web): cancel people merge selection: do not show "Change name successfully" notification (#15744)
fix(web): cancel people merge selection: do not show "Change name successfully" notification.

Co-authored-by: André Ventura <afv@users.noreply.github.com>
2025-01-28 11:43:52 -06:00
Miguel Angel Nubla
c2ba1cc202 docs: add immich-upload-optimizer to Community Projects list (#15738) 2025-01-28 09:40:00 -06:00
PastLeo
08db77db23 feat: resolution selection and default preview playback for 360° panorama videos (#15747)
* original/preview switching in photo-sphere-viewer

1. default to preview in photo-sphere-viewer video mode
2. install and integrate @photo-sphere-viewer/settings-plugin & @photo-sphere-viewer/resolution-plugin

* fix lint errors
2025-01-28 09:09:40 -06:00
RiggiG
92dff839d0 fix(web): do not throw error when hash fails (#15740)
change: do not throw error when hash fails
2025-01-28 03:54:56 +00:00
Christian Kündig
fe1e09e51f fix(server): Allow negative rating (for rejected images) (#15699)
Allow negative rating (for rejected images)
2025-01-27 21:54:29 -06:00
github-actions
f44669447f chore: version v1.125.6 2025-01-28 02:58:27 +00:00
Mert
92412ca2f7 fix(server): person thumbnail generation always being queued (#15734)
* fix person thumbnail generation always being queued

* fix thumbhash comparison

* fix mock
2025-01-27 16:20:18 -06:00
github-actions
64d926581f chore: version v1.125.5 2025-01-27 20:04:50 +00:00
Alex
c139e05170 fix(mobile): locale option causes the datetime filter error out (#15704) 2025-01-27 14:02:23 -06:00
Alex
0fe62298e1 fix(server): duplicate detection (#15727) 2025-01-27 13:53:59 -06:00
github-actions
e5794e6cfc chore: version v1.125.4 2025-01-27 18:44:12 +00:00
Alex
f6cbc9db06 fix(server): cannot render album page when all assets of an album are in trash (#15690)
* fix(server): cannot render album page when all assets of an album are in trash

* inner join

* add e2e test

* check empty albums too

* render add to album button on empty album

* lint

* count 0 if undefined

* fix album card test

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-01-26 21:18:34 -06:00
Alex
8dab5d3798 chore(mobile): post release task (#15662) 2025-01-26 15:09:15 -06:00
Carsten Otto
e864811a85 fix(web): sort folders (#15691)
fixes #13145
2025-01-26 15:07:22 -06:00
github-actions
72a55c13b6 chore: version v1.125.3 2025-01-26 14:14:48 +00:00
sudbrack
206412267a fix(server): /search/random API returns same assets every call (#15682)
* Fix for server searchRandom function not returning random results

* Fix lint
2025-01-26 14:06:18 +00:00
Damiano Ferrari
f780a56e24 fix(mobile): Misaligned text icon in circle avatar (#15683)
style(mobile): Use `DefaultTextStyle` for the text icon in `CircleAvatar`
2025-01-26 07:51:46 -06:00
Alex
7bbffccf76 fix(web): neon overflow on mobile screen (#15676) 2025-01-26 08:06:26 -05:00
Mert
05a446c259 fix(server): avoid duplicate rows in album queries (#15670)
* avoid duplicate rows

* left join, handle null vs. undefined

* update sql
2025-01-25 22:37:19 -06:00
Carsten Otto
4f725b95e1 fix(server): do not count deleted assets for album summary (#15668)
fixes #15645
fixes #15646
2025-01-25 16:45:13 -06:00
Carsten Otto
64b92cb24c fix(server): do not reset fileCreatedDate (#15650)
When marking an offline asset as online again, do not reset the
fileCreatedAt value. This value contains the "true" date, copied
from exif.dateTimeOriginal. If we overwrite this value, we'd need
to run the metadata extraction job again. Instead, we just leave
the old (and correct) value in place.

fixes #15640
2025-01-25 13:50:37 -06:00
Gagan Yadav
19f2f888ee fix(mobile): improve timezone picker (#15615)
- Fix missing timezones

- Remove the UTC prefix from timezone display text to align with web app

- Remove unnecessary layout builder

- Created a custom `DropdownSearchMenu` widget

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-25 13:36:49 -06:00
Alex
d12b1c907d fix(server): bulk update location (#15642) 2025-01-25 11:58:07 -06:00
Robert Schütz
947c053c15 chore(server): add DB_URL supports Unix sockets unit test (#15629)
* test(server): DB_URL supports Unix sockets

* chore: format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-25 10:38:00 +00:00
Regenxyz
79592701dd chore: fix typos in Thai Language Readme (#15637)
Update README_th_TH.md

Fixing weird Thai Translate
2025-01-25 10:30:53 +00:00
jdicioccio
39697cd973 fix: increase upload timeout (#15588)
Fix upload timeout issue

Fix an issue where when uploading a large file, the upload would consistently abort after 30 minutes. I changed this timeout from 30 minutes to 1 day. Maybe that's excessive, or maybe the timeout isn't even needed, but the current 30 minute timeout definitely seems way too short.
2025-01-25 04:26:52 -06:00
Jonathan Jogenfors
10e518db42 chore(server): print stack in case of worker error (#15632)
feat: show error stack
2025-01-24 22:45:55 -05:00
Mert
72fa31f9e9 fix(server): changing vector dim size (#15630) 2025-01-24 20:01:24 -05:00
github-actions
9871a04d54 chore: version v1.125.2 2025-01-24 19:09:06 +00:00
Mert
ba01b40e7c fix(server): sslmode not working (#15587)
* parse db url before passing it to the driver

* don't be lazy

* simplify

* simplify

* add tests

* update sql sync script

* update mock

* remove unused import

* remove unused imports
2025-01-24 13:01:55 -06:00
Alex
f5a3d7ba23 fix(mobile): failed to load ga/gl locale (#15623) 2025-01-24 12:47:29 -06:00
Alex
d4a9eed4a1 fix(server): migration mentions public schema (#15622) 2025-01-24 18:11:22 +00:00
Alex
9d8072b994 fix(server): failed to get albums with archived assets (#15611)
* fix(mobile): failed to get albums with archived assets

* sql
2025-01-24 17:54:53 +00:00
Saschl
3c1fa22109 fix(mobile): deletion of single assets (#15597)
fix: set asset in currentassetprovider on image load

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-24 17:47:54 +00:00
Matthew Momjian
c0210bd6c0 fix(mobile): translation (no /api, experimental features) (#15600)
* initial /api removal

* translations /api

* experimental features

* japanese url update

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-24 11:47:01 -06:00
Mert
a6ace5151c fix(server): no exif metadata in the deduplication utility (#15585)
add exif to `getDuplicates`
2025-01-24 11:42:39 -06:00
Jason Rasmussen
ede9c99adb fix: demo login page (#15616) 2025-01-24 11:39:06 -06:00
Alex
ec7ab209f3 fix(server): link live photos (#15612)
* fix(server): link live photos

* chore: sql

* formatting
2025-01-24 11:38:59 -06:00
Alex
61bc24d7ea chore(mobile): post release task (#15581) 2025-01-24 17:28:00 +00:00
Alex
6c95eb22b7 fix(mobile): full refresh doesn't get albums (#15560) 2025-01-24 17:27:33 +00:00
Jason Rasmussen
aaea5cf1ad fix: login page (#15613) 2025-01-24 17:17:04 +00:00
Alex
96d2e9b4c5 fix(mobile): unit test (#15604)
* fix(mobile): unit test

* fix(mobile): unit test
2025-01-24 12:11:38 -05:00
Alex
19740a3560 fix(web): neon artifacts (#15582) 2025-01-24 09:18:26 -06:00
bo0tzz
8a481e2ea1 docs: add FAQ about app update approval (#15599) 2025-01-24 09:08:01 -06:00
Mert
ba105d9f19 fix(server): searchRandom response (#15580)
* fix searchRandom

* add e2e

* set outer limit
2025-01-24 00:41:54 -05:00
Lukas
065d885ca0 fix(server): Fix for sorting faces during merging (#15571)
* Fix for sorting faces

* Put uneccessary orderBy in if statement
2025-01-23 21:33:24 -05:00
Mert
a07ae9b5b2 fix(server): set updatedAt on updates (#15573)
* `updatedAt` triggers

* drop function at the end
2025-01-23 19:24:29 -05:00
Jason Rasmussen
1869b1b41a refactor: repositories (#15561)
* refactor: version history repository

* refactor: oauth repository

* refactor: trash repository

* refactor: telemetry repository

* refactor: metadata repository

* refactor: cron repository

* refactor: map repository

* refactor: server-info repository

* refactor: album user repository

* refactor: notification repository
2025-01-23 18:10:17 -05:00
Alex
995314446b feat(web): neon light behinds login form (#15570) 2025-01-23 17:23:23 -05:00
Jason Rasmussen
a1691ddc0f fix(web): auth page padding (#15569) 2025-01-23 21:38:34 +00:00
Mert
071b271484 fix(server): getTimeBuckets not handling boolean filters correctly (#15567)
fix boolean handling
2025-01-23 15:08:20 -06:00
github-actions
50a2f6193f chore: version v1.125.1 2025-01-23 16:52:23 +00:00
bo0tzz
907fed1081 fix: use push-o-matic to create release (#15562) 2025-01-23 10:46:56 -06:00
github-actions
49a16045bd chore: version v1.125.0 2025-01-23 16:23:47 +00:00
Alex
a47aa86392 chore: minor form bottom padding increase (#15558) 2025-01-23 15:45:07 +00:00
David Baxter
f32c5d97cd feat(web): Show lens model in the asset viewer detail panel (#15460)
* Adds lens details to the asset viewer

* Update lens detail search links

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-23 09:34:36 -06:00
Alex
afc6e91c66 fix(web): No EXIF info on stack navigation (#15533)
* fix(web): No EXIF info on stack navigation

* fix(web): No EXIF info on stack navigation

* add exif info to get stack query

* e2e test
2025-01-23 15:22:27 +00:00
renovate[bot]
1311189fab chore(deps): update base-image to v20250123 (major) (#15555)
chore(deps): update base-image to v20250123

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 08:32:32 -05:00
renovate[bot]
fa3b5a4c8f chore(deps): update node (#15554)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-23 08:32:14 -05:00
Jason Rasmussen
d3446f3092 refactor: logging repository (#15540) 2025-01-23 08:31:30 -05:00
renovate[bot]
b31414af8f fix(deps): update dependency @nestjs/bullmq to v11 (#15534)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:37:02 +00:00
renovate[bot]
cf99dcb279 fix(deps): update dependency @nestjs/event-emitter to v3 (#15535)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:34:50 +00:00
renovate[bot]
dc56ed5d45 fix(deps): update dependency @nestjs/schedule to v5 (#15537)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 22:32:35 +00:00
renovate[bot]
d1d26c60d6 fix(deps): update typescript-projects (#14892)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2025-01-22 22:15:38 +00:00
Jason Rasmussen
66849d0d45 refactor: migrate media repository (#15536) 2025-01-22 22:11:07 +00:00
renovate[bot]
30b8864d2d chore(deps): update base-image to v20250121 (major) (#15497)
chore(deps): update base-image to v20250121

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 16:45:25 -05:00
Jason Rasmussen
78464a4ba3 refactor: ui icon buttons (#15531) 2025-01-22 16:44:59 -05:00
Jason Rasmussen
1f19a65d1a refactor: migrate memory repository (#15532) 2025-01-22 21:39:13 +00:00
Jason Rasmussen
ca3619658b fix: incorrect event configuration (#15530) 2025-01-22 20:43:06 +00:00
renovate[bot]
c7a1f2944f chore(deps): update vitest monorepo to v3 (major) (#15528)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-22 20:28:41 +00:00
Daniel Dietzler
7b71c145c8 chore: upgrade to vite 6 (#15508)
chore: upgrade to vite 5
2025-01-22 15:18:04 -05:00
Mert
49a6961ec6 fix(server): query fixes (#15509) 2025-01-22 14:17:42 -06:00
Alex
7b882b35e5 chore(mobile): translations update (#15523)
chore(mobile): translation update
2025-01-22 18:38:01 +00:00
Weblate (bot)
443aad5794 chore(web): update translations (#15335)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/cv/
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/fi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fil/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/
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/nn/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/
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/zh_Hant/
Translation: Immich/immich

Co-authored-by: Andreas Johansen <andreas@josern.com>
Co-authored-by: Andrius Cimakevicius <andrius.cim@gmail.com>
Co-authored-by: FDS <sfranzdanielsss@gmail.com>
Co-authored-by: Fredrik Rambris <fredrik@rambris.com>
Co-authored-by: Kaspar Brygger <kaspar.brygger@gmail.com>
Co-authored-by: Lauri Koo <late91@gmail.com>
Co-authored-by: Ramy <ramy.feteha@gmail.com>
Co-authored-by: Rico Sonntag <mail@ricosonntag.de>
Co-authored-by: Sedat Albayrak <sedat.albayrak@icloud.com>
Co-authored-by: Torin Wu <xuan329269@gmail.com>
Co-authored-by: ValinRo <edicomna@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: anton garcias <isaga.percompartir@gmail.com>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: Øyvind Hovden <oyvhov@gmail.com>
Co-authored-by: Ümit Solmz <usnetv@users.noreply.hosted.weblate.org>
Co-authored-by: Мĕтри Сантăр ывалĕ Упа-Миччи <mefisteron@gmail.com>
2025-01-22 18:28:13 +00:00
Jason Rasmussen
8d6cbb51e2 fix: get asset by id for stacks (#15522) 2025-01-22 18:13:09 +00:00
renovate[bot]
c8abe9a2fd chore(deps): update node.js to v22.13.1 (#15503)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 16:16:46 -06:00
Alex
58a75d59bd chore: update ui 14.1 (#15498) 2025-01-21 16:16:26 -06:00
Jason Rasmussen
36058b9b59 chore: remove unused code (#15499) 2025-01-21 16:47:48 -05:00
Matthew Momjian
8440f146e2 feat(docs): CIFS/Samba in-Docker example (#15502)
* CIFS

* quotes

* quote 2

* quote 3, lol
2025-01-21 12:59:30 -06:00
Matthew Momjian
3da17da7b4 fix(docs): remove old attribution (#15501)
update
2025-01-21 12:59:13 -06:00
Jason Rasmussen
ccf6d71c3c refactor: view repository (#15496) 2025-01-21 18:26:13 +00:00
renovate[bot]
5171630b98 fix(deps): update machine-learning (#15494)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 13:17:55 -05:00
Jason Rasmussen
9a27a99cab refactor: config repository (#15495)
* refactor: access repository

* refactor: config repository
2025-01-21 13:13:09 -05:00
Daniel Dietzler
332a865ce6 refactor: migrate person repository to kysely (#15242)
* refactor: migrate person repository to kysely

* `asVector` begone

* linting

* fix metadata faces

* update test

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2025-01-21 13:12:28 -05:00
renovate[bot]
0c152366ec chore(deps): update docker/build-push-action action to v6.12.0 (#15493)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 12:34:14 -05:00
Alex
c35fd6cbdb refactor: migrate album repo to kysely (#15474) 2025-01-21 11:24:48 -06:00
renovate[bot]
58d5cc1e4b chore(deps): update dependency @types/node to ^22.10.7 (#15479)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-21 11:54:47 -05:00
Jason Rasmussen
9a1068c867 refactor: api key repository (#15491) 2025-01-21 10:45:59 -06:00
Jason Rasmussen
1745f48f3d feat: better spec urls (#15487) 2025-01-21 10:26:52 -06:00
Jason Rasmussen
b0cdd8f475 refactor: access repository (#15490) 2025-01-21 10:09:24 -06:00
Alex
318dd32363 refactor: migrate stack repo to kysely (#15440)
* wip

* wip: add tags

* wip

* sql

* pr feedback

* pr feedback

* ergonomic

* pr feedback

* pr feedback
2025-01-21 09:36:28 -06:00
Jeff Sloyer
887267b133 fix: broken link on monitoring page (#15478)
* fix: broken link on monitoring page

* use absolute link
2025-01-20 22:20:03 -06:00
Tempest
1d0d4fc281 feat: Allow multiple ML models to be preloaded (#15418) 2025-01-20 21:39:14 -05:00
renovate[bot]
345791c0e6 chore(deps): update machine-learning (#15476) 2025-01-20 21:38:50 -05:00
Aaron Rodrigues
07698f8a40 fix: grammar on docs homepage (#15455)
Fix grammar on index.tsx
2025-01-20 16:14:49 -06:00
Yan-Ru Huang
6fdb8f83f0 feat: Add rule on robots.txt to allow robots access og tags (#15470)
Allow social media access og tags
2025-01-20 09:22:05 -05:00
Alex
a0b2c69b99 fix(mobile): cannot get new photos on Android (#15461) 2025-01-20 07:25:43 -06:00
David Wolff
70809c1465 fix(server): searching for multiple people yields false positives (#15447) 2025-01-19 13:01:21 -05:00
Robert Schütz
97ec3b147c fix(deps): use node-addon-api v8 (#15438) 2025-01-19 11:26:25 -05:00
Alex
d249b63c99 fix(mobile): Cannot type date format on Samsung phone (#15430)
* fix(mobile): Cannot type date format on Samsung phone

* use calendar
2025-01-18 21:56:18 +00:00
Alex
0f803a4f5e fix(web): scrolling memory timeline reset position (#15429) 2025-01-18 20:09:38 +00:00
renovate[bot]
8eac82c5a3 chore(deps): update dependency eslint-config-prettier to v10 (#15428)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-18 14:04:46 -06:00
Daniel Dietzler
3d13da7f11 refactor: migrate shared-link repository to kysely (#15289)
* refactor: migrate shared-link repository to kysely

* fix duplicate individual shared link return in getAll when there are more than 1 asset in the shared link

* using correct order condition

* using eb.table

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-18 13:25:15 -06:00
Sam Wilson
430d0b86ee fix(docs): Update unraid.md to use correct image url (#15427)
Update unraid.md to use correct image
2025-01-18 13:24:31 -06:00
imakida
f40fdce658 fix(web): Update asset count when deleting assets from person page (#15416)
Call updateAssetCount() after deleting assets.
2025-01-17 20:51:38 -06:00
Jason Rasmussen
097183b31d refactor: migrate partner repo to kysely (#15366) 2025-01-17 18:49:21 -05:00
Tempest
d5a9294eeb feat: Add additional env variables to ML container (#15398)
* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Update config.py

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Apply formatting

* minor update

* formatting

* root validator

* minor update

* minor update

* minor update

* change to support explicit models

* minor update

* minor change

* minor change

* minor change

* minor update

* add logs, resolve errors

* minor change

* add new enviornment variables

* minor revisons

* remove comments

* add additional variables to ML (fixed)

* add additional variables to ML (fixed)

* add additional variables to ML

* formatting

* remove comment

* remove mypy error

* remove unused module

* merge f strings
2025-01-17 17:22:05 -05:00
Alex
c5582fc8d9 chore: update ui 13 (#15414) 2025-01-17 20:03:11 +00:00
Alex
6993726d50 chore: generate sql for stack repo (#15413)
* chore: generate sql for stack repo

* update sql
2025-01-17 20:02:28 +00:00
Zack Pollard
c821458e6c refactor: migrate map repository to kysely (#15348)
* chore: migrate map repository to kysely

* chore: add kysely codegen command, exclude from prettier and re-run it on latest migrations

* refactor: migrate map repository to kysely

* chore: dont log postgres notices
2025-01-17 09:14:42 -06:00
Tom Graham
efbc0cb192 fix(mobile): 14983 Images upload to shared album with common name (#15127)
* Initial look at fixing issue where images are uploaded to the wrong album if a shared album conflicts with a local users album.

* Use owner instead of shared flag when fetching albums.

* Fix issue with refreshRemoteAlbums getting shared items twice and removed incorrect isShared comment.

Using `getAll(shared: true)` gets all shared albums the user can access (regardless of owner, despite the previous comment).

Using `getAll(shared: null)` gets all albums (incuding shared = true and shared = false). I presume the intent here was to get albums that were shared (and not mine), and not shared (ie: mine), but the logic is way off. It also just then combines them - so makes more sense to just get them in a single call.

* Fix formatting.

* Fixed tests.

* Revert "Fixed tests."

This reverts commit c38f5af5ac.

* Revert "Fix issue with refreshRemoteAlbums getting shared items twice and removed incorrect isShared comment."

This reverts commit 979ce90abf.

* Added comments to explain why filters behave the way they do for getAll() albums.

---------

Co-authored-by: Tom graham <tomg@questps.com.au>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-16 21:24:09 -06:00
Alex
fd99bd05cf feat(mobile): share to mechanism (#15229)
* setup ios

* chore: succesfully sent media to the app

* share from Android

* wip: navigate to share screen

* wip: UI for displaying upload candidate

* wip: logic

* wip: upload logic

* wip: up up up we got it up

* wip

* wip

* wip

* upload state

* feat: i18n

* fix: release build ios'

* feat: clear file cache

* pr feedback

* using const for checking download status

---------

Co-authored-by: Alex <alex@pop-os.localdomain>
2025-01-16 21:20:44 -06:00
Jason Rasmussen
3a2bf91889 refactor: replace link-button component with immich-ui buttons (#15374)
* refactor: replace link-button component with immich-ui buttons

* minor styling tweak

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-16 16:03:04 +00:00
Alex
378bd3c993 refactor: migrate access repo to kysely (#15365) 2025-01-16 09:25:03 -06:00
Jason Rasmussen
89f40b311c fix(web): map settings (#15375) 2025-01-16 10:05:14 -05:00
Jason Rasmussen
6ce1533117 fix: activity types (#15368) 2025-01-15 23:31:26 -05:00
Alex
0ce62d8efd chore: update immich ui 12 (#15378)
* chore: update ui 12

* chore: update ui 12
2025-01-15 23:02:49 +00:00
imakida
e151248b16 fix(web): end selection after click when choosing thumbnail, and notify (#15373)
fix(web): Change viewMode state after updateThumbnail

Fixes #14692

viewMode state was being changed before updateThumbnail which caused
AssetGrid.handleSelectAssets() to continue, instead of returning.

Also added notification to notify user that the album cover was
updated.
2025-01-15 21:13:16 +00:00
Daniel Dietzler
a2207f2eef refactor: migrate library repository to kysely (#15271) 2025-01-15 15:01:28 -06:00
Mattia Natali
81568dbda3 feat(web): Context menu scrolls on small devices (#15367)
Context menu scrolls on small devices
2025-01-15 14:48:26 -06:00
Jason Rasmussen
a60da1ccab refactor: migrate create user form to immich ui (#15350)
* refactor: migrate create user form to immich ui

* minor styling tweak

* remove unintentional commit

* revert formating diff

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-15 15:09:19 -05:00
Jason Rasmussen
2d2966caa0 chore: use port 2286 for the auth server (#15369) 2025-01-15 15:03:20 -05:00
Alex
7d087371b5 chore: sql sync (#15370)
* chore: sql sync

* chore: sql sync
2025-01-15 19:55:29 +00:00
Jason Rasmussen
93e2545275 refactor: migrate memory to kysely (#15314) 2025-01-15 11:34:11 -05:00
renovate[bot]
43b3181f45 chore(deps): update base-image to v20250114 (major) (#15347)
chore(deps): update base-image to v20250114

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 19:58:02 -05:00
Mert
2903ad8156 refactor(server): migrate album-user repo to kysely (#15351) 2025-01-14 19:27:16 -05:00
Tempest
c5476a99b1 feat: Add additional env variables for Machine Learning (#15326)
* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Update config.py

* Add additional variables to preload part ML models

* Add additional variables to preload part ML models

* Apply formatting

* minor update

* formatting

* root validator

* minor update

* minor update

* minor update

* change to support explicit models

* minor update

* minor change

* minor change

* minor change

* minor update

* add logs, resolve errors

* minor change

* add new enviornment variables

* minor revisons

* remove comments
2025-01-14 22:06:01 +00:00
Jason Rasmussen
5d2e421800 chore: add renovate config for immich-ui (#15349) 2025-01-14 21:01:21 +01:00
Jason Rasmussen
b9000d8770 feat(web): immich-ui components (#14263)
* feat: add immich-ui to auth pages

* fix: welcome icon

* styling

* fix: mobile padding

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-14 14:53:33 -05:00
renovate[bot]
073fccb517 chore(deps): update python:3.11-slim-bookworm docker digest to 6ed5bff (#15346)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 18:33:27 +00:00
renovate[bot]
3e11b90851 chore(deps): update node.js to v22.13.0 (#15337)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-14 13:20:12 -05:00
renovate[bot]
19e2504583 fix(deps): update machine-learning (#15336) 2025-01-14 10:19:01 -05:00
Mattia Natali
4279cd6e1e feat(web): Slideshow is enabled everywhere. It no longer needs assetStore. (#15077)
Slideshow no longer needs assetStore. It is enabled everywhere

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-14 14:24:58 +00:00
Jason Rasmussen
f70ee3f350 refactor: auth pages (#15328) 2025-01-14 08:14:28 -06:00
Jason Rasmussen
9e1651ef66 fix: bump web dependencies (#15325) 2025-01-13 23:40:19 -05:00
Alex
a35af2b242 refactor: migrate move repository to kysely (#15327)
* refactor: migrate move repository to kysely

* fix: tests

* fix: tests
2025-01-13 23:22:03 -05:00
Yonathan Randolph
fc99c5f530 chore(server): avoid copying sources in dev (#12794)
* chore(server): avoid copying sources in dev

Add a dev target to the web and server Dockerfiles, and change docker-compose.dev.yml to use the dev target. The dev target avoids copying files so that the docker image is smaller.

* chore: respond to PR: don't add dev target

web/Dockerfile is only used by docker-compose.dev.yml so a dev target is redundant. Instead, just remove the copy

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-01-13 22:00:55 -05:00
Weblate (bot)
e978b8c685 chore(web): update translations (#15145)
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/el/
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/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/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ms/
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/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/ta/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/
Translation: Immich/immich

Co-authored-by: Andreas Johansen <andreas@josern.com>
Co-authored-by: AtmosphericIgnition <github.vvwnk@passmail.net>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Damian Krysta <krypton9208@gmail.com>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: Dmitry Banny <dj.icecore@gmail.com>
Co-authored-by: Dominik Mielcarek <weblate@bmx.email>
Co-authored-by: Erik Järlestrand <erik.jarlestrand@gmail.com>
Co-authored-by: Filip Hanes <filip@hanes.tech>
Co-authored-by: Gerardo Doro <gerrydoro97@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <IndrekHaav@users.noreply.hosted.weblate.org>
Co-authored-by: Jendrik Köhler <jendrik.koehler11@gmail.com>
Co-authored-by: Jordi Masip <jordi@masip.cat>
Co-authored-by: Kenji Opdam <kenji.opdam@gmail.com>
Co-authored-by: Krisztián <fabkrisz5+hosted_weblate_org@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Michal Micech <michal.micech@gmail.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Milan Šalka <salka.milan@googlemail.com>
Co-authored-by: Milo Germanus <milogermanus@gmail.com>
Co-authored-by: Máté Molnár <matmolni@gmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: Rui <rui-costa@users.noreply.hosted.weblate.org>
Co-authored-by: Santiago <santiwever@hotmail.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Vykintas Vyšniauskas <vykintasv@gmail.com>
Co-authored-by: Xo <xocodokie@users.noreply.hosted.weblate.org>
Co-authored-by: chamdim <chamdim@protonmail.com>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: rezi nagro <rezinagro@hotmail.com>
Co-authored-by: scudo <whiteshield.tg@protonmail.com>
Co-authored-by: stelle <itsazripp2@gmail.com>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Пламен Марков <tsmhunter@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-14 02:57:54 +00:00
Zer0x00
3b06220219 feat: Upgrade devcontainer setup (#14419)
* Feat: Upgrade devcontainer

* Style: Format devcontainer.json

* Chore: Remove settings from devcontainer

* chore: add shebang

* chore: fix shellcheck

---------

Co-authored-by: Bünyamin Olgun <buenyamin.olgun@cancom.de>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-01-13 21:42:32 -05:00
renovate[bot]
dc53e2a9b9 chore(deps): update docker.io/redis:6.2-alpine docker digest to 905c4ee (#15193)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-13 21:32:52 -05:00
Daniel Dietzler
28b08ed417 refactor: migrate audit repository to kysely (#15269) 2025-01-13 21:23:12 -05:00
Matthew Momjian
b74f013b53 fix(docs): database name for restore commands (#15276)
* cleanup dbname

* 2

* Update database-queries.md

* Update backup-and-restore.md

* Update backup-and-restore.md
2025-01-13 20:57:19 -05:00
Alex
79726acc72 refactor: migrate sessions repository to kysely (#15268)
* wip: search

* wip: getByToken

* wip: getByToken

* wip: getByUserId

* wip: create/update/delete

* remove unused code

* clean up and pr feedback

* fix: test

* fix: e2e test

* pr feedback
2025-01-13 20:45:52 -05:00
Jason Rasmussen
36eef9807b fix: version history sql (#15321) 2025-01-14 01:38:11 +00:00
Alex
3da750117f refactor: migrate user repository to kysely (#15296)
* refactor: migrate user repository to kysely

* refactor: migrate user repository to kysely

* refactor: migrate user repository to kysely

* refactor: migrate user repository to kysely

* fix: test

* clean up

* fix: metadata retrieval bug

* use correct typeing for upsert metadata

* pr feedback

* pr feedback

* fix: add deletedAt check

* fix: get non deleted user by default

* remove console.log

* fix: stop kysely after command finishes

* final clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-01-13 20:30:34 -05:00
Dr-Electron
a6c8eb57f1 fix(docs): fix admonition in mobile section (#15291)
fix(docs): Fix admonition in mobile section
2025-01-12 21:12:26 -06:00
Austin Dudzik
efe4396e54 fix(docs): Fix link label to refer to correct location on page (#15279)
Fix link label to refer to correct location on page
2025-01-12 09:51:55 -06:00
Desmond Cox
c4a8fdf0f3 fix(cli): handle folders with single quotes (#15283)
* fix(cli): handle folders with single quotes

* fix(cli): skip single quote test on Windows

* fix(cli): support double quote and backtick as well
2025-01-12 15:44:51 +00:00
Jin Xuan
abf5b0afe1 fix(web): mismatched deviceAssetId when uploading images (#15130)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-12 04:28:39 +00:00
Ferdinand Holzer
77d4eb8787 fix(web): render whitespaces in file names and paths on photos and folders pages correctly (#15266) 2025-01-11 22:10:33 -06:00
bo0tzz
e7abfe3067 docs: clarify filesystem backup paths (#15243)
* docs: clarify filesystem backup paths

* fix: backticks
2025-01-11 22:10:23 -06:00
Matthew Momjian
be1187bc46 chore(docs): clarify experimental network features (#15228)
* auth

* URL switch

* mobile app

* caps

* headers, app changes

* oxford comma

* Match case to other use in Immich

* add url

* asset download also causes issues
2025-01-11 22:09:54 -06:00
Mert
fef36e6a37 chore(server)!: default max bitrate unit to kbps (#15264)
default unit to kbps
2025-01-11 22:09:19 -06:00
imakida
a39fbcb8ac feat: #15237 toggle password visibility on shared albums (#15238)
* feat: toggle password visibility on shared albums

* feat: toggle password visibility on shared albums

* use password-field component

* remove div wrapping PasswordField

---------

Co-authored-by: Ian <ian@zetabyte.dev>
2025-01-11 22:08:08 -06:00
renovate[bot]
ca75bba3b0 chore(deps): update prom/prometheus docker digest to 6559acb (#15244)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:06:45 -06:00
renovate[bot]
f3dbbfa16d chore(deps): update redis:6.2-alpine docker digest to 905c4ee (#15245)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:06:30 -06:00
renovate[bot]
8b4390c247 chore(deps): update dependency @types/node to ^22.10.5 (#15246)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:06:18 -06:00
renovate[bot]
581d32269d fix(deps): update machine-learning (#15247)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:05:20 -06:00
renovate[bot]
2b76112014 chore(deps): update github-actions (#15248)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:04:40 -06:00
renovate[bot]
2301affd7e chore(deps): update node.js to v22.13.0 (#15249)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:04:22 -06:00
renovate[bot]
2f9a66e961 chore(config): migrate renovate config (#15262)
chore(config): migrate config renovate.json

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 22:02:12 -06:00
renovate[bot]
0b8cfc6b82 chore(deps): update base-image to v20250107 (major) (#15251)
chore(deps): update base-image to v20250107

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-11 23:00:50 -05:00
bo0tzz
cab201270c chore: migrate version-history repository to kysely (#15267)
* chore: generate sql for version-history repository

* chore: run kysely-codegen

* chore: migrate version-history repository to kysely

* fix: change `| null` to `| undefined`

* chore: clean up unneeded async
2025-01-11 20:12:34 +00:00
Mert
beb31cebed fix(mobile): don't crash android app when video player throws exception (#15236)
update commit ref
2025-01-10 19:20:56 -06:00
Jason Rasmussen
e51091b6e5 refactor: migrate trash to kysely (#15233) 2025-01-10 18:48:21 -05:00
Jason Rasmussen
cc6a8b0c74 refactor: migrate system metadata to kysely (#15231) 2025-01-10 14:20:15 -05:00
Jason Rasmussen
930f979960 feat: migration api keys to use kysely (#15206) 2025-01-10 14:02:12 -05:00
Jin Xuan
3030e74fc3 fix(web): escape key to clear selection and go to previous page (#15142) (#15219) 2025-01-10 15:27:35 +00:00
Tom Graham
f9db60f25b fix(mobile): 15072 Fix issue with boolean filters filtering out results when they shouldn't (#15208)
Fix issue with boolean filters filtering out results when they shouldn't.

Co-authored-by: Tom graham <tomg@questps.com.au>
2025-01-10 09:18:40 -06:00
Daniel Dietzler
7d50d3032b refactor: activity queries (#15207) 2025-01-09 18:14:36 -05:00
Daniel Dietzler
1fb2b3f899 refactor: migrate activity repo to kysely (#15203)
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2025-01-09 14:31:46 -05:00
Mert
2e12c46980 refactor(server): use kysely (#12857) 2025-01-09 11:15:41 -05:00
renovate[bot]
1489d69f81 chore(deps): update terraform cloudflare to v4.50.0 (#14956)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-01-09 10:31:15 +00:00
github-actions
8d836ae04f chore: version v1.124.2 2025-01-08 23:16:35 +00:00
Arno
cc473c42b5 fix(web): Reset asset selection when refreshing and changing folder (#15178)
* fix(web): Reset asset selection when refreshing and changing folder

* fix: refactor
2025-01-08 17:13:17 -06:00
Jonathan Jogenfors
bab04378dc fix(server): generate thumbnails for external assets (#15183)
fix: thumbnail generation of external assets
2025-01-08 17:12:39 -06:00
github-actions
cc10fc15c3 chore: version v1.124.1 2025-01-08 16:16:15 +00:00
Yaros
8cb9196bcb chore(docs): Translated README to Ukrainian (#15167)
* Wrote Ukrainian README

* Added links to Ukrainian README translation
2025-01-08 08:55:25 -06:00
Julian Collins
6cce24f391 chore(docs): update and polish Russian README (#15164)
* Actualize and polish RU readme

* Actualize and polish RU readme
2025-01-08 08:53:26 -06:00
Alex
36a9ae7c54 fix(server): email cannot render with pretty flag (#15157) 2025-01-08 08:53:08 -06:00
bo0tzz
4aabbec742 fix: invert asset path link nesting (#15163) 2025-01-08 07:52:59 -06:00
Alex
c5baf79f61 chore(mobile): post release task (#15148) 2025-01-08 11:51:08 +01:00
Alex
e183c9b917 chore(docs): remove duplicate filename (#15149) 2025-01-07 19:58:50 +00:00
github-actions
840d99ed25 chore: version v1.124.0 2025-01-07 19:00:42 +00:00
Sebastian Schneider
b4c1304b46 feat(web): Improve duplicate suggestion (#14947)
* feat: Improve duplicate suggestion

* format

* feat(web): Add deduplication info popup

* fix: lint

* fmt

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-07 18:30:11 +00:00
Jonathan Jogenfors
23f3e737fd fix(server): don't delete offline files from disk when trash empties (#14777)
fix: don't delete offline files from disk when emptying trash

Move logic to asset deletion check
2025-01-07 18:25:43 +00:00
Weblate (bot)
10e569cc1c chore(web): update translations (#14759)
Co-authored-by: -J- <heyj0e@tuta.io>
Co-authored-by: AMIR IZZUDDIN <amirizzuddin@outlook.com>
Co-authored-by: Abhishek Kumar <abyshek.kumar@gmail.com>
Co-authored-by: Alan Ye <git@wrye.dev>
Co-authored-by: Alan Ye <me@wrye.dev>
Co-authored-by: Alexander Roumeliotis <alexroumeli@gmail.com>
Co-authored-by: Benjamin Freeman <b.t.freeman16@gmail.com>
Co-authored-by: Berkan Özkan <ozkanberkan@yandex.com>
Co-authored-by: Bezruchenko Simon <worcposj44@gmail.com>
Co-authored-by: Bohuslav Uličný <justadreamer@duck.com>
Co-authored-by: Christian Shannon <dargelf@gmail.com>
Co-authored-by: Chuckame <antoine.michaud.17@gmail.com>
Co-authored-by: Cristi Bklash <bklash3@gmail.com>
Co-authored-by: Denis Saamiev <samievdenis@gmail.com>
Co-authored-by: Dominik <dominik.falis@proton.me>
Co-authored-by: Eirik Rolland Enger <engeir@pm.me>
Co-authored-by: Eshgin <eshqin.quluzade1@gmail.com>
Co-authored-by: Evgenij Spitsyn <evgeniy@spitsyn.net>
Co-authored-by: Filip <fjokovic0@gmail.com>
Co-authored-by: Fredrik Andersson <membership@fredande.se>
Co-authored-by: Heine Olsen <olsen10051988@gmail.com>
Co-authored-by: Hurricane-32 <rodrigorimo@hotmail.com>
Co-authored-by: Indrek Haav <IndrekHaav@users.noreply.hosted.weblate.org>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jozef Pistej <pistej2@gmail.com>
Co-authored-by: Kalilinoe Alohilani <akalilinoe@gmail.com>
Co-authored-by: Kamil Warczek <kamilwarczek@gmail.com>
Co-authored-by: Kenji Opdam <kenji.opdam@gmail.com>
Co-authored-by: Lab Forensic <recoverylabkor@gmail.com>
Co-authored-by: Leo Bottaro <github@leobottaro.com>
Co-authored-by: Leon S. Kennedy <leonsk29@gmail.com>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Manar Aldroubi <droubi@gmail.com>
Co-authored-by: Matteo Morari <matteo.morari04@gmail.com>
Co-authored-by: Max <mail@heavygale.de>
Co-authored-by: Michal Micech <michal.micech@gmail.com>
Co-authored-by: Miki Mrvos <medolino2009@gmail.com>
Co-authored-by: Mystically <github@mytrydas.com>
Co-authored-by: Nicolai Bonde <git@nicolaibonde.dk>
Co-authored-by: Noisy Fridge <pureprince.habib@gmail.com>
Co-authored-by: OskarSidor <oskar.sidor@gmail.com>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pascal Dietrich <pascal.1.dietrich@hotmail.com>
Co-authored-by: Paul Kupper <p@ul-kupper.de>
Co-authored-by: Paul Murphy <djxistenz@gmail.com>
Co-authored-by: Petri Hämäläinen <petri.hamalainen@mailbox.org>
Co-authored-by: Rekku484 <rekku484@gmail.com>
Co-authored-by: Reza Almanda <rezaalmanda27@gmail.com>
Co-authored-by: Roger Veciana Rovira <rveciana@gmail.com>
Co-authored-by: Sander Brilman <brilmansander@gmail.com>
Co-authored-by: Sedat Albayrak <sedat.albayrak@icloud.com>
Co-authored-by: Shawn <xiaxinx@gmail.com>
Co-authored-by: Shogo Takata <pineapplehunter.daniel@gmail.com>
Co-authored-by: Stijn <gielisstijn@gmail.com>
Co-authored-by: Sylvain Pichon <Sp_@users.noreply.hosted.weblate.org>
Co-authored-by: Tal Sitton <tal.sitton04@gmail.com>
Co-authored-by: Thomas Möller <thomas.moeller@mailbox.org>
Co-authored-by: Torben Eims <torben.eims@gmx.de>
Co-authored-by: Venkateswaran Ganesan <vganes3@gmail.com>
Co-authored-by: Vojta Linha <vojtech.linha+weblate@gmail.com>
Co-authored-by: WilliamNT <hwbendeguz@gmail.com>
Co-authored-by: Yuki IWATA <daburutti@gmail.com>
Co-authored-by: aln <imyapear@gmail.com>
Co-authored-by: chamdim <chamdim@protonmail.com>
Co-authored-by: gjarek98 <gjarek48@gmail.com>
Co-authored-by: grgergo <gergo_g@proton.me>
Co-authored-by: pyccl <changcongliang@163.com>
Co-authored-by: pyorot <FMasic@hotmail.co.uk>
Co-authored-by: quitroot <quitroot@gmail.com>
Co-authored-by: rezi nagro <rezinagro@hotmail.com>
Co-authored-by: scudo <whiteshield.tg@protonmail.com>
Co-authored-by: sibber5 <ghasjado@gmail.com>
Co-authored-by: thehijacker <thehijacker@gmail.com>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
Co-authored-by: Álvaro Orduna León <alvaro.orduna.92@gmail.com>
Co-authored-by: Ľubomír Dlhý <dlhylubomir@gmail.com>
Co-authored-by: Вячеслав Лукьяненко <madeinchuguev@gmail.com>
Co-authored-by: Мĕтри Сантăр ывалĕ Упа-Миччи <mefisteron@gmail.com>
Co-authored-by: Пламен Марков <tsmhunter@gmail.com>
Co-authored-by: தமிழ்நேரம் <anishprabu.t@gmail.com>
2025-01-07 17:13:40 +00:00
Alex
61c9c14ba6 fix(mobile): mismatch language (#15141) 2025-01-07 17:02:52 +00:00
Alex
d0ec24ef08 chore(mobile): translations update (#15139)
chore(mobile): translation update
2025-01-07 16:52:25 +00:00
David Koňařík
fa0b352bd0 feat: "add to album" shortcut and generic menu option shortcuts (#15056)
* Add shortcut prop to MenuOption

* Add "add to album" shortcut in photo grid
2025-01-07 10:29:22 -06:00
John Stef
c148a28a82 fix(mobile): fix text search (#14873)
* fix(mobile): fix text search

* chore(mobile): add tests for SearchPage

* fix(mobile): fix render overflow for small screens

Needed for SearchPage test to not throw overflow error

* chore(mobile): update import_rule_openapi

* styling

* preserve styling and skip a test

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-07 16:26:14 +00:00
Zack Pollard
776be7d205 fix: reverse geocoding data import don't use unlogged tables (#15136) 2025-01-07 15:36:07 +00:00
Alex
c937097db4 chore(mobile): add new language locales (#15138)
chore(mobile): add new languages locale
2025-01-07 15:30:52 +00:00
Jordan
ff7a66a2c5 docs: add warning about superuser requirement for automated database backups. (#15045)
Add warning about superuser requirement for automated database backups.
2025-01-07 09:04:04 -06:00
Jason Rasmussen
9bc13aca7c chore: use 307 for redirects (#15119) 2025-01-06 17:51:18 -05:00
Ovidijus R
a13b7b364e feat(mobile): add ability to force view original videos (#15094) 2025-01-06 15:29:47 -05:00
scottdesilva
1f0ffd634a fix(server): unit test for creating tag with color (#15120)
* Pass color to tag repo on creation

* unit test for creating tag with optional color
2025-01-06 13:33:48 -06:00
scottdesilva
c78e9d6ad5 fix(server): save color when creating tag (#15106)
Pass color to tag repo on creation
2025-01-06 12:21:55 -05:00
loowiz
6fd9f93375 docs: Add disable iOS low power mode suggestion (#15110)
* docs: Add disable iOS low power mode suggestion

Just added the suggestion under "Why is background backup on iOS not working?" to also disable low power mode.
I've seen so many people who just have low power mode permanently on, but this can affect background app refresh (https://support.apple.com/en-us/101604)

* Update FAQ.mdx

* Make wording more consistent in docs/docs/FAQ.mdx

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

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2025-01-06 15:38:36 +00:00
Matthew Momjian
e7667a7896 docs: warning for LXC use (#15071)
warn for LXC use
2025-01-05 16:10:00 -05:00
Matthew Momjian
a81fb73faf feat(docs): additional corruption checks (#15102)
corruption checks
2025-01-05 18:03:53 +00:00
Buan
6a9d57ad33 docs: update Spanish README (#15081)
* Update README_es_ES.md

* Add Traduction and activity section, and corrected a string
2025-01-04 13:00:46 -06:00
Mattia Natali
d29bdd6308 feat(web): Swipe down shows slideshow controlbar on mobile (#15080)
Swipe down show slideshow controlbar on mobile
2025-01-04 18:18:05 +00:00
Mert
fcd372238f chore(server): add support for .m2t (#15064)
* add m2t

* ordering
2025-01-03 16:28:32 -06:00
Arno
b45ff8d09f feat(web): Enable selection interactions in folder view (#15049)
* feat(web): Enable selection interactions in folder view

* feat(web): Add link to parent folder in detail pane, if folders are enabled

* Added invalidation and refreshing of asset cache on changes

* fix: removed unused imports and changed link

* chore: styling

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2025-01-03 16:09:31 +00:00
819 changed files with 38775 additions and 23747 deletions

2
.devcontainer/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
.env
library

View File

@@ -1,2 +1,16 @@
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:9791f4aa527774bc370c6bd2f6705ce5a686f1e6f204badd8dfaacce28c631ae
FROM ${BASEIMAGE}
# Flutter SDK
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
ENV FLUTTER_CHANNEL="stable"
ENV FLUTTER_VERSION="3.24.5"
ENV FLUTTER_HOME=/flutter
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
# Flutter SDK
RUN mkdir -p ${FLUTTER_HOME} \
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
&& rm flutter.tar.xz \
&& chown -R 1000:1000 ${FLUTTER_HOME}

View File

@@ -1,20 +1,26 @@
{
"name": "Immich devcontainers",
"build": {
"dockerfile": "Dockerfile",
"args": {
"BASEIMAGE": "mcr.microsoft.com/devcontainers/typescript-node:22"
}
},
"customizations": {
"vscode": {
"extensions": [
"svelte.svelte-vscode"
]
}
},
"forwardPorts": [],
"postCreateCommand": "make install-all",
"remoteUser": "node"
"name": "Immich",
"service": "immich-devcontainer",
"dockerComposeFile": [
"docker-compose.yml",
"../docker/docker-compose.dev.yml"
],
"customizations": {
"vscode": {
"extensions": [
"Dart-Code.dart-code",
"Dart-Code.flutter",
"dbaeumer.vscode-eslint",
"dcmdev.dcm-vscode-extension",
"esbenp.prettier-vscode",
"svelte.svelte-vscode"
]
}
},
"forwardPorts": [],
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
"overrideCommand": true,
"workspaceFolder": "/immich",
"remoteUser": "node"
}

View File

@@ -0,0 +1,8 @@
services:
immich-devcontainer:
build:
dockerfile: Dockerfile
extra_hosts:
- 'host.docker.internal:host-gateway'
volumes:
- ..:/immich:cached

View File

@@ -0,0 +1,6 @@
#!/bin/bash
# If .env file does not exist, create it by copying example.env from the docker folder
if [ ! -f ".devcontainer/.env" ]; then
cp docker/example.env .devcontainer/.env
fi

View File

@@ -0,0 +1,25 @@
#!/bin/bash
# Enable multiarch for arm64 if necessary
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
sudo dpkg --add-architecture amd64 && \
sudo apt-get update && \
sudo apt-get install -y --no-install-recommends \
qemu-user-static \
libc6:amd64 \
libstdc++6:amd64 \
libgcc1:amd64
fi
# Install DCM
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
sudo apt-get update
sudo apt-get install dcm
dart --disable-analytics
# Install immich
cd /immich || exit
make install-all

View File

@@ -1,2 +1 @@
blank_issues_enabled: false
blank_pull_request_template_enabled: false

View File

@@ -1,22 +0,0 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
## Screenshots (if appropriate):
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable

36
.github/pull_request_template.md vendored Normal file
View File

@@ -0,0 +1,36 @@
## Description
<!--- Describe your changes in detail -->
<!--- Why is this change required? What problem does it solve? -->
<!--- If it fixes an open issue, please link to the issue here. -->
Fixes # (issue)
## How Has This Been Tested?
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
- [ ] Test A
- [ ] Test B
<details><summary><h2>Screenshots (if appropriate)</h2></summary>
<!-- Images go below this line. -->
</details>
<!-- API endpoint changes (if relevant)
## API Changes
The `/api/something` endpoint is now `/api/something-else`
-->
## Checklist:
- [ ] I have performed a self-review of my own code
- [ ] I have made corresponding changes to the documentation if applicable
- [ ] I have no unrelated changes in the PR.
- [ ] I have confirmed that any new dependencies are strictly necessary.
- [ ] I have written tests for new code (if applicable)
- [ ] I have followed naming conventions/patterns in the surrounding code
- [ ] All code in `src/services` uses repositories implementations for database calls, filesystem operations, etc.
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services`)

View File

@@ -29,9 +29,11 @@ jobs:
filters: |
mobile:
- 'mobile/**'
workflow:
- '.github/workflows/build-mobile.yml'
- 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"
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
build-sign-android:
name: Build and sign Android

View File

@@ -56,10 +56,10 @@ jobs:
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.2.0
uses: docker/setup-qemu-action@v3.4.0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.8.0
uses: docker/setup-buildx-action@v3.9.0
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
@@ -88,7 +88,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@v6.10.0
uses: docker/build-push-action@v6.13.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

@@ -1,73 +0,0 @@
# This workflow runs on certain conditions to check for and potentially
# delete container images from the GHCR which no longer have an associated
# code branch.
# Requires a PAT with the correct scope set in the secrets.
#
# This workflow will not trigger runs on forked repos.
name: Docker Cleanup
on:
pull_request:
types:
- "closed"
push:
paths:
- ".github/workflows/docker-cleanup.yml"
concurrency:
group: registry-tags-cleanup
cancel-in-progress: false
jobs:
cleanup-images:
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
is_org: "true"
do_delete: "true"
package_name: "${{ matrix.primary-name }}"
scheme: "pull_request"
repo_name: "immich"
match_regex: '^pr-(\d+)$|^(\d+)$'
cleanup-untagged-images:
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
runs-on: ubuntu-24.04
needs:
- cleanup-images
strategy:
fail-fast: false
matrix:
include:
- primary-name: "immich-server"
- primary-name: "immich-machine-learning"
- primary-name: "immich-build-cache"
env:
# Requires a personal access token with the OAuth scope delete:packages
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
do_delete: "true"
is_org: "true"
package_name: "${{ matrix.primary-name }}"

View File

@@ -36,10 +36,12 @@ jobs:
- 'i18n/**'
machine-learning:
- 'machine-learning/**'
workflow:
- '.github/workflows/docker.yml'
- 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"
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
retag_ml:
name: Re-Tag ML
@@ -61,8 +63,10 @@ jobs:
REGISTRY_NAME="ghcr.io"
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
TAG_OLD=main${{ matrix.suffix }}
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
retag_server:
name: Re-Tag Server
@@ -84,107 +88,100 @@ jobs:
REGISTRY_NAME="ghcr.io"
REPOSITORY=${{ github.repository_owner }}/immich-server
TAG_OLD=main${{ matrix.suffix }}
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
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
runs-on: ${{ matrix.runner }}
env:
image: immich-machine-learning
context: machine-learning
file: machine-learning/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- platforms: linux/amd64,linux/arm64
- platform: linux/amd64
runner: ubuntu-latest
device: cpu
- platforms: linux/amd64
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: cpu
- platform: linux/amd64
runner: ubuntu-latest
device: cuda
suffix: -cuda
- platforms: linux/amd64
- platform: linux/amd64
runner: ubuntu-latest
device: openvino
suffix: -openvino
- platforms: linux/arm64
- platform: linux/arm64
runner: ubuntu-24.04-arm
device: armnn
suffix: -armnn
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- 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.8.0
- 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 }}
uses: docker/setup-buildx-action@v3.9.0
- 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
- name: Generate cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks 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
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v6.10.0
id: build
uses: docker/build-push-action@v6.13.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 }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=${{ matrix.device }}
BUILD_ID=${{ github.run_id }}
@@ -192,100 +189,245 @@ jobs:
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_ml:
name: Merge & Push ML
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
DOCKER_REPO: altran1502/immich-machine-learning
strategy:
matrix:
include:
- device: cpu
- device: cuda
suffix: -cuda
- device: openvino
suffix: -openvino
- device: armnn
suffix: -armnn
needs:
- build_and_push_ml
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: ml-digests-${{ matrix.device }}-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate docker image tags
id: meta
uses: docker/metadata-action@v5
with:
flavor: |
# Disable latest tag
latest=false
images: |
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},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 long commit sha hash
type=sha,format=long,prefix=commit-,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: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
run: |
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
build_and_push_server:
name: Build and Push Server
runs-on: ubuntu-latest
runs-on: ${{ matrix.runner }}
needs: pre-job
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
env:
image: immich-server
context: .
file: server/Dockerfile
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
strategy:
fail-fast: false
matrix:
include:
- platforms: linux/amd64,linux/arm64
device: cpu
- platform: linux/amd64
runner: ubuntu-latest
- platform: linux/arm64
runner: ubuntu-24.04-arm
steps:
- name: Prepare
run: |
platform=${{ matrix.platform }}
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
- 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.8.0
- 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 }}
uses: docker/setup-buildx-action@v3
- 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 cache key suffix
run: |
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
else
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
fi
- name: Generate cache target
id: cache-target
run: |
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
# Essentially just ignore the cache output (forks 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,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
fi
- name: Build and push image
id: build
uses: docker/build-push-action@v6.13.0
with:
context: ${{ env.context }}
file: ${{ env.file }}
platforms: ${{ matrix.platform }}
labels: ${{ steps.metadata.outputs.labels }}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
cache-from: |
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
build-args: |
DEVICE=cpu
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 }}
- name: Export digest
run: |
mkdir -p ${{ runner.temp }}/digests
digest="${{ steps.build.outputs.digest }}"
touch "${{ runner.temp }}/digests/${digest#sha256:}"
- name: Upload digest
uses: actions/upload-artifact@v4
with:
name: server-digests-${{ env.PLATFORM_PAIR }}
path: ${{ runner.temp }}/digests/*
if-no-files-found: error
retention-days: 1
merge_server:
name: Merge & Push Server
runs-on: ubuntu-latest
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
env:
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
DOCKER_REPO: altran1502/immich-server
needs:
- build_and_push_server
steps:
- name: Download digests
uses: actions/download-artifact@v4
with:
path: ${{ runner.temp }}/digests
pattern: server-digests-*
merge-multiple: true
- name: Login to Docker Hub
if: ${{ github.event_name == 'release' }}
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Generate docker image tags
id: metadata
id: meta
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' }}
name=${{ env.GHCR_REPO }}
name=${{ env.DOCKER_REPO }},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 long commit sha hash
type=sha,format=long,prefix=commit-,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
- name: Create manifest list and push
working-directory: ${{ runner.temp }}/digests
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.10.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 }}
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
success-check-server:
name: Docker Build & Push Server Success
needs: [build_and_push_server, retag_server]
needs: [merge_server, retag_server]
runs-on: ubuntu-latest
if: always()
steps:
@@ -298,7 +440,7 @@ jobs:
success-check-ml:
name: Docker Build & Push ML Success
needs: [build_and_push_ml, retag_ml]
needs: [merge_ml, retag_ml]
runs-on: ubuntu-latest
if: always()
steps:

View File

@@ -15,7 +15,7 @@ jobs:
pre-job:
runs-on: ubuntu-latest
outputs:
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -25,9 +25,11 @@ jobs:
filters: |
docs:
- 'docs/**'
workflow:
- '.github/workflows/docs-build.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
build:
name: Docs Build

View File

@@ -68,10 +68,17 @@ jobs:
needs: build_mobile
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: Download APK
uses: actions/download-artifact@v4

17
.github/workflows/preview-comment.yaml vendored Normal file
View File

@@ -0,0 +1,17 @@
name: Preview comment
on:
pull_request:
types: [labeled]
jobs:
comment-status:
runs-on: ubuntu-latest
if: ${{ github.event.label.name == 'preview' }}
permissions:
pull-requests: write
steps:
- uses: mshick/add-pr-comment@v2
with:
message-id: "preview-status"
message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"

View File

@@ -23,9 +23,11 @@ jobs:
filters: |
mobile:
- 'mobile/**'
workflow:
- '.github/workflows/static_analysis.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
mobile-dart-analyze:
name: Run Dart Code Analysis

View File

@@ -43,10 +43,12 @@ jobs:
- 'mobile/**'
machine-learning:
- 'machine-learning/**'
workflow:
- '.github/workflows/test.yml'
- name: Check if we should force jobs to run
id: should_force
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
server-unit-tests:
name: Test & Lint Server

View File

@@ -29,6 +29,7 @@
<a href="readme_i18n/README_nl_NL.md">Nederlands</a>
<a href="readme_i18n/README_tr_TR.md">Türkçe</a>
<a href="readme_i18n/README_zh_CN.md">中文</a>
<a href="readme_i18n/README_uk_UA.md">Українська</a>
<a href="readme_i18n/README_ru_RU.md">Русский</a>
<a href="readme_i18n/README_pt_BR.md">Português Brasileiro</a>
<a href="readme_i18n/README_sv_SE.md">Svenska</a>

View File

@@ -1 +1 @@
22.12.0
22.14.0

View File

@@ -1,4 +1,4 @@
FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS core
FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 AS core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

1130
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.37",
"version": "2.2.50",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -20,15 +20,15 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
"@types/node": "^22.10.2",
"@types/node": "^22.13.2",
"@typescript-eslint/eslint-plugin": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^3.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
"commander": "^12.0.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^56.0.1",
"globals": "^15.9.0",
@@ -36,9 +36,9 @@
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^4.0.0",
"typescript": "^5.3.3",
"vite": "^5.0.12",
"vite": "^6.0.0",
"vite-tsconfig-paths": "^5.0.0",
"vitest": "^2.0.5",
"vitest": "^3.0.0",
"vitest-fetch-mock": "^0.4.0",
"yaml": "^2.3.1"
},
@@ -67,6 +67,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "22.12.0"
"node": "22.14.0"
}
}

View File

@@ -6,6 +6,7 @@ interface Test {
test: string;
options: Omit<CrawlOptions, 'extensions'>;
files: Record<string, boolean>;
skipOnWin32?: boolean;
}
const cwd = process.cwd();
@@ -48,6 +49,18 @@ const tests: Test[] = [
'/photos/image.jpg': true,
},
},
{
test: 'should crawl folders with quotes',
options: {
pathsToCrawl: ["/photo's/", '/photo"s/', '/photo`s/'],
},
files: {
"/photo's/image1.jpg": true,
'/photo"s/image2.jpg': true,
'/photo`s/image3.jpg': true,
},
skipOnWin32: true, // single quote interferes with mockfs root on Windows
},
{
test: 'should crawl a single file',
options: {
@@ -270,8 +283,12 @@ describe('crawl', () => {
});
describe('crawl', () => {
for (const { test, options, files } of tests) {
it(test, async () => {
for (const { test: name, options, files, skipOnWin32 } of tests) {
if (process.platform === 'win32' && skipOnWin32) {
test.skip(name);
continue;
}
it(name, async () => {
// The file contents is the same as the path.
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file])));

View File

@@ -146,7 +146,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
}
const searchPatterns = patterns.map((pattern) => {
let escapedPattern = pattern;
let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]');
if (recursive) {
escapedPattern = escapedPattern + '/**';
}

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.48.0"
constraints = "4.48.0"
version = "4.52.0"
constraints = "4.52.0"
hashes = [
"h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
"h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
"h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
"h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
"h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
"h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
"h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
"h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
"h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
"h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
"h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
"h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
"h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
"h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
"zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
"zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
"zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
"zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
"zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
"zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
"zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
"zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
"zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
"zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
"zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
"zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
"zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
]
}

View File

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

View File

@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.48.0"
constraints = "4.48.0"
version = "4.52.0"
constraints = "4.52.0"
hashes = [
"h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
"h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
"h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
"h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
"h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
"h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
"h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
"h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
"h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
"h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
"h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
"h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
"h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
"h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
"zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
"zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
"zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
"zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
"zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
"zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
"zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
"zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
"zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
"zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
"zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
"zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
"zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
]
}

View File

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

View File

@@ -1,4 +1,13 @@
# See:
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
# For development see:
# - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting
@@ -71,6 +80,7 @@ services:
- ../web:/usr/src/app
- ../i18n:/usr/src/i18n
- ../open-api/:/usr/src/open-api/
# - ../../ui:/usr/ui
- /usr/src/app/node_modules
ulimits:
nofile:
@@ -106,7 +116,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
healthcheck:
test: redis-cli ping || exit 1

View File

@@ -1,3 +1,12 @@
#
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
name: immich-prod
services:
@@ -47,7 +56,7 @@ services:
redis:
container_name: immich_redis
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
healthcheck:
test: redis-cli ping || exit 1
restart: always
@@ -91,7 +100,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd
image: prom/prometheus@sha256:5888c188cf09e3f7eebc97369c3b2ce713e844cdbd88ccf36f5047c958aea120
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -103,7 +112,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
image: grafana/grafana:11.5.1-ubuntu@sha256:9a4ab78cec1a2ec7d1ca5dfd5aacec6412706a1bc9e971fc7184e2f6696a63f5
volumes:
- grafana-data:/var/lib/grafana

View File

@@ -1,10 +1,11 @@
#
# WARNING: Make sure to use the docker-compose.yml of the current release:
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
#
# Make sure to use the docker-compose.yml of the current release:
#
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
#
# The compose file on main may not be compatible with the latest release.
#
name: immich
@@ -48,7 +49,7 @@ services:
redis:
container_name: immich_redis
image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
healthcheck:
test: redis-cli ping || exit 1
restart: always

View File

@@ -48,6 +48,7 @@ services:
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
devices:
- /dev/dri:/dev/dri
- /dev/dxg:/dev/dxg
volumes:
- /usr/lib/wsl:/usr/lib/wsl
environment:

View File

@@ -1 +1 @@
22.12.0
22.14.0

View File

@@ -49,13 +49,22 @@ The behaviors differ based on your device manufacturer and operating system, but
On iOS (iPhone and iPad), the operating system determines if a particular app can invoke background tasks based on multiple factors, most of which the Immich app has no control over. To increase the likelihood that the background backup task is run, follow the steps below:
- Enable Background App Refresh for Immich in the iOS settings at `Settings > General > Background App Refresh`.
- Disable **Low Power Mode** when not needed, as this can prevent apps from running in the background.
- 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?
### Why are features in the mobile app not working with a self-signed certificate, Basic Auth, custom headers, or mutual TLS?
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/).
These network features are experimental. They often do not work with video playback, asset upload or download, and other features.
Many of these limitations are tracked in [#15230](https://github.com/immich-app/immich/issues/15230).
Instead of these experimental features, we recommend using the URL switching feature, a VPN, or a [free trusted SSL certificate](https://letsencrypt.org/) for your domain.
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
Please discuss any large PRs with our dev team to ensure your time is not wasted.
### Why isn't the mobile app updated yet?
The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases.
---
@@ -155,6 +164,35 @@ For example, say you have existing transcodes with the policy "Videos higher tha
No. Our design principle is that the original assets should always be untouched.
### How can I mount a CIFS/Samba volume within Docker?
If you aren't able to or prefer not to mount Samba on the host (such as Windows environment), you can mount the volume within Docker.
Below is an example in the `docker-compose.yml`.
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
```diff
...
services:
immich-server:
...
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
+ - originals:/usr/src/app/originals
...
volumes:
model-cache:
+ originals:
+ driver_opts:
+ type: cifs
+ o: 'iocharset=utf8,username=USERNAMEHERE,password=PASSWORDHERE,rw' # change to `ro` if read only desired
+ device: '//localipaddress/sharename'
```
---
## Albums
@@ -277,7 +315,7 @@ The initial backup is the most intensive due to the number of jobs running. The
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
- If these changes are not enough, see [above](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
### Can I limit CPU and RAM usage?
@@ -420,7 +458,7 @@ A result of `on` means that checksums are enabled.
<summary>Check if checksums are enabled</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="show data_checksums"
data_checksums
----------------
on
@@ -435,7 +473,7 @@ If checksums are enabled, you can check the status of the database with the foll
<summary>Check for database corruption</summary>
```bash
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
datname | checksum_failures | checksum_last_failure
-----------+-------------------+-----------------------
postgres | 0 |
@@ -447,6 +485,24 @@ docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --
</details>
You can also scan the Postgres database file structure for errors:
<details>
<summary>Scan for file structure errors</summary>
```bash
docker exec -it immich_postgres pg_amcheck --username=postgres --heapallindexed --parent-check --rootdescend --progress --all --install-missing
```
A normal result will end something like this and return with an exit code of `0`:
```bash
7470/8832 relations (84%), 730829/734735 pages (99%)
8425/8832 relations (95%), 734367/734735 pages (99%)
8832/8832 relations (100%), 734735/734735 pages (100%)
```
</details>
If corruption is detected, you should immediately make a backup before performing any other work in the database.
To do so, you may need to set the `zero_damaged_pages=on` flag for the database server to allow `pg_dumpall` to succeed.
After taking a backup, the recommended next step is to restore the database from a healthy backup before corruption was detected.

View File

@@ -55,7 +55,7 @@ sleep 10 # Wait for Postgres server to start up
# Check the database user if you deviated from the default
gunzip < "/path/to/backup/dump.sql.gz" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| docker exec -i immich_postgres psql --username=postgres # Restore Backup
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
docker compose up -d # Start remainder of Immich apps
```
@@ -70,18 +70,16 @@ docker compose up -d # Start remainder of Immich apps
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch
## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database
# Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch
## You should mount the backup (as a volume, example: - 'C:\path\to\backup\dump.sql':/dump.sql) into the immich_postgres container using the docker-compose.yml
docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them
docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
# Check the database user if you deviated from the default
cat "/dump.sql" \
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
| psql --username=postgres # Restore Backup
exit # Exit the Docker shell
docker compose up -d # Start remainder of Immich apps
## You should mount the backup (as a volume, example: `- 'C:\path\to\backup\dump.sql:/dump.sql'`) into the immich_postgres container using the docker-compose.yml
docker compose pull # Update to latest version of Immich (if desired)
docker compose create # Create Docker containers for Immich apps without running them
docker start immich_postgres # Start Postgres server
sleep 10 # Wait for Postgres server to start up
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
exit # Exit the Docker shell
docker compose up -d # Start remainder of Immich apps
```
</TabItem>
@@ -95,12 +93,14 @@ Some deployment methods make it difficult to start the database without also sta
## Filesystem
Immich stores two types of content in the filesystem: (1) original, unmodified assets (photos and videos), and (2) generated content. Only the original content needs to be backed-up, which is stored in the following folders:
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
1. `UPLOAD_LOCATION/library`
2. `UPLOAD_LOCATION/upload`
3. `UPLOAD_LOCATION/profile`
If you choose to back up only those folders, you will need to rerun the transcoding and thumbnail generation jobs for all assets after you restore from a backup.
:::caution
If you moved some of these folders onto a different storage device, such as `profile/`, make sure to adjust the backup path to match your setup
:::

View File

@@ -42,6 +42,10 @@ Typically Immich expects superuser permission in the database, which you can gra
This method is recommended for **advanced users only** and often requires manual intervention when updating Immich.
:::
:::danger
Currently, automated backups require superuser permission due to the usage of `pg_dumpall`.
:::
Immich can run without superuser permissions by following the below instructions at the `psql` prompt to prepare the database.
```sql title="Set up Postgres for Immich"
@@ -66,4 +70,4 @@ When installing a new version of pgvecto.rs, you will need to manually update th
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html

View File

@@ -50,19 +50,18 @@ The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users contr
The Immich backend is divided into several services, which are run as individual docker containers.
1. `immich-server` - Handle and respond to REST API requests
1. `immich-microservices` - Execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
1. `immich-machine-learning` - Execute machine learning models
1. `postgres` - Persistent data storage
1. `redis`- Queue management for `immich-microservices`
1. `redis`- Queue management for background jobs
### Immich Server
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, with [TypeORM](https://typeorm.io/) for database management. The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`infra/`) from core business logic (`domain/`).
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, [Express](https://expressjs.com/) server, and the query builder [Kysely](https://kysely.dev/). The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`src/repositories`) from core business logic (`src/services`).
#### REST Endpoints
### API Endpoints
The server is a list of HTTP endpoints and associated handlers (controllers). Each controller usually implements the following CRUD operations:
An incoming HTTP request is mapped to a controller (`src/controllers`). Controllers are collections of HTTP endpoints. Each controller usually implements the following CRUD operations for its respective resource type:
- `POST` `/<type>` - **Create**
- `GET` `/<type>` - **Read** (all)
@@ -70,13 +69,13 @@ The server is a list of HTTP endpoints and associated handlers (controllers). Ea
- `PUT` `/<type>/:id` - **Updated** (by id)
- `DELETE` `/<type>/:id` - **Delete** (by id)
#### DTOs
### Domain Transfer Objects (DTOs)
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
### Microservices
### Background Jobs
The Immich Microservices image uses the same `Dockerfile` as the Immich Server, but with a different entrypoint. The Immich Microservices service mainly handles executing jobs, which include the following:
Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src/utils/misc.ts#L266) to run background jobs. These jobs include:
- Thumbnail Generation
- Metadata Extraction

View File

@@ -63,9 +63,20 @@ If you only want to do web development connected to an existing, remote backend,
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
```
#### `@immich/ui`
To see local changes to `@immich/ui` in Immich, do the following:
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
1. Build the `@immich/ui` project via `npm run build`
1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
1. Start up the stack via `make dev`
1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
### Mobile app
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.

View File

@@ -58,7 +58,7 @@ If your photos are on a network drive, automatic file watching likely won't work
#### Troubleshooting
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watches` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
```
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'

View File

@@ -11,7 +11,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
- ARM NN (Mali)
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
## Limitations
@@ -43,8 +43,9 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### OpenVINO
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
## Setup

View File

@@ -36,11 +36,15 @@ You can enable automatic backup on supported devices. For more information see [
If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server.
First, you need to enable the Storage Indicator in your app's settings. Navigate to **<ins>Settings -> Photo Grid</ins>** and enable **"Show Storage indicator on asset tiles"**; this makes it easy to distinguish local-only assets and synced assets.
:::note
This will enable a small cloud icon on the bottom right corner of the asset tile, indicating that the asset is synced to the server:
1. <Icon path={mdiCloudOffOutline} size={1} /> - Local-only asset; not synced to the server
2. <Icon path={mdiCloudCheckOutline} size={1} /> - Asset is synced to the server :::
2. <Icon path={mdiCloudCheckOutline} size={1} /> - Asset is synced to the server
:::
Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **<ins>Library -> On this device</ins>**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu.

View File

@@ -68,7 +68,7 @@ After bringing down the containers with `docker compose down` and back up with `
:::note
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.
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](../install/environment-variables/#general).
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
:::
### Usage

View File

@@ -8,22 +8,23 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
## Image formats
| Format | Extension(s) | Supported? | Notes |
| :-------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
| Format | Extension(s) | Supported? | Notes |
| :---------- | :---------------------------- | :----------------: | :-------------- |
| `AVIF` | `.avif` | :white_check_mark: | |
| `BMP` | `.bmp` | :white_check_mark: | |
| `GIF` | `.gif` | :white_check_mark: | |
| `HEIC` | `.heic` | :white_check_mark: | |
| `HEIF` | `.heif` | :white_check_mark: | |
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `PNG` | `.webp` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
| `RW2` | `.rw2` | :white_check_mark: | |
| `SVG` | `.svg` | :white_check_mark: | |
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
| `WEBP` | `.webp` | :white_check_mark: | |
## Video formats
@@ -34,7 +35,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `FLV` | `.flv` | :white_check_mark: | |
| `M4V` | `.m4v` | :white_check_mark: | |
| `MATROSKA` | `.mkv` | :white_check_mark: | |
| `MP2T` | `.mts` `.m2ts` | :white_check_mark: | |
| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | |
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
| `QUICKTIME` | `.mov` | :white_check_mark: | |

View File

@@ -49,5 +49,3 @@ The `thumbs/` folder contains both the small thumbnails displayed in the timelin
The storage metrics of the Immich server will track available storage at `UPLOAD_LOCATION`, so the administrator must set up some sort of monitoring to ensure the storage does not run out of space. The `profile/` folder is much smaller, usually less than 1 MB.
:::
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.

View File

@@ -5,9 +5,9 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo
:::
:::tip
Run `docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME>` to connect to the database via the container directly.
Run `docker exec -it immich_postgres psql --dbname=<DB_DATABASE_NAME> --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)).
(Replace `<DB_DATABASE_NAME>` and `<DB_USERNAME>` with the values from your [`.env` file](/docs/install/environment-variables#database)).
:::
## Assets
@@ -27,6 +27,10 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
```
```sql title="Find by ID"
SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
```
:::note
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
:::

View File

@@ -148,24 +148,26 @@ 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_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) | 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 |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | 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) | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and 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 |
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.

View File

@@ -14,6 +14,9 @@ Hardware and software requirements for Immich:
If you still want to try to use a non-Linux OS, you can set it up as follows:
- Windows: [Docker Desktop on Windows](https://docs.docker.com/desktop/install/windows-install/) or [WSL 2](https://docs.docker.com/desktop/wsl/).
- macOS: [Docker Desktop on Mac](https://docs.docker.com/desktop/install/mac-install/).
- Immich runs well in a virtualized environment when running in a full virtual machine.
The use of Docker in LXC containers is [not recommended](https://pve.proxmox.com/wiki/Linux_Container), but may be possible for advanced users.
If you have issues, we recommend that you switch to a supported VM deployment.
- **RAM**: Minimum 4GB, recommended 6GB.
- **CPU**: Minimum 2 cores, recommended 4 cores.
- **Storage**: Recommended Unix-compatible filesystem (EXT4, ZFS, APFS, etc.) with support for user/group ownership and permissions.

View File

@@ -27,7 +27,7 @@ The script will perform the following actions:
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
2. Start the containers.
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
The web application and mobile app will be available at `http://<machine-ip-address>:2283`
The directory which is used to store the library files is `./immich-app` relative to the current directory.

View File

@@ -0,0 +1,76 @@
---
sidebar_position: 85
---
# Synology [Community]
:::note
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
:::
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
## Step 1 - Download the required files
Create a directory of your choice (e.g. `./immich-app`) to house Immich. In general, it's a best practice to have all Docker-based applications running under the `./docker` directory, so in this case, your directory structure will look like `./docker/immich-app`.
Now create a `./postgres` and `./library` directory as sub-directories of the `./docker/immich-app`.
When you're all done, you should have the following:
- `./docker/immich-app/postgres`
- `./docker/immich-app/library`
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
## Step 2 - Populate the .env file with custom values
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
## Step 3 - Create a new project in Container Manager
Open Container Manager, and select the "**Project**" action on the left navigation bar and then click "**Create**".
![Create Project](../../static/img/synology-container-manager-create-project.png)
In the settings of your new project, set "**Project name**" to a name you'll remember, such as _immich-app_. When setting the "**Path**", select the `./docker/immich-app` directory you created earlier. Doing so will prompt a message to use the existing `docker-compose.yml` already present in the directory for your project. Click "**OK**" to continue.
![Set Path](../../static/img/synology-container-manager-set-path.png)
The following screen will give you the option to further customize your `docker-compose.yml` file, giving you a warning regarding the `start_interval` property. Under the `healthcheck` heading, remove the `start_interval: 30s` completely and click "**Next**".
![start interval](../../static/img/synology-container-manager-customize-docker-compose.png)
Skip the section asking to set-up a portal for Web Station, and then complete the wizard which will build and start the containers for your project.
Once your containers are successfully running, navigate to the "**Container**" section of Container Manager, right-click on the "**immich-server**" container, and choose the "**Details**".
Scroll to the bottom of the "**Details**" section, and find the `IP Address` of the container, located in the `Network` section. Take note of the container's IP address as you will need it for **Step 4**.
![Container Details](../../static/img/synology-container-manager-container-details.png)
## Step 4 - Configure Firewall Settings
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
![Firewall rules](../../static/img/synology-firewall-rules.png)
Click "**Edit Rules**" and add the following firewall rules:
- Add a "**Source IP**" rule for the IP address of your container that you obtained in Step 3 above
- Add a "**Ports**" rule for the port specified in the `docker-compose.yml`, which should be `2283`
## Next Steps
Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below.
### Setting up optional features
- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich
- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding
- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich

View File

@@ -41,7 +41,7 @@ className="border rounded-xl"
:::info Permissions
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, Immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
:::
## Installing the Immich Application
@@ -160,6 +160,10 @@ The image above has example values.
### Additional Storage [(External Libraries)](/docs/features/libraries)
:::danger Advanced Users Only
This feature should only be used by advanced users. If this is your first time installing Immich, then DO NOT mount an external library until you have a working setup. Also, your mount path MUST be something unique and should NOT be your library or upload location or a Linux directory like `/lib`. The picture below shows a valid example.
:::
<img
src={require('./img/truenas10.webp').default}
width="40%"
@@ -168,7 +172,7 @@ className="border rounded-xl"
/>
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
The **Mount Path** is the loaction you will need to copy and paste into the External Library settings within Immich.
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
<!-- A section for Labels would go here but I don't know what they do. -->

View File

@@ -72,7 +72,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
</ul>
</details>
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
@@ -111,7 +111,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
<details >
<summary>Using the FolderView plugin for organizing your Docker containers? Click me! Otherwise you're complete!</summary>
<p>If you are using the FolderView plugin go the Docker tab and select "<b>New Folder</b>".<br />Label it <i>"Immich"</i> and use this URL as the logo: https://raw.githubusercontent.com/immich-app/immich/main/design/immich-logo.webp<br/>Then simply select all the Immich related containers before clicking "<b>Submit</b>"</p>
<p>If you are using the FolderView plugin go the Docker tab and select "<b>New Folder</b>".<br />Label it <i>"Immich"</i> and use this URL as the logo: https://raw.githubusercontent.com/immich-app/immich/main/design/immich-logo.png<br/>Then simply select all the Immich related containers before clicking "<b>Submit</b>"</p>
<img
src={require('./img/unraid07.webp').default}
width="80%"

View File

@@ -1,8 +0,0 @@
---
sidebar_position: 2
---
# Comparison
If you're new here and came from other asset self-hosting alternatives you might want to look at a comparison between Immich and your current solution.
Here you can see a [comparison between the various OpenSource Photo Libraries](https://meichthys.github.io/foss_photo_libraries/) including Immich.

View File

@@ -1,3 +1,3 @@
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283`
<img src={require('./img/sign-in-phone.webp').default} width='50%' title='Mobile App Sign In' />

View File

@@ -110,9 +110,9 @@ const config = {
label: 'API',
},
{
to: '/blog',
href: 'https://immich.store',
position: 'right',
label: 'Blog',
label: 'Merch',
},
{
href: 'https://github.com/immich-app/immich',

6627
docs/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -16,8 +16,8 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.5.2",
"@docusaurus/preset-classic": "~3.5.2",
"@docusaurus/core": "~3.7.0",
"@docusaurus/preset-classic": "~3.7.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -35,7 +35,7 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.5.2",
"@docusaurus/module-type-aliases": "~3.7.0",
"prettier": "^3.2.4",
"typescript": "^5.1.6"
},
@@ -55,6 +55,6 @@
"node": ">=20"
},
"volta": {
"node": "22.12.0"
"node": "22.14.0"
}
}

View File

@@ -99,6 +99,11 @@ const projects: CommunityProjectProps[] = [
description: 'Downloads a configurable number of random photos based on people or album ID.',
url: 'https://github.com/jon6fingrs/immich-dl',
},
{
title: 'Immich Upload Optimizer',
description: 'Automatically optimize files uploaded to Immich in order to save storage space',
url: 'https://github.com/miguelangel-nubla/immich-upload-optimizer',
},
];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {

View File

@@ -44,12 +44,12 @@ export default function VersionSwitcher(): JSX.Element {
return (
versions.length > 0 && (
<DropdownNavbarItem
className="navbar__item"
className="version-switcher-34ab39"
label={label}
mobile={windowSize === 'mobile'}
items={versions.map(({ label, url }) => ({
label,
to: url,
to: url + location.pathname + location.hash,
target: '_self',
}))}
/>

View File

@@ -75,6 +75,11 @@ div[class^='announcementBar_'] {
font-weight: 500;
}
/* workaround for version switcher PR 15894 */
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
display: none;
}
code {
font-weight: 600;
}

View File

@@ -50,6 +50,13 @@ function HomepageHeader() {
>
Demo
</Link>
<Link
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
to="https://immich.store"
>
Buy Merch
</Link>
</div>
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
@@ -73,9 +80,9 @@ function HomepageHeader() {
/>
<div>
<p className="font-bold text-2xl md:text-5xl ">Download mobile app</p>
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
<p className="text-lg">
Download Immich app and start backing up your photos and videos securely to your own server
Download the Immich app and start backing up your photos and videos securely to your own server
</p>
</div>
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">

View File

@@ -1,32 +1,32 @@
/docs /docs/overview/introduction 301
/docs/mobile-app-beta-program /docs/features/mobile-app 301
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 301
/docs/install /docs/install/docker-compose 301
/docs/installation/one-step-installation /docs/install/script 301
/docs/installation/portainer-installation /docs/install/portainer 301
/docs/installation/recommended-installation /docs/install/docker-compose 301
/docs/installation/unraid /docs/install/unraid 301
/docs/installation/requirements /docs/install/requirements 301
/docs/overview/logo-meaning /docs/overview/logo 301
/docs/overview/technology-stack /docs/developer/architecture 301
/docs/usage/automatic-backup /docs/features/automatic-backup 301
/docs/usage/bulk-upload /docs/features/command-line-interface 301
/docs/features/bulk-upload /docs/features/command-line-interface 301
/docs/usage/oauth /docs/administration/oauth 301
/docs/usage/post-installation /docs/install/post-install 301
/docs/usage/update /docs/install/docker-compose#step-4---upgrading 301
/docs/usage/server-commands /docs/administration/server-commands 301
/docs/features/jobs /docs/administration/jobs 301
/docs/features/oauth /docs/administration/oauth 301
/docs/features/password-login /docs/administration/password-login 301
/docs/features/server-commands /docs/administration/server-commands 301
/docs/features/storage-template /docs/administration/storage-template 301
/docs/features/user-management /docs/administration/user-management 301
/docs/developer/contributing /docs/developer/pr-checklist 301
/docs/guides/machine-learning /docs/guides/remote-machine-learning 301
/docs/administration/password-login /docs/administration/system-settings 301
/docs/features/search /docs/features/searching 301
/docs/features/smart-search /docs/features/searching 301
/docs/guides/api-album-sync /docs/community-projects 301
/docs/guides/remove-offline-files /docs/community-projects 301
/milestones /roadmap 301
/docs /docs/overview/introduction 307
/docs/mobile-app-beta-program /docs/features/mobile-app 307
/docs/contribution-guidelines /docs/overview/support-the-project#contributing 307
/docs/install /docs/install/docker-compose 307
/docs/installation/one-step-installation /docs/install/script 307
/docs/installation/portainer-installation /docs/install/portainer 307
/docs/installation/recommended-installation /docs/install/docker-compose 307
/docs/installation/unraid /docs/install/unraid 307
/docs/installation/requirements /docs/install/requirements 307
/docs/overview/logo-meaning /docs/overview/logo 307
/docs/overview/technology-stack /docs/developer/architecture 307
/docs/usage/automatic-backup /docs/features/automatic-backup 307
/docs/usage/bulk-upload /docs/features/command-line-interface 307
/docs/features/bulk-upload /docs/features/command-line-interface 307
/docs/usage/oauth /docs/administration/oauth 307
/docs/usage/post-installation /docs/install/post-install 307
/docs/usage/update /docs/install/docker-compose#step-4---upgrading 307
/docs/usage/server-commands /docs/administration/server-commands 307
/docs/features/jobs /docs/administration/jobs 307
/docs/features/oauth /docs/administration/oauth 307
/docs/features/password-login /docs/administration/password-login 307
/docs/features/server-commands /docs/administration/server-commands 307
/docs/features/storage-template /docs/administration/storage-template 307
/docs/features/user-management /docs/administration/user-management 307
/docs/developer/contributing /docs/developer/pr-checklist 307
/docs/guides/machine-learning /docs/guides/remote-machine-learning 307
/docs/administration/password-login /docs/administration/system-settings 307
/docs/features/search /docs/features/searching 307
/docs/features/smart-search /docs/features/searching 307
/docs/guides/api-album-sync /docs/community-projects 307
/docs/guides/remove-offline-files /docs/community-projects 307
/milestones /roadmap 307

View File

@@ -1,4 +1,56 @@
[
{
"label": "v1.126.1",
"url": "https://v1.126.1.archive.immich.app"
},
{
"label": "v1.126.0",
"url": "https://v1.126.0.archive.immich.app"
},
{
"label": "v1.125.7",
"url": "https://v1.125.7.archive.immich.app"
},
{
"label": "v1.125.6",
"url": "https://v1.125.6.archive.immich.app"
},
{
"label": "v1.125.5",
"url": "https://v1.125.5.archive.immich.app"
},
{
"label": "v1.125.4",
"url": "https://v1.125.4.archive.immich.app"
},
{
"label": "v1.125.3",
"url": "https://v1.125.3.archive.immich.app"
},
{
"label": "v1.125.2",
"url": "https://v1.125.2.archive.immich.app"
},
{
"label": "v1.125.1",
"url": "https://v1.125.1.archive.immich.app"
},
{
"label": "v1.125.0",
"url": "https://v1.125.0.archive.immich.app"
},
{
"label": "v1.124.2",
"url": "https://v1.124.2.archive.immich.app"
},
{
"label": "v1.124.1",
"url": "https://v1.124.1.archive.immich.app"
},
{
"label": "v1.124.0",
"url": "https://v1.124.0.archive.immich.app"
},
{
"label": "v1.123.0",
"url": "https://v1.123.0.archive.immich.app"

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -1 +1 @@
22.12.0
22.14.0

View File

@@ -34,7 +34,7 @@ services:
- 2285:2285
redis:
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
database:
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0

1151
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.123.0",
"version": "1.126.1",
"description": "",
"main": "index.js",
"type": "module",
@@ -25,16 +25,16 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
"@types/node": "^22.10.2",
"@types/node": "^22.13.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": "^8.15.0",
"@typescript-eslint/parser": "^8.15.0",
"@vitest/coverage-v8": "^2.0.5",
"@vitest/coverage-v8": "^3.0.0",
"eslint": "^9.14.0",
"eslint-config-prettier": "^9.1.0",
"eslint-config-prettier": "^10.0.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^56.0.1",
"exiftool-vendored": "^28.3.1",
@@ -50,9 +50,9 @@
"supertest": "^7.0.0",
"typescript": "^5.3.3",
"utimes": "^5.2.1",
"vitest": "^2.0.5"
"vitest": "^3.0.0"
},
"volta": {
"node": "22.12.0"
"node": "22.14.0"
}
}

View File

@@ -22,79 +22,92 @@ const user1NotShared = 'user1NotShared';
const user2SharedUser = 'user2SharedUser';
const user2SharedLink = 'user2SharedLink';
const user2NotShared = 'user2NotShared';
const user4DeletedAsset = 'user4DeletedAsset';
const user4Empty = 'user4Empty';
describe('/albums', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetMediaResponseDto;
let user1Asset2: AssetMediaResponseDto;
let user4Asset1: AssetMediaResponseDto;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];
let deletedAssetAlbum: AlbumResponseDto;
let user3: LoginResponseDto; // deleted
let user4: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
[user1, user2, user3] = await Promise.all([
[user1, user2, user3, user4] = await Promise.all([
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user4),
]);
[user1Asset1, user1Asset2] = await Promise.all([
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
utils.createAsset(user1.accessToken, { isFavorite: true }),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
user1Albums = await Promise.all([
utils.createAlbum(user1.accessToken, {
albumName: user1SharedEditorUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
assetIds: [user1Asset1.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedViewerUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
assetIds: [user1Asset1.id],
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
Promise.all([
utils.createAlbum(user1.accessToken, {
albumName: user1SharedEditorUser,
albumUsers: [
{ userId: admin.userId, role: AlbumUserRole.Editor },
{ userId: user2.userId, role: AlbumUserRole.Editor },
],
assetIds: [user1Asset1.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
utils.createAlbum(user1.accessToken, {
albumName: user1SharedViewerUser,
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
assetIds: [user1Asset1.id],
}),
]),
Promise.all([
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
albumUsers: [
{ userId: user1.userId, role: AlbumUserRole.Editor },
{ userId: user3.userId, role: AlbumUserRole.Editor },
],
}),
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
]),
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
}),
]);
user2Albums = await Promise.all([
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
albumUsers: [
{ userId: user1.userId, role: AlbumUserRole.Editor },
{ userId: user3.userId, role: AlbumUserRole.Editor },
],
}),
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
]);
await utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
});
await addAssetsToAlbum(
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
{ headers: asBearerAuth(user1.accessToken) },
);
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
await Promise.all([
addAssetsToAlbum(
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
{ headers: asBearerAuth(user1.accessToken) },
),
addAssetsToAlbum(
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
{ headers: asBearerAuth(user4.accessToken) },
),
// add shared link to user1SharedLink album
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
@@ -107,7 +120,11 @@ describe('/albums', () => {
}),
]);
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
[user2Albums[0]] = await Promise.all([
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
]);
});
describe('GET /albums', () => {
@@ -142,6 +159,10 @@ describe('/albums', () => {
...user1Albums[0],
assets: [expect.objectContaining({ isFavorite: false })],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
endDate: expect.any(String),
shared: true,
albumUsers: expect.any(Array),
});
});
@@ -280,6 +301,25 @@ describe('/albums', () => {
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
it('should return empty albums and albums where all assets are deleted', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user4.userId,
albumName: user4DeletedAsset,
shared: false,
}),
expect.objectContaining({
ownerId: user4.userId,
albumName: user4Empty,
shared: false,
}),
]),
);
});
});
describe('GET /albums/:id', () => {
@@ -299,6 +339,10 @@ describe('/albums', () => {
...user1Albums[0],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
endDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
});
@@ -330,6 +374,10 @@ describe('/albums', () => {
...user1Albums[0],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
endDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
});
@@ -344,6 +392,30 @@ describe('/albums', () => {
assets: [],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
});
it('should not count trashed assets', async () => {
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
const { status, body } = await request(app)
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
assets: [],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
});
});

View File

@@ -3,11 +3,11 @@ import {
AssetMediaStatus,
AssetResponseDto,
AssetTypeEnum,
LoginResponseDto,
SharedLinkType,
getAssetInfo,
getConfig,
getMyUser,
LoginResponseDto,
SharedLinkType,
updateConfig,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
@@ -19,7 +19,7 @@ import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -41,8 +41,6 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
return dto;
};
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`;
@@ -703,6 +701,20 @@ describe('/asset', () => {
expect(status).toEqual(200);
});
it('should set the negative rating', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ rating: -1 });
expect(body).toMatchObject({
id: user1Assets[0].id,
exifInfo: expect.objectContaining({
rating: -1,
}),
});
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)
@@ -766,7 +778,7 @@ describe('/asset', () => {
expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
});
it('should move an asset to the trash', async () => {
it('should move an asset to trash', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
const before = await utils.getAssetInfo(admin.accessToken, assetId);
@@ -782,6 +794,38 @@ describe('/asset', () => {
expect(after.isTrashed).toBe(true);
});
it('should permanently delete an asset from trash', async () => {
const { id: assetId } = await utils.createAsset(admin.accessToken);
{
const { status } = await request(app)
.delete('/assets')
.send({ ids: [assetId] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
const trashed = await utils.getAssetInfo(admin.accessToken, assetId);
expect(trashed.isTrashed).toBe(true);
{
const { status } = await request(app)
.delete('/assets')
.send({ ids: [assetId], force: true })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
{
const { status } = await request(app)
.get(`/assets/${assetId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
}
});
it('should clean up live photos', async () => {
const { id: motionId } = await utils.createAsset(admin.accessToken, {
assetData: { filename: 'test.mp4', bytes: makeRandomImage() },

View File

@@ -0,0 +1,86 @@
import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename } from 'node:path';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
describe('/jobs', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
});
describe('PUT /jobs', () => {
afterEach(async () => {
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
force: false,
});
});
it('should require authentication', async () => {
const { status, body } = await request(app).put('/jobs/metadataExtraction');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should queue metadata extraction for missing assets', async () => {
const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`;
await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(path1), filename: basename(path1) },
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Pause,
force: false,
});
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(path2), filename: basename(path2) },
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
{
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo?.make).toBeNull();
}
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Empty,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Resume,
force: false,
});
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
command: JobCommand.Start,
force: false,
});
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
{
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
}
});
});
});

View File

@@ -1,4 +1,4 @@
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
import { Socket } from 'socket.io-client';
import { userDto, uuidDto } from 'src/fixtures';
@@ -8,8 +8,6 @@ import request from 'supertest';
import { utimes } from 'utimes';
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
describe('/libraries', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
@@ -298,13 +296,35 @@ describe('/libraries', () => {
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
const { assets } = await utils.searchAssets(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
libraryId: library.id,
});
expect(assets.count).toBe(1);
});
it('should process metadata and thumbnails for external asset', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
});
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, {
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
libraryId: library.id,
});
expect(assets.count).toBe(1);
const asset = assets.items[0];
expect(asset.exifInfo).not.toBe(null);
expect(asset.exifInfo?.dateTimeOriginal).not.toBe(null);
expect(asset.thumbhash).not.toBe(null);
});
it('should scan external library with exclusion pattern', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
@@ -312,13 +332,7 @@ describe('/libraries', () => {
exclusionPatterns: ['**/directoryA'],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -332,13 +346,7 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -357,13 +365,7 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -385,13 +387,7 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -443,13 +439,7 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -473,23 +463,12 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -520,21 +499,12 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -564,21 +534,14 @@ describe('/libraries', () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
@@ -588,7 +551,7 @@ describe('/libraries', () => {
expect(newAssets.items).toEqual([]);
});
it('should set an asset offline its file is not in any import path', async () => {
it('should set an asset offline if its file is not in any import path', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const library = await utils.createLibrary(admin.accessToken, {
@@ -596,26 +559,18 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [`${testAssetDirInternal}/temp/another-path/`] });
await utils.updateLibrary(admin.accessToken, library.id, {
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
});
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
@@ -635,8 +590,7 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, {
libraryId: library.id,
@@ -644,13 +598,9 @@ describe('/libraries', () => {
});
expect(assets.count).toBe(1);
await request(app)
.put(`/libraries/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/directoryB/**'] });
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(trashedAsset.isTrashed).toBe(true);
@@ -666,25 +616,18 @@ describe('/libraries', () => {
]);
});
it('should not trash an online asset', async () => {
it('should not set an asset offline if its file exists, is in an import path, and not 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 utils.scan(admin.accessToken, library.id);
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assetsBefore.count).toBeGreaterThan(1);
const { status } = await request(app)
.post(`/libraries/${library.id}/scan`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send();
expect(status).toBe(204);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -701,11 +644,7 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -728,10 +667,7 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -755,10 +691,7 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -782,19 +715,13 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -817,18 +744,12 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -851,18 +772,12 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -886,19 +801,13 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -922,18 +831,12 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -957,18 +860,12 @@ describe('/libraries', () => {
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
await utils.scan(admin.accessToken, library.id);
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
@@ -982,6 +879,144 @@ describe('/libraries', () => {
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
});
});
it('should set an offline asset to online if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(offlineAsset.isTrashed).toBe(true);
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(offlineAsset.isOffline).toBe(true);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
expect(assets.count).toBe(1);
}
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
await utils.scan(admin.accessToken, library.id);
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(backOnlineAsset.isTrashed).toBe(false);
expect(backOnlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(backOnlineAsset.isOffline).toBe(false);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.count).toBe(1);
}
});
it('should not set an offline asset to online if its file exists, is not covered by an exclusion pattern, but is outside of all import paths', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
expect(assets.count).toBe(1);
}
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(offlineAsset.isTrashed).toBe(true);
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(offlineAsset.isOffline).toBe(true);
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
await utils.updateLibrary(admin.accessToken, library.id, {
importPaths: [`${testAssetDirInternal}/temp/another-path`],
});
await utils.scan(admin.accessToken, library.id);
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(stillOfflineAsset.isTrashed).toBe(true);
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(stillOfflineAsset.isOffline).toBe(true);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
expect(assets.count).toBe(1);
}
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
});
it('should not set an offline asset to online if its file exists, is in an import path, but is covered by an exclusion pattern', async () => {
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
await utils.scan(admin.accessToken, library.id);
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
await utils.scan(admin.accessToken, library.id);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
expect(assets.count).toBe(1);
}
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(offlineAsset.isTrashed).toBe(true);
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(offlineAsset.isOffline).toBe(true);
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await utils.scan(admin.accessToken, library.id);
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
expect(stillOfflineAsset.isTrashed).toBe(true);
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
expect(stillOfflineAsset.isOffline).toBe(true);
{
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
expect(assets.count).toBe(1);
}
});
});
describe('POST /libraries/:id/validate', () => {
@@ -1089,8 +1124,7 @@ describe('/libraries', () => {
importPaths: [`${testAssetDirInternal}/temp`],
});
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
await utils.scan(admin.accessToken, library.id);
const { status, body } = await request(app)
.delete(`/libraries/${library.id}`)

View File

@@ -93,8 +93,6 @@ describe('/memories', () => {
data: { year: 2021 },
createdAt: expect.any(String),
updatedAt: expect.any(String),
deletedAt: null,
seenAt: null,
isSaved: false,
memoryAt: expect.any(String),
ownerId: user.userId,

View File

@@ -13,8 +13,8 @@ import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
const authServer = {
internal: 'http://auth-server:3000',
external: 'http://127.0.0.1:3000',
internal: 'http://auth-server:2286',
external: 'http://127.0.0.1:2286',
};
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { getPerson, LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, utils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
@@ -195,12 +195,29 @@ describe('/people', () => {
.send({
name: 'New Person',
birthDate: '1990-01-01',
color: '#333',
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
name: 'New Person',
birthDate: '1990-01-01',
birthDate: '1990-01-01T00:00:00.000Z',
});
});
it('should create a favorite person', async () => {
const { status, body } = await request(app)
.post(`/people`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
name: 'New Favorite Person',
isFavorite: true,
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
name: 'New Favorite Person',
isFavorite: true,
});
});
});
@@ -216,6 +233,7 @@ describe('/people', () => {
{ key: 'name', type: 'string' },
{ key: 'featureFaceAssetId', type: 'string' },
{ key: 'isHidden', type: 'boolean value' },
{ key: 'isFavorite', type: 'boolean value' },
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
@@ -244,7 +262,7 @@ describe('/people', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: '1990-01-01' });
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: '1990-01-01' });
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
});
it('should clear a date of birth', async () => {
@@ -255,6 +273,42 @@ describe('/people', () => {
expect(status).toBe(200);
expect(body).toMatchObject({ birthDate: null });
});
it('should set a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: '#555' });
expect(status).toBe(200);
expect(body).toMatchObject({ color: '#555' });
});
it('should clear a color', async () => {
const { status, body } = await request(app)
.put(`/people/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ color: null });
expect(status).toBe(200);
expect(body.color).toBeUndefined();
});
it('should mark a person as favorite', async () => {
const person = await utils.createPerson(admin.accessToken, {
name: 'visible_person',
});
expect(person.isFavorite).toBe(false);
const { status, body } = await request(app)
.put(`/people/${person.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ isFavorite: true });
expect(status).toBe(200);
expect(body).toMatchObject({ isFavorite: true });
const person2 = await getPerson({ id: person.id }, { headers: asBearerAuth(admin.accessToken) });
expect(person2).toMatchObject({ id: person.id, isFavorite: true });
});
});
describe('POST /people/:id/merge', () => {

View File

@@ -1,10 +1,10 @@
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const today = DateTime.now();
@@ -462,6 +462,55 @@ describe('/search', () => {
});
});
describe('POST /search/random', () => {
beforeAll(async () => {
await Promise.all([
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
]);
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
});
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app)
.post('/search/random')
.send({ size: 1 })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(1);
expect(assets[0].ownerId).toBe(admin.userId);
});
it.each(TEN_TIMES)('should return 2 random assets', async () => {
const { status, body } = await request(app)
.post('/search/random')
.send({ size: 2 })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(2);
expect(assets[0].ownerId).toBe(admin.userId);
expect(assets[1].ownerId).toBe(admin.userId);
});
});
describe('GET /search/explore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/explore');

View File

@@ -89,7 +89,7 @@ describe('/shared-links', () => {
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /share/${key}', () => {
describe('GET /share/:key', () => {
it('should have correct asset count in meta tag for non-empty album', async () => {
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
expect(resp.status).toBe(200);
@@ -139,7 +139,10 @@ describe('/shared-links', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: linkWithAlbum.id }),
expect.objectContaining({ id: linkWithAssets.id }),
expect.objectContaining({
id: linkWithAssets.id,
assets: expect.arrayContaining([expect.objectContaining({ id: asset1.id })]),
}),
expect.objectContaining({ id: linkWithPassword.id }),
expect.objectContaining({ id: linkWithMetadata.id }),
expect.objectContaining({ id: linkWithoutMetadata.id }),
@@ -147,6 +150,30 @@ describe('/shared-links', () => {
);
});
it('should filter on albumId', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${album.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: linkWithAlbum.id }),
expect.objectContaining({ id: linkWithPassword.id }),
]),
);
});
it('should find 0 albums', async () => {
const { status, body } = await request(app)
.get(`/shared-links?albumId=${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(0);
});
it('should not get shared links created by other users', async () => {
const { status, body } = await request(app)
.get('/shared-links')
@@ -170,13 +197,19 @@ describe('/shared-links', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
album,
album: expect.objectContaining({ id: album.id }),
userId: user1.userId,
type: SharedLinkType.Album,
}),
);
});
it('should increment the view count', async () => {
const request1 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
const request2 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
expect(request2.body.viewCount).toBe(request1.body.viewCount + 1);
});
it('should return unauthorized for incorrect shared link', async () => {
const { status, body } = await request(app)
.get('/shared-links/me')
@@ -208,7 +241,7 @@ describe('/shared-links', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
album,
album: expect.objectContaining({ id: album.id }),
userId: user1.userId,
type: SharedLinkType.Album,
}),
@@ -262,7 +295,7 @@ describe('/shared-links', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
album,
album: expect.objectContaining({ id: album.id }),
userId: user1.userId,
type: SharedLinkType.Album,
}),

View File

@@ -119,93 +119,84 @@ describe('/stacks', () => {
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] });
describe('GET /assets/:id', () => {
it('should include stack details for the primary asset', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// expect(status).toBe(400);
// expect(body).toEqual(errorDto.noPermission);
// });
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
// 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] });
const { status, body } = await request(app)
.get(`/assets/${asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
// expect(status).toBe(204);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: asset1.id,
stack: {
id: expect.any(String),
assetCount: 2,
primaryAssetId: asset1.id,
},
}),
);
});
// 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 include stack details for a non-primary asset', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// 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] });
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
// expect(status).toBe(204);
const { status, body } = await request(app)
.get(`/assets/${asset2.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
// 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 }),
// ]),
// );
// });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: asset2.id,
stack: {
id: expect.any(String),
assetCount: 2,
primaryAssetId: asset1.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] });
describe('GET /stacks/:id', () => {
it('should include exifInfo in stack assets', async () => {
const [asset1, asset2] = await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
// expect(status).toBe(204);
const stack = await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
// expect(asset.stack).toBeUndefined();
// });
const { status, body } = await request(app)
.get(`/stacks/${stack.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
// 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 }),
// ]),
// );
// });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: stack.id,
primaryAssetId: asset1.id,
assets: expect.arrayContaining([
expect.objectContaining({ id: asset1.id, exifInfo: expect.any(Object) }),
expect.objectContaining({ id: asset2.id, exifInfo: expect.any(Object) }),
]),
}),
);
});
});
});

View File

@@ -73,7 +73,7 @@ describe('/trash', () => {
expect(existsSync(before.originalPath)).toBe(false);
});
it('should not delete offline-trashed assets from disk', async () => {
it('should remove offline assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
@@ -88,7 +88,7 @@ describe('/trash', () => {
expect(assets.items.length).toBe(1);
const asset = assets.items[0];
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -105,6 +105,41 @@ describe('/trash', () => {
const assetAfter = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetAfter).toMatchObject({ isTrashed: true, isOffline: true });
});
it.skip('should not delete offline assets from disk', async () => {
// Can't be tested at the moment due to no mechanism to forward time
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
});
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
expect(assets.items.length).toBe(1);
const asset = assets.items[0];
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
expect(existsSync(`${testAssetDir}/temp/offline/offline.png`)).toBe(true);
@@ -137,7 +172,7 @@ describe('/trash', () => {
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: false }));
});
it('should not restore offline-trashed assets', async () => {
it('should not restore offline assets', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
@@ -152,7 +187,7 @@ describe('/trash', () => {
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await scan(admin.accessToken, library.id);
@@ -195,7 +230,7 @@ describe('/trash', () => {
expect(after.isTrashed).toBe(false);
});
it('should not restore an offline-trashed asset', async () => {
it('should not restore an offline asset', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp/offline`],
@@ -210,7 +245,7 @@ describe('/trash', () => {
expect(assets.count).toBe(1);
const assetId = assets.items[0].id;
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');

View File

@@ -356,5 +356,24 @@ describe('/admin/users', () => {
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should restore a user', async () => {
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.post(`/admin/users/${user.userId}/restore`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
id: user.userId,
email: user.userEmail,
status: 'active',
deletedAt: null,
}),
);
});
});
});

View File

@@ -129,6 +129,8 @@ describe('/users', () => {
expect(body).toEqual({
...before,
updatedAt: expect.any(String),
profileChangedAt: expect.any(String),
createdAt: expect.any(String),
name: 'Name',
});
});
@@ -177,6 +179,8 @@ describe('/users', () => {
...before,
email: 'non-admin@immich.cloud',
updatedAt: expect.anything(),
createdAt: expect.anything(),
profileChangedAt: expect.anything(),
});
});
});

View File

@@ -51,7 +51,7 @@ const setup = async () => {
const { privateKey, publicKey } = await generateKeyPair('RS256');
const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
const port = 3000;
const port = 2286;
const host = '0.0.0.0';
const oidc = new Provider(`http://${host}:${port}`, {
renderError: async (ctx, out, error) => {

View File

@@ -6,10 +6,13 @@ import {
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
JobCommandDto,
JobName,
MetadataSearchDto,
Permission,
PersonCreateDto,
SharedLinkCreateDto,
UpdateLibraryDto,
UserAdminCreateDto,
UserPreferencesUpdateDto,
ValidateLibraryDto,
@@ -27,7 +30,9 @@ import {
getAssetInfo,
getConfigDefaults,
login,
scanLibrary,
searchAssets,
sendJobCommand,
setBaseUrl,
signUpAdmin,
tagAssets,
@@ -35,6 +40,7 @@ import {
updateAlbumUser,
updateAssets,
updateConfig,
updateLibrary,
updateMyPreferences,
upsertTags,
validate,
@@ -42,7 +48,7 @@ import {
import { BrowserContext } from '@playwright/test';
import { exec, spawn } from 'node:child_process';
import { createHash } from 'node:crypto';
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { existsSync, mkdirSync, renameSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path, { dirname } from 'node:path';
import { setTimeout as setAsyncTimeout } from 'node:timers/promises';
@@ -74,6 +80,7 @@ export const immichCli = (args: string[]) =>
export const immichAdmin = (args: string[]) =>
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const executeCommand = (command: string, args: string[]) => {
let _resolve: (value: CommandResponse) => void;
@@ -392,6 +399,14 @@ export const utils = {
rmSync(path);
},
renameImageFile: (oldPath: string, newPath: string) => {
if (!existsSync(oldPath)) {
return;
}
renameSync(oldPath, newPath);
},
removeDirectory: (path: string) => {
if (!existsSync(path)) {
return;
@@ -447,6 +462,9 @@ export const utils = {
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
updateLibrary: (accessToken: string, id: string, dto: UpdateLibraryDto) =>
updateLibrary({ id, updateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
createPartner: (accessToken: string, id: string) => createPartner({ id }, { headers: asBearerAuth(accessToken) }),
updateMyPreferences: (accessToken: string, userPreferencesUpdateDto: UserPreferencesUpdateDto) =>
@@ -461,6 +479,9 @@ export const utils = {
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
await context.addCookies([
{
@@ -533,6 +554,14 @@ export const utils = {
await immichCli(['login', app, `${key.secret}`]);
return key.secret;
},
scan: async (accessToken: string, id: string) => {
await scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
await utils.waitForQueueFinish(accessToken, 'library');
await utils.waitForQueueFinish(accessToken, 'sidecar');
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
},
};
utils.initSdk();

View File

@@ -6,7 +6,7 @@ import {
SharedLinkType,
createAlbum,
} from '@immich/sdk';
import { test } from '@playwright/test';
import { expect, test } from '@playwright/test';
import { asBearerAuth, utils } from 'src/utils';
test.describe('Shared Links', () => {
@@ -65,6 +65,38 @@ test.describe('Shared Links', () => {
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
});
test('show-password button visible', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`);
await page.getByPlaceholder('Password').fill('test-password');
await page.getByRole('button', { name: 'Show password' }).waitFor();
});
test('view password for shared link', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`);
const input = page.getByPlaceholder('Password');
await input.fill('test-password');
await page.getByRole('button', { name: 'Show password' }).click();
// await page.getByText('test-password', { exact: true }).waitFor();
await expect(input).toHaveAttribute('type', 'text');
});
test('hide-password button visible', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`);
const input = page.getByPlaceholder('Password');
await input.fill('test-password');
await page.getByRole('button', { name: 'Show password' }).click();
await page.getByRole('button', { name: 'Hide password' }).waitFor();
});
test('hide password for shared link', async ({ page }) => {
await page.goto(`/share/${sharedLinkPassword.key}`);
const input = page.getByPlaceholder('Password');
await input.fill('test-password');
await page.getByRole('button', { name: 'Show password' }).click();
await page.getByRole('button', { name: 'Hide password' }).click();
await expect(input).toHaveAttribute('type', 'password');
});
test('show error for invalid shared link', async ({ page }) => {
await page.goto('/share/invalid');
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();

View File

@@ -23,6 +23,7 @@
"add_to": "Voeg na...",
"add_to_album": "Voeg na album",
"add_to_shared_album": "Voeg na gedeelde album",
"add_url": "Voeg URL by",
"added_to_archive": "By argief gevoeg",
"added_to_favorites": "By gunstelinge gevoeg",
"added_to_favorites_count": "Het {count, number} by gunstelinge gevoeg",
@@ -52,6 +53,10 @@
"cron_expression_description": "Stel die skanderingsinterval in met die cron-formaat. Vir meer inligting verwys asseblief na bv. <link>Crontab Guru</link>",
"cron_expression_presets": "Cron uitdrukking voorafinstellings",
"disable_login": "Deaktiveer aanmelding",
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search"
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search",
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
"external_library_management": "Eksterne Biblioteek-opsies",
"face_detection": "Gesigsopsporing"
}
}

View File

@@ -1,5 +1,5 @@
{
"about": "تحديث",
"about": "من نحن",
"account": "الحساب",
"account_settings": "إعدادات الحساب",
"acknowledge": "أُدرك ذلك",
@@ -23,6 +23,7 @@
"add_to": "إضافة إلى…",
"add_to_album": "إضافة إلى ألبوم",
"add_to_shared_album": "إضافة إلى ألبوم مشترك",
"add_url": "إضافة رابط",
"added_to_archive": "أُضيفت للأرشيف",
"added_to_favorites": "أُضيفت للمفضلات",
"added_to_favorites_count": "تم إضافة {count, number} إلى المفضلات",
@@ -130,7 +131,7 @@
"machine_learning_smart_search_description": "البحث عن الصور بشكل دلالي باستخدام تضمينات CLIP",
"machine_learning_smart_search_enabled": "تفعيل البحث الذكي",
"machine_learning_smart_search_enabled_description": "إذا تم تعطيله، فلن يتم ترميز الصور للبحث الذكي.",
"machine_learning_url_description": "عنوان URL لخادم التعلم الآلي",
"machine_learning_url_description": "عنوان URL لخادم التعلم الآلي. إذا تم توفير أكثر من عنوان URL، فسيتم محاولة الوصول إلى كل خادم على حدة حتى يستجيب أحد الخوادم بنجاح، بالترتيب من الأول إلى الأخير.",
"manage_concurrency": "إدارة التزامن",
"manage_log_settings": "إدارة إعدادات السجلات",
"map_dark_style": "النمط الداكن",
@@ -249,6 +250,8 @@
"storage_template_user_label": "<code>{label}</code> هو تسمية التخزين الخاصة بالمستخدم",
"system_settings": "إعدادات النظام",
"tag_cleanup_job": "تنظيف العلامة",
"template_email_preview": "عرض مسبق",
"template_email_settings": "نماذج البريد الالكتروني",
"theme_custom_css_settings": "CSS مخصص",
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
"theme_settings": "إعدادات السمة",

View File

@@ -1,5 +1,5 @@
{
"about": "Yenilə",
"about": "Haqqinda",
"account": "Hesab",
"account_settings": "Hesab parametrləri",
"acknowledge": "Təsdiq et",

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,8 @@
{
"about": "Sobre",
"about": "Quant a",
"account": "Compte",
"account_settings": "Configuració del compte",
"acknowledge": "Confirmar",
"acknowledge": "D'acord",
"action": "Acció",
"actions": "Accions",
"active": "Actiu",
@@ -131,7 +131,7 @@
"machine_learning_smart_search_description": "Cerca imatges semànticament emprant incrustacions CLIP",
"machine_learning_smart_search_enabled": "Activa la cerca intel·ligent",
"machine_learning_smart_search_enabled_description": "Si està deshabilitat les imatges no es codificaran per la cerca intel·ligent.",
"machine_learning_url_description": "URL del servidor d'aprenentatge automàtic",
"machine_learning_url_description": "La URL del servidor d'aprenentatge automàtic. Si es proporciona més d'una URL, s'intentarà accedir a cada servidor en ordre fins que un d'ells respongui correctament.",
"manage_concurrency": "Gestiona la concurrència",
"manage_log_settings": "Gestiona la configuració del registre",
"map_dark_style": "Tema fosc",
@@ -223,6 +223,8 @@
"send_welcome_email": "Enviar correu electrònic de benvinguda",
"server_external_domain_settings": "Domini extern",
"server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
"server_public_users": "Usuaris públics",
"server_public_users_description": "Tots els usuaris (nom i correu electrònic) apareixen a la llista a l'afegir un usuari als àlbums compartits. Si es desactiva, la llista només serà disponible pels usuaris administradors.",
"server_settings": "Configuració del servidor",
"server_settings_description": "Gestiona la configuració del servidor",
"server_welcome_message": "Missatge de benvinguda",
@@ -248,6 +250,16 @@
"storage_template_user_label": "<code>{label}</code> és l'etiqueta d'emmagatzematge de l'usuari",
"system_settings": "Configuració del sistema",
"tag_cleanup_job": "Neteja d'etiqueta",
"template_email_available_tags": "Pots fer servir les següents variables a la teva plantilla: {tags}",
"template_email_if_empty": "Si la plantilla està buida, es farà servir el correu electrònic predeterminat.",
"template_email_invite_album": "Plantilla per l'àlbum d'invitacions",
"template_email_preview": "Vista prèvia",
"template_email_settings": "Plantilles de correu electrònic",
"template_email_settings_description": "Gestionar les plantilles de notificació per correu electrònic personalitzades",
"template_email_update_album": "Actualitzar la plantilla de l'àlbum",
"template_email_welcome": "Plantilla del correu de benvinguda",
"template_settings": "Plantilles de notificació",
"template_settings_description": "Gestiona les plantilles personalitzades per les notificacions.",
"theme_custom_css_settings": "CSS personalitzat",
"theme_custom_css_settings_description": "Els Fulls d'Estil en Cascada permeten personalitzar el disseny d'Immich.",
"theme_settings": "Configuració del tema",
@@ -277,6 +289,8 @@
"transcoding_constant_rate_factor": "Factor de taxa constant (-crf)",
"transcoding_constant_rate_factor_description": "Nivell de qualitat del vídeo. Els valors típics són 23 per a H.264, 28 per a HEVC, 31 per a VP9 i 35 per a AV1. Més baix és millor, però produeix fitxers més grans.",
"transcoding_disabled_description": "No transcodifiqueu cap vídeo, pot interrompre la reproducció en alguns clients",
"transcoding_encoding_options": "Opcions de codificació",
"transcoding_encoding_options_description": "Establiu còdecs, resolució, qualitat i altres opcions per als vídeos codificats",
"transcoding_hardware_acceleration": "Acceleració de maquinari",
"transcoding_hardware_acceleration_description": "Experimental. Molt més ràpid, però tindrà una qualitat més baixa amb la mateixa taxa de bits",
"transcoding_hardware_decoding": "Descodificació de maquinari",
@@ -289,6 +303,8 @@
"transcoding_max_keyframe_interval": "Interval màxim de fotogrames clau",
"transcoding_max_keyframe_interval_description": "Estableix la distància màxima entre fotogrames clau. Els valors més baixos empitjoren l'eficiència de la compressió, però milloren els temps de cerca i poden millorar la qualitat en escenes amb moviment ràpid. 0 estableix aquest valor automàticament.",
"transcoding_optimal_description": "Vídeos superiors a la resolució objectiu o que no tenen un format acceptat",
"transcoding_policy": "Política de transcodificació",
"transcoding_policy_description": "Estableix quan es transcodificarà un vídeo",
"transcoding_preferred_hardware_device": "Dispositiu de maquinari preferit",
"transcoding_preferred_hardware_device_description": "S'aplica només a VAAPI i QSV. Estableix el node dri utilitzat per a la transcodificació de maquinari.",
"transcoding_preset_preset": "Preestablert (-preset)",
@@ -297,7 +313,7 @@
"transcoding_reference_frames_description": "El nombre de fotogrames a fer referència en comprimir un fotograma determinat. Els valors més alts milloren l'eficiència de la compressió, però alenteixen la codificació. 0 estableix aquest valor automàticament.",
"transcoding_required_description": "Només vídeos que no tenen un format acceptat",
"transcoding_settings": "Configuració de transcodificació de vídeo",
"transcoding_settings_description": "Gestiona la resolució i codificació dels fitxers de vídeo",
"transcoding_settings_description": "Gestionar quins vídeos transcodificar i com processar-los",
"transcoding_target_resolution": "Resolució objectiu",
"transcoding_target_resolution_description": "Les resolucions més altes poden conservar més detalls, però triguen més temps a codificar-se, tenen mides de fitxer més grans i poden reduir la capacitat de resposta de l'aplicació.",
"transcoding_temporal_aq": "AQ temporal",
@@ -507,6 +523,10 @@
"date_range": "Interval de dates",
"day": "Dia",
"deduplicate_all": "Desduplica-ho tot",
"deduplication_criteria_1": "Mida d'imatge en bytes",
"deduplication_criteria_2": "Quantitat de dades EXIF",
"deduplication_info": "Informació de deduplicació",
"deduplication_info_description": "Per preseleccionar recursos automàticament i eliminar els duplicats de manera massiva, ens fixem en:",
"default_locale": "Localització predeterminada",
"default_locale_description": "Format de dates i números segons la configuració del navegador",
"delete": "Esborra",
@@ -722,6 +742,7 @@
"external": "Extern",
"external_libraries": "Llibreries externes",
"face_unassigned": "Sense assignar",
"failed_to_load_assets": "Error carregant recursos",
"favorite": "Preferit",
"favorite_or_unfavorite_photo": "Foto preferida o no preferida",
"favorites": "Preferits",
@@ -742,6 +763,7 @@
"get_help": "Aconseguir ajuda",
"getting_started": "Començant",
"go_back": "Torna",
"go_to_folder": "Anar al directori",
"go_to_search": "Vés a cercar",
"group_albums_by": "Agrupa àlbums per...",
"group_no": "Cap agrupació",
@@ -1021,6 +1043,7 @@
"reassigned_assets_to_new_person": "{count, plural, one {S'ha reassignat # recurs} other {S'han reassignat # recursos}} a una persona nova",
"reassing_hint": "Assignar els elements seleccionats a una persona existent",
"recent": "Recent",
"recent-albums": "Àlbums recents",
"recent_searches": "Cerques recents",
"refresh": "Actualitzar",
"refresh_encoded_videos": "Actualitza vídeos codificats",
@@ -1042,6 +1065,7 @@
"remove_from_album": "Treu de l'àlbum",
"remove_from_favorites": "Eliminar dels preferits",
"remove_from_shared_link": "Eliminar de l'enllaç compartit",
"remove_url": "Eliminar URL",
"remove_user": "Eliminar l'usuari",
"removed_api_key": "Eliminada la clau d'API: {name}",
"removed_from_archive": "Eliminat de l'arxiu",
@@ -1126,6 +1150,7 @@
"server_version": "Versió del servidor",
"set": "Establir",
"set_as_album_cover": "Establir com a portada de l'àlbum",
"set_as_featured_photo": "Estableix com a foto destacada",
"set_as_profile_picture": "Establir com a imatge de perfil",
"set_date_of_birth": "Establir data de naixement",
"set_profile_picture": "Establir imatge de perfil",
@@ -1181,11 +1206,12 @@
"sort_items": "Nombre d'elements",
"sort_modified": "Data de modificació",
"sort_oldest": "Foto més antiga",
"sort_people_by_similarity": "Ordenar personar per semblança",
"sort_recent": "Foto més recent",
"sort_title": "Títol",
"source": "Font",
"stack": "Apila",
"stack_duplicates": "Aplicar duplicats",
"stack_duplicates": "Aplica duplicats",
"stack_select_one_photo": "Selecciona una imatge principal per la pila",
"stack_selected_photos": "Apila les fotos seleccionades",
"stacked_assets_count": "Apilats {count, plural, one {# element} other {# elements}}",
@@ -1224,6 +1250,7 @@
"they_will_be_merged_together": "Es combinaran",
"third_party_resources": "Recursos de tercers",
"time_based_memories": "Records basats en el temps",
"timeline": "Cronologia",
"timezone": "Fus horari",
"to_archive": "Arxivar",
"to_change_password": "Canviar la contrasenya",
@@ -1233,6 +1260,7 @@
"to_trash": "Paperera",
"toggle_settings": "Canvia configuració",
"toggle_theme": "Alternar tema",
"total": "Total",
"total_usage": "Ús total",
"trash": "Paperera",
"trash_all": "Envia-ho tot a la paperera",
@@ -1305,6 +1333,7 @@
"view_all_users": "Mostra tot els usuaris",
"view_in_timeline": "Mostrar a la línia de temps",
"view_links": "Mostra enllaços",
"view_name": "Veure",
"view_next_asset": "Mostra el següent element",
"view_previous_asset": "Mostra l'element anterior",
"view_stack": "Veure la pila",

View File

@@ -289,6 +289,8 @@
"transcoding_constant_rate_factor": "Faktor konstantní rychlosti (-crf)",
"transcoding_constant_rate_factor_description": "Úroveň kvality videa. Typické hodnoty jsou 23 pro H.264, 28 pro HEVC, 31 pro VP9 a 35 pro AV1. Nižší je lepší, ale vytváří větší soubory.",
"transcoding_disabled_description": "Nepřekódovávejte žádná videa, u některých klientů může dojít k znemožnění přehrávání",
"transcoding_encoding_options": "Možnosti kódování",
"transcoding_encoding_options_description": "Nastavte kodeky, rozlišení, kvalitu a další možnosti pro kódovaná videa",
"transcoding_hardware_acceleration": "Hardwarová akcelerace",
"transcoding_hardware_acceleration_description": "Experimentální; mnohem rychlejší, ale při stejném datovém toku bude mít nižší kvalitu",
"transcoding_hardware_decoding": "Hardwarové dekódování",
@@ -301,6 +303,8 @@
"transcoding_max_keyframe_interval": "Maximální interval klíčových snímků",
"transcoding_max_keyframe_interval_description": "Nastavuje maximální vzdálenost mezi klíčovými snímky. Nižší hodnoty zhoršují účinnost komprese, ale zlepšují rychlost při přeskakování a mohou zlepšit kvalitu ve scénách s rychlým pohybem. Hodnota 0 nastavuje tuto hodnotu automaticky.",
"transcoding_optimal_description": "Videa s vyšším než cílovým rozlišením nebo videa, která nejsou v akceptovaném formátu",
"transcoding_policy": "Politika překódovávání",
"transcoding_policy_description": "Nastavte po překódování videa",
"transcoding_preferred_hardware_device": "Preferované hardwarové zařízení",
"transcoding_preferred_hardware_device_description": "Platí pouze pro VAAPI a QSV. Nastaví dri uzel použitý pro hardwarové překódování.",
"transcoding_preset_preset": "Preset (-preset)",
@@ -309,7 +313,7 @@
"transcoding_reference_frames_description": "Počet referenčních snímků při kompresi daného snímku. Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Hodnota 0 toto nastavuje automaticky.",
"transcoding_required_description": "Pouze videa, která nejsou v akceptovaném formátu",
"transcoding_settings": "Překódování videa",
"transcoding_settings_description": "Správa informací o rozlišení a kódování videosouborů",
"transcoding_settings_description": "Správa rozlišení a kódování videosouborů",
"transcoding_target_resolution": "Cílové rozlišení",
"transcoding_target_resolution_description": "Vyšší rozlišení mohou zachovat více detailů, ale jejich kódování trvá déle, mají větší velikost souboru a mohou snížit odezvu aplikace.",
"transcoding_temporal_aq": "Temporal AQ",
@@ -519,6 +523,10 @@
"date_range": "Rozsah dat",
"day": "Den",
"deduplicate_all": "Odstranit všechny duplicity",
"deduplication_criteria_1": "Velikost obrázku v bajtech",
"deduplication_criteria_2": "Počet EXIF dat",
"deduplication_info": "Informace o deduplikaci",
"deduplication_info_description": "Pro automatický předvýběr položek a hromadné odstranění duplicit se zohledňuje:",
"default_locale": "Výchozí jazyk",
"default_locale_description": "Formátovat datumy a čísla podle místního prostředí prohlížeče",
"delete": "Smazat",
@@ -755,6 +763,7 @@
"get_help": "Získat pomoc",
"getting_started": "Začínáme",
"go_back": "Přejít zpět",
"go_to_folder": "Přejít do složky",
"go_to_search": "Přejít na vyhledávání",
"group_albums_by": "Seskupit alba podle...",
"group_no": "Neseskupovat",
@@ -916,7 +925,7 @@
"oldest_first": "Nejstarší první",
"onboarding": "Zahájení",
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.",
"onboarding_theme_description": "Zvolte si barevné téma pro svou instanci. Můžete to později změnit v nastavení.",
"onboarding_theme_description": "Zvolte si barevný motiv pro svou instanci. Můžete to později změnit v nastavení.",
"onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.",
"onboarding_welcome_user": "Vítej, {user}",
"online": "Online",
@@ -1141,6 +1150,7 @@
"server_version": "Verze serveru",
"set": "Nastavit",
"set_as_album_cover": "Nastavit jako obal alba",
"set_as_featured_photo": "Nastavit jako hlavní fotografii",
"set_as_profile_picture": "Nastavit jako profilový obrázek",
"set_date_of_birth": "Nastavit datum narození",
"set_profile_picture": "Nastavit profilový obrázek",
@@ -1196,6 +1206,7 @@
"sort_items": "Počet položek",
"sort_modified": "Datum modifikace",
"sort_oldest": "Nejstarší fotka",
"sort_people_by_similarity": "Seřadit lidi podle podobnosti",
"sort_recent": "Nejnovější fotka",
"sort_title": "Název",
"source": "Zdroj",
@@ -1275,7 +1286,7 @@
"unselect_all": "Zrušit výběr všech",
"unselect_all_duplicates": "Zrušit výběr všech duplicit",
"unstack": "Zrušit seskupení",
"unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}",
"unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}",
"untracked_files": "Nesledované soubory",
"untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
"up_next": "To je prozatím vše",

View File

@@ -45,8 +45,42 @@
"image_preview_quality_description": "1-100 таран малтанхи пахалӑх. Ҫӳллӗреххи лайӑхрах, анчах та пысӑкрах файлсем туса кӑларать тата приложенисен хуравлӑхне чакарма пултарать. Пӗчӗк хак лартни машинӑллӑ вӗренӳ пахалӑхне витӗм кӳме пултарать.",
"image_preview_title": "Малтанлӑха пӑхмалли ӗнерлевсем",
"image_quality": "Пахалӑх",
"image_resolution": "Виҫе"
"image_resolution": "Виҫе",
"map_gps_settings": "Карттӑ тата GPS ĕнерленĕвĕ",
"map_gps_settings_description": "Карттӑпа GPS (каялла геоюмлани) ӗнерленисене йӗркелесе тӑрӑр",
"map_settings": "Карттӑ"
},
"albums": "Албумсем",
"albums_count": "{count, plural, one {{count, number} албум} other {{count, number} албумсем}}",
"all": "Пурте",
"all_albums": "Пурте албумсем",
"explore": "Тишкер",
"explorer": "Тишкерӳҫӗ",
"favorite": "Юратнӑ",
"favorite_or_unfavorite_photo": "Юратнӑ е юратман сӑнӳкерчӗк",
"favorites": "Юратнисем",
"feature_photo_updated": "Уйрӑм сӑнӳкерчӗк ҫӗнетнӗ",
"manage_sharing_with_partners": "Партнерсемпе пайланассине йӗркелесе пырӑр",
"map": "Карттӑ",
"map_marker_for_images": "{city}, {country} ҫинче ӳкернӗ ӳкерчӗксем валли карттӑ маркерӗ",
"map_marker_with_image": "Карттӑ маркерӗ ӳкерчӗкпе",
"map_settings": "Карттӑ ĕнерленĕвĕ",
"no_explore_results_message": "Хӑвӑр коллекципе киленмешкӗн сӑнӳкерчӗксем ытларах тийӗр.",
"open_in_openstreetmap": "OpenStreetMap-па уҫ",
"partner_sharing": "Партнер пайланӑвӗ",
"people": "Ҫынсем",
"photos": "Сӑнӳкерчӗксем",
"photos_and_videos": "Сӑнӳкерчӗксем тете Видеосем",
"photos_count": "{count, plural, one {{count, number} Сӑнӳкерчӗк} other {{count, number} Сӑнӳкерчӗксем}}",
"photos_from_previous_years": "Иртнӗ ҫулсенчи сӑнӳкерчӗксем",
"place": "Тӗл",
"places": "Тӗлсем",
"play": "Выля",
"play_memories": "Асаилӳсем выля",
"search_your_photos": "Сӑнӳкерчӗксене шырӑр",
"select_photos": "Сӑнӳкерчӗксем суйлӑр",
"sharing": "Пайлани",
"sharing_enter_password": "Ку питне курма пароль кӗртӗр.",
"user_usage_stats": "Шута ҫырни усӑ курмалли статистика",
"user_usage_stats_description": "Шута ҫырни усӑ курмалли статистикӑна пӑхасси"
}

View File

@@ -7,7 +7,7 @@
"actions": "Handlinger",
"active": "Aktive",
"activity": "Aktivitet",
"activity_changed": "Aktivitet er {aktiveret, valg, sand {aktiveret} andet {deaktiveret}}",
"activity_changed": "Aktivitet er {aktiveret, vælg, sandt {aktiveret} andet {deaktiveret}}",
"add": "Tilføj",
"add_a_description": "Tilføj en beskrivelse",
"add_a_location": "Tilføj en placering",
@@ -51,6 +51,7 @@
"create_job": "Opret job",
"cron_expression": "Cron formel",
"cron_expression_description": "Indstil skannings intervallet i cron format. For mere information se: <link>Crontab Guru</link>",
"cron_expression_presets": "cron predefinerede indstillinger",
"disable_login": "Deaktiver login",
"duplicate_detection_job_description": "Kør maskinlæring på mediefiler for at opdage lignende billeder. Er afhængig af Smart Søgning",
"exclusion_pattern_description": "Ekskluderingsmønstre lader dig ignorere filer og mapper, når du scanner dit bibliotek. Dette er nyttigt, hvis du har mapper, der indeholder filer, du ikke vil importere, såsom RAW-filer.",
@@ -69,14 +70,18 @@
"image_prefer_wide_gamut": "Foretrækker bred farveskala",
"image_prefer_wide_gamut_setting_description": "Brug Display P3 til miniaturebilleder. Dette bevarer billeder med brede farveskalaers dynamik bedre, men billeder kan komme til at se anderledes ud på gamle enheder med en gammel browserversion. sRGB-billeder bliver beholdt som sRGB for at undgå farveskift.",
"image_preview_description": "Mellemstørrelse billede med fjernet metadata, der bruges, når du ser en enkelt mediefil og til machine learning",
"image_preview_quality_description": "Kvalitet af forhåndsvisning fra 1-100. Højere er bedre, men producerer større filer og kan reducere apprespons. Valg af en lav værdi kan påvirke kvaliteten af machine learning.",
"image_preview_quality_description": "Kvalitet af forhåndsvisning fra 1-100. Højere er bedre, men producerer større filer og kan reducere apprespons. Valg af en lav værdi kan påvirke kvaliteten af maskin læring.",
"image_preview_title": "Indstillinger for forhåndsvisning",
"image_quality": "Kvalitet",
"image_resolution": "Opløsning",
"image_resolution_description": "højere opløsning indeholder flere detaljer, men tager længere tid at processerer, giver større filer og sænker svartiderne i applikationen.",
"image_settings": "Billedindstillinger",
"image_settings_description": "Administrer kvaliteten og opløsningen af genererede billeder",
"image_thumbnail_description": "Små miniaturer uden metadata, bruges når der ses samlinger eller den primære tidslinie",
"image_thumbnail_quality_description": "Miniaturer kvaliteten indstilles fra 1 til 100. Nu højre, nu bedre kvalitet. Men giver større filer og påvirker programmets svartider.",
"image_thumbnail_title": "Thumbnail-indstillinger",
"job_concurrency": "{job} samtidighed",
"job_created": "opgaven er skabt",
"job_not_concurrency_safe": "Denne opgave er ikke sikker at køre samtidigt med andre.",
"job_settings": "Jobindstillinger",
"job_settings_description": "Administrér samtidige opgaver",
@@ -126,7 +131,7 @@
"machine_learning_smart_search_description": "Søg semantisk efter billeder ved hjælp af CLIP-indlejringer",
"machine_learning_smart_search_enabled": "Aktiver smart søgning",
"machine_learning_smart_search_enabled_description": "Hvis deaktiveret, vil billeder ikke blive kodet til smart søgning.",
"machine_learning_url_description": "URL på maskinlæringsserveren",
"machine_learning_url_description": "URL på maskinlæringsserveren. Hvis der er angivet mere end én URL, hver server vil blive forsøgt en ad gangen, indtil en reagerer med succes, i rækkefølge fra første til sidste.",
"manage_concurrency": "Administrer antallet af samtidige opgaver",
"manage_log_settings": "Administrer logindstillinger",
"map_dark_style": "Mørk tema",
@@ -214,9 +219,12 @@
"reset_settings_to_default": "Nulstil indstillingerne til standard",
"reset_settings_to_recent_saved": "Nulstil indstillinger til de senest gemte indstillinger",
"scanning_library": "Scanner bibliotek",
"search_jobs": "søg opgaver ..",
"send_welcome_email": "Send velkomst-email",
"server_external_domain_settings": "Eksternt domæne",
"server_external_domain_settings_description": "Domæne til offentligt delte links, inklusiv http(s)://",
"server_public_users": "Offentlige brugere",
"server_public_users_description": "Alle brugere (navn og e-mail) vises, når en bruger tilføjes til delte album. Når den er deaktiveret, vil brugerlisten kun være tilgængelig for administratorbrugere.",
"server_settings": "Serverindstillinger",
"server_settings_description": "Administrér serverindstillinger",
"server_welcome_message": "Velkomstbesked",
@@ -233,7 +241,7 @@
"storage_template_migration": "Lagringsskabelonmigration",
"storage_template_migration_description": "Anvend den nuværende <link>{template}</link> på tidligere uploadede mediefiler",
"storage_template_migration_info": "Skabelonændringer vil kun gælde for nye mediefiler. For at anvende skabelonen retroaktivt på tidligere uploadede mediefiler skal du køre <link>{job}</link>.",
"storage_template_migration_job": "Lager Skabelon Migrationsjob",
"storage_template_migration_job": "Lager Skabelon Migreringsjob",
"storage_template_more_details": "For flere detaljer om denne funktion, referer til <template-link>Lager Skabelonen</template-link> og dens <implications-link>implikationer</implications-link>",
"storage_template_onboarding_description": "Når denne funktion er aktiveret, vil den automatisk organisere filer baseret på en brugerdefineret skabelon. På grund af stabilitetsproblemer er funktionen som standard slået fra. For mere information, se <link>dokumentation</link>.",
"storage_template_path_length": "Anslået sti-længde begrænsning <b>{length, number}</b>/{limit, number}",
@@ -241,6 +249,17 @@
"storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil",
"storage_template_user_label": "<code>{label}</code> er brugerens Lagringsmærkat",
"system_settings": "Systemindstillinger",
"tag_cleanup_job": "\"Tag\" cleanup",
"template_email_available_tags": "Du kan bruge følgende variabler i din skabelon: {tags}",
"template_email_if_empty": "Hvis skabelonen er tom, vil standard-e-mailen blive brugt.",
"template_email_invite_album": "Inviterings albumskabelon",
"template_email_preview": "Forhåndsvisning",
"template_email_settings": "Email skabeloner",
"template_email_settings_description": "Administrer tilpassede e-mailmeddelelsesskabeloner",
"template_email_update_album": "Opdater albumskabelon",
"template_email_welcome": "Velkomst e-mail skabelon",
"template_settings": "Notifikations skabeloner",
"template_settings_description": "Administrer tilpassede skabeloner for notifikationer.",
"theme_custom_css_settings": "Brugerdefineret CSS",
"theme_custom_css_settings_description": "Cascading Style Sheets tillader at give Immich et brugerdefineret look.",
"theme_settings": "Temaindstillinger",
@@ -270,6 +289,8 @@
"transcoding_constant_rate_factor": "Konstant ratefaktor (-crf)",
"transcoding_constant_rate_factor_description": "Videokvalitetsniveau. Typiske værdier er 23 for H.264, 28 for HEVC, 31 for VP9 og 35 for AV1. Lavere er bedre, men producerer større filer.",
"transcoding_disabled_description": "Lad være med at transkode nogen videoer, kan ødelægge playback hos nogle clients",
"transcoding_encoding_options": "Kodningsmuligheder",
"transcoding_encoding_options_description": "Indstil codecs, opløsning, kvalitet og andre muligheder for de kodede videoer",
"transcoding_hardware_acceleration": "Hardwareacceleration",
"transcoding_hardware_acceleration_description": "Eksperimentel; meget hurtigere, men vil have lavere kvalitet ved samme bitrate",
"transcoding_hardware_decoding": "Hardware-afkodning",
@@ -282,15 +303,17 @@
"transcoding_max_keyframe_interval": "Maksimal keyframe-interval",
"transcoding_max_keyframe_interval_description": "Sætter den maksimale frameafstand mellem keyframes. Lavere værdier forringer kompressionseffektiviteten, men forbedrer søgetider og kan forbedre kvaliteten i scener med hurtig bevægelse. 0 sætter denne værdi automatisk.",
"transcoding_optimal_description": "Videoer højere end målopløsningen eller ikke i et godkendt format",
"transcoding_policy": "Omkodningspolitik",
"transcoding_policy_description": "Indstil, hvornår en video skal omkodes",
"transcoding_preferred_hardware_device": "Foretrukne hardwareenhed",
"transcoding_preferred_hardware_device_description": "Gælder kun VAAPI og QSV. Sætter dri node'n som bruges til hardware-transkodning.",
"transcoding_preset_preset": "Forudindstilling (-preset)",
"transcoding_preset_preset_description": "Kompressionshastighed. Langsommere forudindstillinger producerer mindre filer, og øger kvalitet når der gås efter en specifik bitrate. VP9 ignorerer hastigheder hurtigere end \"hurtigere\".",
"transcoding_reference_frames": "Referenceframes",
"transcoding_reference_frames": "Referencerammer",
"transcoding_reference_frames_description": "Antallet af frames, der skal refereres til, når en given frame komprimeres. Højere værdier forbedrer kompressionseffektiviteten, men gør indkodning langsommere. 0 sætter denne værdi automatisk.",
"transcoding_required_description": "Kun videoer ikke i et godkendt format",
"transcoding_settings": "Videotranskodningsindstillinger",
"transcoding_settings_description": "Administrér videofilernes opløsning- og indkodningsinformation",
"transcoding_settings_description": "Administrer, hvilke videoer der skal omkodes, og hvordan de behandles",
"transcoding_target_resolution": "Målopløsning",
"transcoding_target_resolution_description": "Højere opløsninger kan bevare flere detaljer, men tager længere tid at indkode, har større filstørrelser og kan gøre appen mere sløv.",
"transcoding_temporal_aq": "Tidsmæssig AQ",
@@ -310,7 +333,7 @@
"trash_number_of_days_description": "Antal dage aktiver i skraldespanden skal beholdes inden de fjernes permanent",
"trash_settings": "Skraldeindstillinger",
"trash_settings_description": "Administrér skraldeindstillinger",
"untracked_files": "Utrackede filer",
"untracked_files": "Usporede filer",
"untracked_files_description": "Applikationen holder ikke styr på disse filer. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller være efterladt på grund af en fejl",
"user_cleanup_job": "Bruger-oprydning",
"user_delete_delay": "<b>{user}</b>'s konto og mediefiler vil blive planlagt til permanent sletning om {delay, plural, one {# dag} other {# dage}}.",
@@ -328,8 +351,8 @@
"user_successfully_removed": "Bruger {email} er blevet fjernet med succes.",
"version_check_enabled_description": "Aktivér versionstjek",
"version_check_implications": "Funktionen til versionstjek er afhængig af periodisk kommunikation med github.com",
"version_check_settings": "Versiontjek",
"version_check_settings_description": "Aktiver/deaktier notifikation for den nye version",
"version_check_settings": "Versionstjek",
"version_check_settings_description": "Aktiver/deaktiverer notifikation for den nye version",
"video_conversion_job": "Transkod videoer",
"video_conversion_job_description": "Transkod videoer for bredere kompatibilitet med browsere og enheder"
},
@@ -338,8 +361,8 @@
"administration": "Administration",
"advanced": "Avanceret",
"age_months": "Alder {months, plural, one {# month} other {# months}}",
"age_year_months": "Alder 1 år, {months, plural, one {# måned} other {# måneder}}",
"age_years": "{years, plural, other {Alder #}}",
"age_year_months": "Alder 1 år, {måneder, flertal, en {# måned} flere {# months}}",
"age_years": "{år, år, andre {Alder #}}",
"album_added": "Album tilføjet",
"album_added_notification_setting_description": "Modtag en emailnotifikation når du bliver tilføjet til en delt album",
"album_cover_updated": "Albumcover opdateret",
@@ -379,27 +402,46 @@
"archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
"archive_size": "Arkiv størelse",
"archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)",
"archived_count": "{antal, flertal, andet {Arkiveret #}}",
"are_these_the_same_person": "Er disse den samme person?",
"are_you_sure_to_do_this": "Er du sikker på, at du vil gøre det her?",
"asset_added_to_album": "Tilføjet til album",
"asset_adding_to_album": "Tilføjer til album...",
"asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret",
"asset_filename_is_offline": "Mediefil {filename} er offline",
"asset_has_unassigned_faces": "Aktivet har ikke-tildelte ansigter",
"asset_hashing": "Hashing...",
"asset_offline": "Mediefil offline",
"asset_offline_description": "Denne eksterne mediefil kan ikke længere findes på drevet. Kontakt venligst din Immich-administrator for hjælp.",
"asset_skipped": "Sprunget over",
"asset_skipped_in_trash": "I skraldespand",
"asset_uploaded": "Uploaded",
"asset_uploading": "Uploader...",
"assets": "elementer",
"assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}",
"assets_added_to_album_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til albummet",
"assets_added_to_name_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til {hasName, select, true {<b>{name}</b>} other {nyt album}}",
"assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}",
"assets_moved_to_trash_count": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til papirkurven",
"assets_permanently_deleted_count": "Slettet permanent {count, plural, one {# mediefil} other {# mediefiler}}",
"assets_removed_count": "Fjernede {count, plural, one {# mediefil} other {# mediefiler}}",
"assets_restore_confirmation": "Er du sikker på, at du vil gendanne alle dine aktiver i papirkurven? Du kan ikke fortryde denne handling! Bemærk, at offline mediefiler ikke kan gendannes på denne måde.",
"assets_restored_count": "Gendannet {count, plural, one {# mediefil} other {# mediefiler}}",
"assets_trashed_count": "Smidt {count, plural, one {# mediefil} other {# mediefiler}} i papirkurven",
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} er allerede en del af albummet",
"authorized_devices": "Tilladte enheder",
"back": "Tilbage",
"back_close_deselect": "Tilbage, luk eller fravælg",
"backward": "Baglæns",
"birthdate_saved": "Fødselsdatoen blev gemt",
"birthdate_set_description": "Fødselsdato bruges til at beregne alderen på denne person på tidspunktet for et billede.",
"blurred_background": "Sløret baggrund",
"bugs_and_feature_requests": "Fejl & forbedringsønsker",
"build": "Byg",
"build_image": "Byggefil",
"bulk_delete_duplicates_confirmation": "Er du sikker på, at du vil slette alle {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde den største fil i hver gruppe og slette alle dubletter. Denne handling kan ikke fortrydes!",
"bulk_keep_duplicates_confirmation": "Er du sikker på, at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil løse alle dubletgrupper uden at slette noget.",
"bulk_trash_duplicates_confirmation": "Er du sikker på, at du vil masseslette {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde det største aktiv i hver gruppe og smide alle andre dubletter.",
"buy": "Køb Immich",
"camera": "Kamera",
"camera_brand": "Kameramærke",
@@ -440,6 +482,7 @@
"confirm": "Bekræft",
"confirm_admin_password": "Bekræft administratoradgangskode",
"confirm_delete_shared_link": "Er du sikker på, at du vil slette dette delte link?",
"confirm_keep_this_delete_others": "Alle andre aktiver i stakken vil blive slettet undtagen dette aktiv. Er du sikker på, at du vil fortsætte?",
"confirm_password": "Bekræft adgangskode",
"contain": "Inddæm",
"context": "Kontekst",
@@ -463,7 +506,10 @@
"create_link_to_share": "Opret link for at dele",
"create_link_to_share_description": "Lad alle med linket se de(t) valgte billede(r)",
"create_new_person": "Opret ny person",
"create_new_person_hint": "Tildel valgte aktiver til en ny person",
"create_new_user": "Opret ny bruger",
"create_tag": "Opret tag",
"create_tag_description": "Opret et nyt tag. For indlejrede tags skal du indtaste den fulde sti til tagget inklusive skråstreger.",
"create_user": "Opret bruger",
"created": "Oprettet",
"current_device": "Nuværende enhed",
@@ -473,19 +519,30 @@
"date_after": "Dato efter",
"date_and_time": "Dato og klokkeslæt",
"date_before": "Dato før",
"date_of_birth_saved": "Fødselsdatoen blev gemt",
"date_range": "Datointerval",
"day": "Dag",
"default_locale": "Standardlokale",
"deduplicate_all": "Dedupliker alle",
"deduplication_criteria_1": "Billedstørrelse i bytes",
"deduplication_criteria_2": "Antal EXIF-data",
"deduplication_info": "Deduplikerings info",
"deduplication_info_description": "For automatisk at forudvælge emner og fjerne dubletter i bulk ser vi på:",
"default_locale": "Standardlokalitet",
"default_locale_description": "Formatér datoer og tal",
"delete": "Slet",
"delete_album": "Slet album",
"delete_api_key_prompt": "Er du sikker på, at du vil slette denne API-nøgle?",
"delete_duplicates_confirmation": "Er du sikker på, at du vil slette disse dubletter permanent?",
"delete_key": "Slet nøgle",
"delete_library": "Slet bibliotek",
"delete_link": "Slet link",
"delete_others": "Slet andre",
"delete_shared_link": "Slet delt link",
"delete_tag": "Slet tag",
"delete_tag_confirmation_prompt": "Er du sikker på, at du vil slette {tagName}-tagget?",
"delete_user": "Slet bruger",
"deleted_shared_link": "Slettede delt link",
"deletes_missing_assets": "Sletter aktiver, der mangler fra disken",
"description": "Beskrivelse",
"details": "DETALJER",
"direction": "Retning",
@@ -500,12 +557,18 @@
"display_original_photos": "Vis originale billeder",
"display_original_photos_setting_description": "Foretræk at vise det originale billede frem for miniaturebilleder når den originale fil er web-kompatibelt. Dette kan gøre billedvisning langsommere.",
"do_not_show_again": "Vis ikke denne besked igen",
"documentation": "Dokumentation",
"done": "Færdig",
"download": "Hent",
"download_include_embedded_motion_videos": "Indlejrede videoer",
"download_include_embedded_motion_videos_description": "Inkluder videoer indlejret i levende billeder som en separat fil",
"download_settings": "Download",
"download_settings_description": "Administrer indstillinger relateret til mediefil-downloads",
"downloading": "Downloader",
"downloading_asset_filename": "Downloader aktiv {filename}",
"drop_files_to_upload": "Slip filer hvor som helst for at uploade dem",
"duplicates": "Duplikater",
"duplicates_description": "Løs hver gruppe ved at angive, hvilke, hvis nogen, er dubletter",
"duration": "Varighed",
"edit": "Rediger",
"edit_album": "Redigér album",
@@ -521,15 +584,18 @@
"edit_location": "Rediger placering",
"edit_name": "Rediger navn",
"edit_people": "Redigér personer",
"edit_tag": "Rediger tag",
"edit_title": "Redigér titel",
"edit_user": "Redigér bruger",
"edited": "Redigeret",
"editor": "Redaktør",
"editor_close_without_save_prompt": "Ændringerne vil ikke blive gemt",
"editor_close_without_save_title": "Luk editor?",
"editor_crop_tool_h2_aspect_ratios": "Størrelsesforhold",
"editor_crop_tool_h2_rotation": "Rotation",
"email": "E-mail",
"empty_trash": "Tøm papirkurv",
"empty_trash_confirmation": "Er du sikker på, at du vil tømme papirkurven? Dette vil fjerne alle aktiver i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!",
"enable": "Aktivér",
"enabled": "Aktiveret",
"end_date": "Slutdato",
@@ -539,6 +605,14 @@
"errors": {
"cannot_navigate_next_asset": "Kan ikke navigere til næste mediefil",
"cannot_navigate_previous_asset": "Kan ikke navigere til forrige mediefil",
"cant_apply_changes": "Ændringerne kan ikke anvendes",
"cant_change_activity": "Kan ikke {enabled, select, true {disable} other {enable}} aktivitet",
"cant_change_asset_favorite": "Kan ikke ændre favorit til aktiv",
"cant_change_metadata_assets_count": "Kan ikke ændre metadata for {count, plural, one {# asset} other {# assets}}",
"cant_get_faces": "Kan ikke hente ansigter",
"cant_get_number_of_comments": "Kan ikke få antallet af kommentarer",
"cant_search_people": "Kan ikke søge efter folk",
"cant_search_places": "Kan ikke søge efter steder",
"cleared_jobs": "Ryddede opgaver for: {job}",
"error_adding_assets_to_album": "Fejl i tilføjelse af mediefiler til album",
"error_adding_users_to_album": "Fejl i tilføjelse af brugere til album",
@@ -546,15 +620,20 @@
"error_downloading": "Fejl i download af {filename}",
"error_hiding_buy_button": "Fejl i skjulning af køb-knap",
"error_removing_assets_from_album": "Fejl i fjernelse af mediefiler fra album. Tjek konsol for flere detaljer",
"error_selecting_all_assets": "Fejl ved valg af alle aktiver",
"exclusion_pattern_already_exists": "Denne udelukkelsesmønster findes allerede.",
"failed_job_command": "Kommando {command} slog fejl for opgave: {job}",
"failed_to_create_album": "Oprettelse af album mislykkedes",
"failed_to_create_shared_link": "Oprettelse af delt link mislykkedes",
"failed_to_edit_shared_link": "Redigering af delt link mislykkedes",
"failed_to_get_people": "Det lykkedes ikke at hente folk",
"failed_to_keep_this_delete_others": "Kunne ikke beholde dette aktiv og slette de andre aktiver",
"failed_to_load_asset": "Indlæsning af mediefil mislykkedes",
"failed_to_load_assets": "Indlæsning af mediefiler mislykkedes",
"failed_to_load_people": "Indlæsning af personer mislykkedes",
"failed_to_remove_product_key": "Fjernelse af produktnøgle mislykkedes",
"failed_to_stack_assets": "Det lykkedes ikke at stable aktiver",
"failed_to_unstack_assets": "Det lykkedes ikke at fjerne stablen af aktiver",
"import_path_already_exists": "Denne importsti findes allerede.",
"incorrect_email_or_password": "Forkert email eller kodeord",
"paths_validation_failed": "{paths, plural, one {# sti} other {# stier}} slog fejl ved validering",
@@ -562,16 +641,25 @@
"quota_higher_than_disk_size": "Du har sat en kvote der er større end disken",
"repair_unable_to_check_items": "Kunne ikke tjekke {count, select, one {element} other {elementer}}",
"unable_to_add_album_users": "Ikke i stand til at tilføje brugere til album",
"unable_to_add_assets_to_shared_link": "Kan ikke tilføje aktiver til delt link",
"unable_to_add_comment": "Ikke i stand til at tilføje kommentar",
"unable_to_add_exclusion_pattern": "Kunne ikke tilføje udelukkelsesmønster",
"unable_to_add_import_path": "Kunne ikke tilføje importsti",
"unable_to_add_partners": "Ikke i stand til at tilføje partnere",
"unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv",
"unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter",
"unable_to_archive_unarchive": "Ude af stand til at {arkiveret, vælg, sand {arkiv} andet {arkiv}}",
"unable_to_change_album_user_role": "Ikke i stand til at ændre albumbrugerens rolle",
"unable_to_change_date": "Ikke i stand til at ændre dato",
"unable_to_change_favorite": "Kan ikke ændre favorit for aktiv",
"unable_to_change_location": "Ikke i stand til at ændre sted",
"unable_to_change_password": "Kunne ikke ændre adgangskode",
"unable_to_change_visibility": "Kan ikke ændre synligheden for {count, plural, one {# person} other {# personer}}",
"unable_to_complete_oauth_login": "Kan ikke fuldføre OAuth-login",
"unable_to_connect": "Kan ikke oprette forbindelse",
"unable_to_connect_to_server": "Kan ikke oprette forbindelse til serveren",
"unable_to_copy_to_clipboard": "Kan ikke kopiere til udklipsholder, sørg for at du tilgår siden gennem https",
"unable_to_create_admin_account": "",
"unable_to_create_admin_account": "Kan ikke oprette en administratorkonto",
"unable_to_create_api_key": "Kunne ikke oprette ny API-nøgle",
"unable_to_create_library": "Ikke i stand til at oprette bibliotek",
"unable_to_create_user": "Ikke i stand til at oprette bruger",
@@ -582,21 +670,31 @@
"unable_to_delete_import_path": "Kunne ikke slette importsti",
"unable_to_delete_shared_link": "Kunne ikke slette delt link",
"unable_to_delete_user": "Ikke i stand til at slette bruger",
"unable_to_download_files": "Kan ikke downloade filer",
"unable_to_edit_exclusion_pattern": "Kunne ikke redigere udelukkelsesmønster",
"unable_to_edit_import_path": "Kunne ikke redigere importsti",
"unable_to_empty_trash": "Ikke i stand til at tømme skraldespand",
"unable_to_enter_fullscreen": "Kan ikke aktivere fuldskærmstilstand",
"unable_to_exit_fullscreen": "Kan ikke forlade fuldskærmstilstand",
"unable_to_get_comments_number": "Kan ikke få antallet af kommentarer",
"unable_to_get_shared_link": "Kunne ikke hente delt link",
"unable_to_hide_person": "Ikke i stand til at gemme person",
"unable_to_link_motion_video": "Kan ikke linke bevægelsesvideo",
"unable_to_link_oauth_account": "Kunne ikke tilkoble OAuth-konto",
"unable_to_load_album": "Ikke i stand til hente album",
"unable_to_load_asset_activity": "Kunne ikke hente aktivitet for mediet",
"unable_to_load_items": "Ikke i stand til at hente ting",
"unable_to_load_liked_status": "Ikke i stand til hente synes-om-status",
"unable_to_log_out_all_devices": "Kan ikke logge af alle enheder",
"unable_to_log_out_device": "Enheden kunne ikke logges af",
"unable_to_login_with_oauth": "Kan ikke logge på med OAuth",
"unable_to_play_video": "Ikke i stand til at afspille video",
"unable_to_reassign_assets_existing_person": "Kan ikke gentildele aktiver til {navn, vælg, null {en eksisterende person} anden {{name}}}",
"unable_to_reassign_assets_new_person": "Kan ikke omfordele aktiver til en ny person",
"unable_to_refresh_user": "Ikke i stand til at genopfriske bruger",
"unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album",
"unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle",
"unable_to_remove_assets_from_shared_link": "Kan ikke fjerne aktiver fra delt link",
"unable_to_remove_deleted_assets": "Kunne ikke fjerne offlinefiler",
"unable_to_remove_library": "Ikke i stand til at fjerne bibliotek",
"unable_to_remove_partner": "Ikke i stand til at fjerne partner",
@@ -609,20 +707,26 @@
"unable_to_restore_user": "Ikke i stand til at genoprette bruger",
"unable_to_save_album": "Ikke i stand til at gemme album",
"unable_to_save_api_key": "Kunne ikke gemme API-nøgle",
"unable_to_save_date_of_birth": "Kan ikke gemme fødselsdatoen",
"unable_to_save_name": "Ikke i stand til at gemme navn",
"unable_to_save_profile": "Ikke i stand til at gemme profil",
"unable_to_save_settings": "Ikke i stand til at gemme indstillinger",
"unable_to_scan_libraries": "Ikke i stand til at skanne biblioteker",
"unable_to_scan_library": "Ikke i stand til at skanne bibliotek",
"unable_to_set_feature_photo": "Det var ikke muligt at indstille et fremhævet billede",
"unable_to_set_profile_picture": "Ikke i stand til at sætte profilbillede",
"unable_to_submit_job": "Ikke i stand til at indsende opgave",
"unable_to_trash_asset": "Kunne ikke slette medie",
"unable_to_unlink_account": "Ikke i stand til at frakoble konto",
"unable_to_unlink_motion_video": "Kunne ikke fjerne linket til bevægelsesvideo",
"unable_to_update_album_cover": "Albumomslaget kunne ikke opdateres",
"unable_to_update_album_info": "Albumoplysningerne kunne ikke opdateres",
"unable_to_update_library": "Ikke i stand til at opdatere bibliotek",
"unable_to_update_location": "Ikke i stand til at opdatere sted",
"unable_to_update_settings": "Ikke i stand til at opdatere indstillinger",
"unable_to_update_timeline_display_status": "Kunne ikke opdate status for tidslinjevisning",
"unable_to_update_user": "Ikke i stand til at opdatere bruger"
"unable_to_update_user": "Ikke i stand til at opdatere bruger",
"unable_to_upload_file": "Filen kunne ikke uploades"
},
"exif": "Exif",
"exit_slideshow": "Forlad slideshow",
@@ -631,11 +735,14 @@
"expired": "Udløbet",
"expires_date": "Udløber {date}",
"explore": "Udforsk",
"explorer": "Udforske",
"export": "Eksportér",
"export_as_json": "Eksportér som JSON",
"extension": "Udvidelse",
"external": "Ekstern",
"external_libraries": "Eksterne biblioteker",
"face_unassigned": "Ikke tildelt",
"failed_to_load_assets": "Kunne ikke indlæse aktiver",
"favorite": "Favorit",
"favorite_or_unfavorite_photo": "Tilføj eller fjern fra yndlingsbilleder",
"favorites": "Favoritter",
@@ -650,14 +757,18 @@
"find_them_fast": "Find dem hurtigt med søgning via navn",
"fix_incorrect_match": "Fix forkert match",
"folders": "Mapper",
"folders_feature_description": "Gennemse mappevisningen efter fotos og videoer på filsystemet",
"forward": "Fremad",
"general": "Generel",
"get_help": "Få hjælp",
"getting_started": "Kom godt i gang",
"go_back": "Gå tilbage",
"go_to_folder": "Gå til mappe",
"go_to_search": "Gå til søgning",
"group_albums_by": "Gruppér albummer efter...",
"group_no": "Ingen gruppering",
"group_owner": "Grupper efter ejer",
"group_year": "Grupper efter år",
"has_quota": "Har kvote",
"hi_user": "Hej {name} ({email})",
"hide_all_people": "Skjul alle personer",
@@ -669,10 +780,21 @@
"host": "Host",
"hour": "Time",
"image": "Billede",
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} taget den {date}",
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} taget med {person1} den {date}",
"image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} taget med {person1} og {person2} den {date}",
"image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} taget med {person1}, {person2}, og {person3} den {date}",
"image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taget med {person1}, {person2}, og {additionalCount, number} andre den {date}",
"image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} taget i {city}, {country} den {date}",
"image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} taget i {city}, {country} med {person1} den {date}",
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taget i {city}, {country} med {person1} og {person2} den {date}",
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taget i {city}, {country} med {person1}, {person2}, og {person3} den {date}",
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taget i {city}, {country} med {person1}, {person2}, og {additionalCount, number} andre den {date}",
"immich_logo": "Immich logo",
"immich_web_interface": "Immich webinterface",
"import_from_json": "Importér fra JSON",
"import_path": "Import-sti",
"in_albums": "I {count, plural, one {# album} other {# albummer}}",
"in_archive": "I arkiv",
"include_archived": "Inkluder arkiveret",
"include_shared_albums": "Inkludér delte albummer",
@@ -687,20 +809,26 @@
},
"invite_people": "Inviter personer",
"invite_to_album": "Inviter til album",
"items_count": "{count, plural, one {# genstand} other {# genstande}}",
"jobs": "Opgaver",
"keep": "Behold",
"keep_all": "Behold alle",
"keep_this_delete_others": "Behold dette, slet andre",
"kept_this_deleted_others": "Beholdt dette aktiv og slettede {count, plural, one {# aktiv} other {# aktiver}}",
"keyboard_shortcuts": "Tastaturgenveje",
"language": "Sprog",
"language_setting_description": "Vælg dit foretrukne sprog",
"last_seen": "Sidst set",
"latest_version": "Seneste version",
"latitude": "Breddegrad",
"leave": "Forlad",
"let_others_respond": "Lad andre svare",
"level": "Niveau",
"library": "Bibliotek",
"library_options": "Biblioteksindstillinger",
"light": "Lys",
"like_deleted": "Ligesom slettet",
"link_motion_video": "Link bevægelses video",
"link_options": "Link-indstillinger",
"link_to_oauth": "Link til OAuth",
"linked_oauth_account": "Tilsluttet OAuth-konto",
@@ -715,9 +843,11 @@
"login_has_been_disabled": "Login er blevet deaktiveret.",
"logout_all_device_confirmation": "Er du sikker på, at du vil logge ud af alle enheder?",
"logout_this_device_confirmation": "Er du sikker på, at du vil logge denne enhed ud?",
"longitude": "Længde",
"look": "Kig",
"loop_videos": "Gentag videoer",
"loop_videos_description": "Aktivér for at genafspille videoer automatisk i detaljeret visning.",
"main_branch_warning": "Du bruger en udviklingsversion; vi anbefaler kraftigt at bruge en udgivelsesversion!",
"make": "Producent",
"manage_shared_links": "Håndter delte links",
"manage_sharing_with_partners": "Administrér deling med partnere",
@@ -727,23 +857,29 @@
"manage_your_devices": "Administrér dine enheder der er logget ind",
"manage_your_oauth_connection": "Administrér din OAuth-tilslutning",
"map": "Kort",
"map_marker_for_images": "Kortmarkør for billeder taget i {city}, {country}",
"map_marker_with_image": "Kortmarkør med billede",
"map_settings": "Kortindstillinger",
"matches": "Parringer",
"media_type": "Medietype",
"memories": "Minder",
"memories_setting_description": "Administrér hvad du ser i dine minder",
"memory": "Hukommelse",
"memory_lane_title": "Minder {title}",
"menu": "Menu",
"merge": "Sammenflet",
"merge_people": "Sammenflet personer",
"merge_people_limit": "Du kan kun flette op til 5 ansigter ad gangen",
"merge_people_prompt": "Vil du slå disse mennesker sammen? Denne handling er uigenkaldelig.",
"merge_people_successfully": "Personer sammenflettet med succes",
"merged_people_count": "Slået sammen {count, plural, one {# person} other {# people}}",
"minimize": "Minimér",
"minute": "Minut",
"missing": "Mangler",
"model": "Model",
"month": "Måned",
"more": "Mere",
"moved_to_trash": "Flyttet til skrald",
"moved_to_trash": "Flyttet til skraldespand",
"my_albums": "Mine albummer",
"name": "Navn",
"name_or_nickname": "Navn eller kælenavn",
@@ -787,7 +923,10 @@
"offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
"ok": "Ok",
"oldest_first": "Ældste først",
"onboarding": "Onboarding",
"onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
"onboarding_theme_description": "Vælg et farvetema til din forekomst. Du kan ændre dette senere i dine indstillinger.",
"onboarding_welcome_description": "Lad os få din instans sat op med nogle almindelige indstillinger.",
"onboarding_welcome_user": "Velkommen, {user}",
"online": "Online",
"only_favorites": "Kun favoritter",
@@ -803,6 +942,7 @@
"other_variables": "Andre variable",
"owned": "Ejet",
"owner": "Ejer",
"partner": "Partner",
"partner_can_access": "{partner} kan tilgå",
"partner_can_access_assets": "Alle dine billeder og videoer, bortset fra dem i Arkiveret og Slettet",
"partner_can_access_location": "Stedet, hvor dine billeder blev taget",
@@ -824,13 +964,21 @@
"paused": "Sat på pause",
"pending": "Afventer",
"people": "Personer",
"people_edits_count": "Redigeret {count, plural, one {# person} other {# people}}",
"people_feature_description": "Gennemse billeder og videoer grupperet efter personer",
"people_sidebar_description": "Vis et link til Personer i sidepanelet",
"permanent_deletion_warning": "Advarsel om permanent sletning",
"permanent_deletion_warning_setting_description": "Vis en advarsel, når medier slettes permanent",
"permanently_delete": "Slet permanent",
"permanently_delete_assets_count": "Slet permanent {count, plural, one {asset} other {assets}}",
"permanently_delete_assets_prompt": "Er du sikker på, at du permanent vil slette {count, plural, one {dette aktiv?} other {disse <b>#</b> aktiver?}} Dette vil også fjerne {count, plural, one {det fra dets} other {dem fra deres}} album(er).",
"permanently_deleted_asset": "Permanent slettet medie",
"permanently_deleted_assets_count": "Slettet permanent {count, plural, one {# aktiv} other {# aktiver}}",
"person": "Person",
"person_hidden": "{name}{hidden, select, true { (skjult)} other {}}",
"photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller også har du ikke nogen bruger at dele med.",
"photos": "Billeder",
"photos_and_videos": "Billeder og videoer",
"photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}",
"photos_from_previous_years": "Billeder fra tidligere år",
"pick_a_location": "Vælg et sted",
@@ -847,78 +995,164 @@
"previous_memory": "Forrige minde",
"previous_or_next_photo": "Forrige eller næste billede",
"primary": "Primære",
"profile_picture_set": "Profilbillede sat.",
"privacy": "Privatliv",
"profile_image_of_user": "Profilbillede af {user}",
"profile_picture_set": "Profilbillede indstillet.",
"public_album": "Offentligt album",
"public_share": "Offentlig deling",
"purchase_account_info": "Supporter",
"purchase_activated_subtitle": "Tak fordi du støtter Immich og open source-software",
"purchase_activated_time": "Aktiveret den {date, date}",
"purchase_activated_title": "Din nøgle er blevet aktiveret",
"purchase_button_activate": "Aktiver",
"purchase_button_buy": "Køb",
"purchase_button_buy_immich": "Køb Immich",
"purchase_button_never_show_again": "Vis aldrig igen",
"purchase_button_reminder": "Påmind mig om 30 dage",
"purchase_button_remove_key": "Fjern nøgle",
"purchase_button_select": "Vælg",
"purchase_failed_activation": "Kunne ikke aktiveres! Tjek venligst din e-mail for den korrekte produktnøgle!",
"purchase_individual_description_1": "For en enkeltperson",
"purchase_individual_description_2": "Supporterstatus",
"purchase_individual_title": "Individuel",
"purchase_input_suggestion": "Har du en produktnøgle? Indtast nøglen nedenfor",
"purchase_license_subtitle": "Køb Immich for at understøtte den fortsatte udvikling af tjenesten",
"purchase_lifetime_description": "Livsvarigt køb",
"purchase_option_title": "KØBEMULIGHEDER",
"purchase_panel_info_1": "At bygge Immich tager meget tid og kræfter, og vi har fuldtidsingeniører, der arbejder på det for at gøre det så godt, som vi overhovedet kan. Vores mission er, at open source-software og etisk forretningspraksis bliver en bæredygtig indtægtskilde for udviklere og at skabe et privatlivsrespekterende økosystem med reelle alternativer til udnyttende cloud-tjenester.",
"purchase_panel_info_2": "Da vi er forpligtet til ikke at tilføje betalingsvægge, vil dette køb ikke give dig yderligere funktioner i Immich. Vi er afhængige af, at brugere som dig støtter Immichs løbende udvikling.",
"purchase_panel_title": "Støt projektet",
"purchase_per_server": "Per server",
"purchase_per_user": "Per bruger",
"purchase_remove_product_key": "Fjern produktnøgle",
"purchase_remove_product_key_prompt": "Er du sikker på, at du vil fjerne produktnøglen?",
"purchase_remove_server_product_key": "Fjern serverens produktnøgle",
"purchase_remove_server_product_key_prompt": "Er du sikker på, at du vil fjerne serverproduktnøglen?",
"purchase_server_description_1": "For hele serveren",
"purchase_server_description_2": "Supporter status",
"purchase_server_title": "Server",
"purchase_settings_server_activated": "Serverens produktnøgle administreres af administratoren",
"rating": "Stjernebedømmelse",
"rating_clear": "Nulstil vurdering",
"rating_count": "{count, plural, one {# stjerne} other {# stjerner}}",
"rating_description": "Vis EXIF-klassificeringen i infopanelet",
"reaction_options": "Reaktionsindstillinger",
"read_changelog": "Læs ændringslog",
"reassign": "Gentildel",
"reassigned_assets_to_existing_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til {name, select, null {en eksisterende person} other {{navne}}}",
"reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person",
"reassing_hint": "Tildel valgte aktiver til en eksisterende person",
"recent": "For nylig",
"recent-albums": "Seneste albums",
"recent_searches": "Seneste søgninger",
"refresh": "Opdatér",
"refresh_encoded_videos": "Opdater kodede videoer",
"refresh_faces": "Opdater ansigter",
"refresh_metadata": "Opdater metadata",
"refresh_thumbnails": "Opdater forhåndsvisning",
"refreshed": "Opdateret",
"refreshes_every_file": "Opdaterer alle filer",
"refreshing_encoded_video": "Opdaterer kodet video",
"refreshing_faces": "Opdaterer ansigter",
"refreshing_metadata": "Opdaterer metadata",
"regenerating_thumbnails": "Regenererer forhåndsvisninger",
"remove": "Fjern",
"remove_assets_album_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra albummet?",
"remove_assets_shared_link_confirmation": "Er du sikker på, at du vil fjerne {count, plural, one {# aktiv} other {# aktiver}} fra dette delte link?",
"remove_assets_title": "Fjern aktiver?",
"remove_custom_date_range": "Fjern tilpasset datointerval",
"remove_deleted_assets": "Fjern fra offlinefiler",
"remove_from_album": "Fjern fra album",
"remove_from_favorites": "Fjern fra favoritter",
"remove_from_shared_link": "Fjern fra delt link",
"remove_url": "Fjern URL",
"remove_user": "Fjern bruger",
"removed_api_key": "Fjernede API-nøgle: {name}",
"removed_from_archive": "Fjernet fra arkiv",
"removed_from_favorites": "Fjernet fra favoritter",
"removed_from_favorites_count": "{count, plural, other {Fjernede #}} fra favoritter",
"removed_tagged_assets": "Fjernede tag fra {count, plural, one {# aktiv} other {# aktiver}}",
"rename": "Omdøb",
"repair": "Reparér",
"repair_no_results_message": "Utrackede og manglende filer vil blive vist her",
"replace_with_upload": "Erstat med upload",
"repository": "Depot",
"require_password": "Kræv adgangskode",
"require_user_to_change_password_on_first_login": "Kræv at bruger skifter adgangskode ved første login",
"reset": "Nulstil",
"reset_password": "Nulstil adgangskode",
"reset_people_visibility": "Nulstil personsynlighed",
"reset_to_default": "Nulstil til standard",
"resolve_duplicates": "Løs dubletter",
"resolved_all_duplicates": "Løste alle dubletter",
"restore": "Gendan",
"restore_all": "Gendan alle",
"restore_user": "Gendan bruger",
"restored_asset": "Gendannet aktiv",
"resume": "Genoptag",
"retry_upload": "Forsøg upload igen",
"review_duplicates": "Review duplikater",
"review_duplicates": "Gennemgå dubletter",
"role": "Rolle",
"role_editor": "Editor",
"role_viewer": "Seer",
"save": "Gem",
"saved_api_key": "Gemt API-nøgle",
"saved_profile": "Gemte profil",
"saved_settings": "Gemte indstillinger",
"say_something": "Skriv noget",
"scan_all_libraries": "Skan gennem alle biblioteker",
"scan_library": "Skan",
"scan_settings": "Skanningsindstillinger",
"scanning_for_album": "Skanner efter albummer...",
"search": "Søg",
"search_albums": "Søg i albummer",
"search_by_context": "Søg efter kontekst",
"search_by_filename": "Søg efter filnavn eller filtypenavn",
"search_by_filename_example": "dvs. IMG_1234.JPG eller PNG",
"search_camera_make": "Søg efter kameraproducent...",
"search_camera_model": "Søg efter kameramodel...",
"search_city": "Søg efter by...",
"search_country": "Søg efter land...",
"search_for_existing_person": "Søg efter eksisterende person",
"search_no_people": "Ingen personer",
"search_no_people_named": "Ingen personer med navnet \"{name}\"",
"search_options": "Søgemuligheder",
"search_people": "Søg i personer",
"search_places": "Søg i steder",
"search_settings": "søgeindstillinger",
"search_state": "Søg efter stat...",
"search_tags": "Søg tags...",
"search_timezone": "Søg i tidszone...",
"search_type": "Søg efter type",
"search_your_photos": "Søg i dine billeder",
"searching_locales": "Søger lokaler...",
"second": "Sekund",
"see_all_people": "Se alle personer",
"select_album_cover": "Vælg albumcover",
"select_all": "Vælg alle",
"select_all_duplicates": "Vælg alle dubletter",
"select_avatar_color": "Vælg avatarfarve",
"select_face": "Vælg ansigt",
"select_featured_photo": "Vælg forsidebillede",
"select_from_computer": "Vælg fra computer",
"select_keep_all": "Vælg gem alle",
"select_library_owner": "Vælg biblioteksejer",
"select_new_face": "Vælg nyt ansigt",
"select_photos": "Vælg billeder",
"select_trash_all": "Vælg smid alle ud",
"selected": "Valgt",
"selected_count": "{count, plural, other {# valgt}}",
"send_message": "Send besked",
"send_welcome_email": "Send velkomstemail",
"server_offline": "Server Offline",
"server_online": "Server Online",
"server_stats": "Serverstatus",
"set": "Sæt",
"set_as_album_cover": "Sæt som albumcover",
"set_as_profile_picture": "Sæt som profilbillede",
"set_date_of_birth": "Sæt fødselsdato",
"server_version": "Server Version",
"set": "Indstil",
"set_as_album_cover": "Indstil som albumcover",
"set_as_featured_photo": "Indstil som fremhævet billede",
"set_as_profile_picture": "Indstil som profilbillede",
"set_date_of_birth": "Indstil fødselsdato",
"set_profile_picture": "Indstil profilbillede",
"set_slideshow_to_fullscreen": "Sæt diasshow til fuldskærmsvisning",
"settings": "Indstillinger",
@@ -926,14 +1160,20 @@
"share": "Del",
"shared": "Delt",
"shared_by": "Delt af",
"shared_by_user": "Delt af {user}",
"shared_by_you": "Delt af dig",
"shared_from_partner": "Billeder fra {partner}",
"shared_link_options": "Muligheder for delt link",
"shared_links": "Delte links",
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}",
"shared_with_partner": "Delt med {partner}",
"sharing": "Delte",
"sharing_enter_password": "Indtast venligst adgangskoden for at se denne side.",
"sharing_sidebar_description": "Vis et link til deling i sidemenuen",
"shift_to_permanent_delete": "tryk på ⇧ for at slette aktiv permanent",
"show_album_options": "Vis albumindstillinger",
"show_albums": "Vis albummer",
"show_all_people": "Vis alle personer",
"show_and_hide_people": "Vis & gem personer",
"show_file_location": "Vis filplacering",
"show_gallery": "Vis galleri",
@@ -947,17 +1187,35 @@
"show_person_options": "Vis personindstillinger",
"show_progress_bar": "Vis statuslinje",
"show_search_options": "Vis søgeindstillinger",
"show_slideshow_transition": "Vis overgang til diasshow",
"show_supporter_badge": "Supportermærke",
"show_supporter_badge_description": "Vis et supportermærke",
"shuffle": "Bland",
"sidebar": "Sidebjælke",
"sidebar_display_description": "Vis et link til visningen i sidebjælken",
"sign_out": "Log af",
"sign_up": "Tilmeld",
"size": "Størrelse",
"skip_to_content": "Spring frem til indhold",
"skip_to_folders": "Spring til mapper",
"skip_to_tags": "Spring til tags",
"slideshow": "Diasshow",
"slideshow_settings": "Diasshowindstillinger",
"sort_albums_by": "Sortér albummer efter...",
"sort_created": "Dato oprettet",
"sort_items": "Antal genstande",
"sort_modified": "Ændret dato",
"sort_oldest": "Ældste foto",
"sort_people_by_similarity": "Sorter folk efter lighed",
"sort_recent": "Seneste foto",
"sort_title": "Titel",
"source": "Kilde",
"stack": "Stak",
"stack_selected_photos": "Stabl valgte billeder",
"stacktrace": "Stakspor",
"stack_duplicates": "Stak dubletter",
"stack_select_one_photo": "Vælg ét hovedbillede til stakken",
"stack_selected_photos": "Stak valgte billeder",
"stacked_assets_count": "Stablet {count, plural, one {# aktiv} other {# aktiver}}",
"stacktrace": "Stacktrace",
"start": "Start",
"start_date": "Startdato",
"state": "Stat",
@@ -972,44 +1230,85 @@
"submit": "Indsend",
"suggestions": "Anbefalinger",
"sunrise_on_the_beach": "Solopgang på stranden",
"support": "Support",
"support_and_feedback": "Support & Feedback",
"support_third_party_description": "Din Immich-installation blev pakket af en tredjepart. Problemer, du oplever, kan være forårsaget af denne pakke, så rejs venligst problemer med dem i første omgang ved at bruge nedenstående links.",
"swap_merge_direction": "Byt retning for sammenfletning",
"sync": "Synkronisér",
"tag": "Tag",
"tag_assets": "Tag aktiver",
"tag_created": "Oprettet tag: {tag}",
"tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner",
"tag_not_found_question": "Kan du ikke finde et tag? <link>Opret et nyt tag.</link>",
"tag_updated": "Opdateret tag: {tag}",
"tagged_assets": "Tagget {count, plural, one {# aktiv} other {# aktiver}}",
"tags": "Tags",
"template": "Skabelon",
"theme": "Tema",
"theme_selection": "Temavalg",
"theme_selection_description": "Indstil automatisk temaet til lyst eller mørkt baseret på din browsers systempræference",
"they_will_be_merged_together": "De vil blive slået sammen",
"third_party_resources": "Tredjepartsressourcer",
"time_based_memories": "Tidsbaserede minder",
"timeline": "Tidslinje",
"timezone": "Tidszone",
"to_archive": "Arkivér",
"to_change_password": "Skift adgangskode",
"to_favorite": "Gør til favorit",
"to_login": "Login",
"to_parent": "Gå op",
"to_trash": "Papirkurv",
"toggle_settings": "Slå indstillinger til eller fra",
"toggle_theme": "Slå mørkt tema til eller fra",
"total": "Total",
"total_usage": "Samlet forbrug",
"trash": "Papirkurv",
"trash_all": "Smid alle ud",
"trash_count": "Skrald {count, number}",
"trash_delete_asset": "Papirkurv/slet aktiv",
"trash_no_results_message": "Udsmidte billeder og videoer vil kunne findes her.",
"trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.",
"type": "Type",
"unarchive": "Afakivér",
"unarchived_count": "{count, plural, other {Uarkiveret #}}",
"unfavorite": "Fjern favorit",
"unhide_person": "Hold op med at gemme person væk",
"unknown": "Ukendt",
"unknown_year": "Ukendt år",
"unlimited": "Ubegrænset",
"unlink_motion_video": "Fjern link til bevægelsesvideo",
"unlink_oauth": "Frakobl OAuth",
"unlinked_oauth_account": "Frakoblede OAuth-konto",
"unnamed_album": "Unavngivet album",
"unnamed_album_delete_confirmation": "Er du sikker på, at du vil slette dette album?",
"unnamed_share": "Unavngivet deling",
"unsaved_change": "Ændring, der ikke er gemt",
"unselect_all": "Fravælg alle",
"unselect_all_duplicates": "Fjern markeringen af alle dubletter",
"unstack": "Fjern fra stak",
"untracked_files": "Utrackede filer",
"unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}",
"untracked_files": "Usporede filer",
"untracked_files_decription": "Disse filer bliver ikke sporet af applikationen. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller efterladt på grund af en fejl",
"up_next": "Næste",
"updated_password": "Opdaterede adgangskode",
"upload": "Upload",
"upload_concurrency": "Uploadsamtidighed",
"upload_errors": "Upload afsluttet med {count, plural, one {# fejl} other {# fejl}}. Opdater siden for at se nye uploadaktiver.",
"upload_progress": "Resterende {remaining, number} - Behandlet {processed, number}/{total, number}",
"upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}",
"upload_status_duplicates": "Dubletter",
"upload_status_errors": "Fejl",
"upload_status_uploaded": "Uploaded",
"upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.",
"url": "URL",
"usage": "Forbrug",
"use_custom_date_range": "Brug tilpasset datointerval i stedet",
"user": "Bruger",
"user_id": "Bruger-ID",
"user_liked": "{user} kunne lide {type, select, photo {dette billede} video {denne video} asset {dette aktiv} other {det}}",
"user_purchase_settings": "Køb",
"user_purchase_settings_description": "Administrer dit køb",
"user_role_set": "Indstil {user} som {role}",
"user_usage_detail": "Detaljer om brugers forbrug",
"user_usage_stats": "Konto anvendelsesstatistik",
"user_usage_stats_description": "Vis konto anvendelsesstatistik",
@@ -1019,21 +1318,33 @@
"validate": "Validér",
"variables": "Variabler",
"version": "Version",
"version_announcement_closing": "Din ven, Alex",
"version_announcement_message": "Hej! En ny version af Immich er tilgængelig. Brug venligst lidt tid på at læse <link>udgivelsesbemærkningerne</link> for at sikre, at din opsætning er opdateret for at forhindre fejlkonfigurationer, især hvis du bruger WatchTower eller en mekanisme, der håndterer automatisk opdatering af din Immich-instans.",
"version_history": "Versionshistorik",
"version_history_item": "Installerede {version} den {date}",
"video": "Video",
"video_hover_setting": "Afspil miniaturevisning af video når musemarkøren er over den",
"video_hover_setting_description": "Afspil miniaturevisning for videoer når musemarkøren holdes over elementet. Selv når det er deaktiveret, kan afspilning startes ved at holde musen over afspilningsikonet.",
"videos": "Videoer",
"videos_count": "{count, plural, one {# Video} other {# Videoer}}",
"view": "Se",
"view_album": "Se album",
"view_all": "Se alle",
"view_all_users": "Se alle brugere",
"view_in_timeline": "Se på tidslinjen",
"view_links": "Vis links",
"view_name": "Se",
"view_next_asset": "Se næste medie",
"view_previous_asset": "Se forrige medie",
"view_stack": "Vis stak",
"visibility_changed": "Synlighed ændret for {count, plural, one {# person} other {# personer}}",
"waiting": "Venter",
"warning": "Advarsel",
"week": "Uge",
"welcome": "Velkommen",
"welcome_to_immich": "Velkommen til immich",
"welcome_to_immich": "Velkommen til Immich",
"year": "År",
"years_ago": "{years, plural, one {# år} other {# år}} siden",
"yes": "Ja",
"you_dont_have_any_shared_links": "Du har ikke nogen delte links",
"zoom_image": "Zoom billede"

View File

@@ -34,7 +34,7 @@
"authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten",
"authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
"background_task_job": "Hintergrund-Aufgaben",
"background_task_job": "Hintergrundaufgaben",
"backup_database": "Datenbank sichern",
"backup_database_enable_description": "Sicherung der Datenbank aktivieren",
"backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
@@ -83,9 +83,9 @@
"job_concurrency": "{job} (Anzahl gleichzeitiger Prozesse)",
"job_created": "Aufgabe erstellt",
"job_not_concurrency_safe": "Diese Aufgabe ist nicht parallelisierungssicher.",
"job_settings": "Aufgaben-Einstellungen",
"job_settings_description": "Gleichzeitige Aufgaben-Prozesse verwalten",
"job_status": "Aufgaben-Status",
"job_settings": "Aufgabeneinstellungen",
"job_settings_description": "Die gleichzeitige Ausführung von Aufgaben verwalten",
"job_status": "Aufgabenstatus",
"jobs_delayed": "{jobCount, plural, other {# verzögert}}",
"jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}",
"library_created": "Bibliothek erstellt: {library}",
@@ -211,7 +211,7 @@
"quota_size_gib": "Kontingent (GiB)",
"refreshing_all_libraries": "Alle Bibliotheken aktualisieren",
"registration": "Admin-Registrierung",
"registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.",
"registration_description": "Da du der erste Benutzer im System bist, wird dir die Rolle des Administrators zugewiesen, womit du für die Verwaltungsaufgaben verantwortlich bist. Weitere Benutzer werden von dir erstellt.",
"repair_all": "Alle reparieren",
"repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden",
"repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert",
@@ -254,12 +254,12 @@
"template_email_if_empty": "Wenn die Vorlage leer ist, wird die Standard-E-Mail verwendet.",
"template_email_invite_album": "E-Mail-Vorlage: Einladung zu Album",
"template_email_preview": "Vorschau",
"template_email_settings": "E-Mail Vorlagen",
"template_email_settings_description": "Benutzerdefinierte E-Mail Benachrichtigungsvorlagen verwalten",
"template_email_update_album": "Album Vorlage aktualisieren",
"template_email_welcome": "Willkommen bei den E-Mail Vorlagen",
"template_email_settings": "E-Mail-Vorlagen",
"template_email_settings_description": "Benutzerdefinierte E-Mail-Benachrichtigungsvorlagen verwalten",
"template_email_update_album": "Album-Vorlage aktualisieren",
"template_email_welcome": "Willkommen bei den E-Mail-Vorlagen",
"template_settings": "Benachrichtigungsvorlagen",
"template_settings_description": "Benutzerdefinierte Vorlagen für Benachrichtigungen verwalten",
"template_settings_description": "Benutzerdefinierte Vorlagen für Benachrichtigungen verwalten.",
"theme_custom_css_settings": "Benutzerdefiniertes CSS",
"theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.",
"theme_settings": "Theme-Einstellungen",
@@ -287,8 +287,10 @@
"transcoding_constant_quality_mode": "Modus für konstante Qualität",
"transcoding_constant_quality_mode_description": "ICQ ist besser als CQP, aber einige Hardware-Beschleunigungsgeräte unterstützen diesen Modus nicht. Wenn diese Option gesetzt wird, wird der angegebene Modus bevorzugt, sobald qualitätsbasierte Kodierung verwendet wird. Wird von NVENC ignoriert, da es ICQ nicht unterstützt.",
"transcoding_constant_rate_factor": "Faktor der konstanten Rate (-crf)",
"transcoding_constant_rate_factor_description": "Video-Qualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
"transcoding_constant_rate_factor_description": "Videoqualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
"transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen",
"transcoding_encoding_options": "Kodierungsoptionen",
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für kodierte Videos",
"transcoding_hardware_acceleration": "Hardware-Beschleunigung",
"transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität",
"transcoding_hardware_decoding": "Hardware-Dekodierung",
@@ -301,6 +303,8 @@
"transcoding_max_keyframe_interval": "Maximales Keyframe-Intervall",
"transcoding_max_keyframe_interval_description": "Legt den maximalen Frame-Abstand zwischen Keyframes fest. Niedrigere Werte verschlechtern die Komprimierungseffizienz, verbessern aber die Suchzeiten und können die Qualität in Szenen mit schnellen Bewegungen verbessern. Bei 0 wird dieser Wert automatisch eingestellt.",
"transcoding_optimal_description": "Videos mit einer höheren Auflösung als der Zielauflösung oder in einem nicht akzeptierten Format",
"transcoding_policy": "Transkodierungsrichtlinie",
"transcoding_policy_description": "Bestimme, wann ein Video transkodiert wird",
"transcoding_preferred_hardware_device": "Bevorzugtes Hardwaregerät",
"transcoding_preferred_hardware_device_description": "Gilt nur für VAAPI und QSV. Legt den für die Hardware-Transkodierung verwendeten dri-Node fest.",
"transcoding_preset_preset": "Voreinstellung (-preset)",
@@ -308,14 +312,14 @@
"transcoding_reference_frames": "Referenz-Frames",
"transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.",
"transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format",
"transcoding_settings": "Video-Transkodierungseinstellungen",
"transcoding_settings_description": "Auflösungs- und Kodierungsinformationen von Videodateien verwalten",
"transcoding_settings": "Einstellungen für die Videotranskodierung",
"transcoding_settings_description": "Verwalten welche Videos transkodiert werden und wie diese verarbeitet werden",
"transcoding_target_resolution": "Ziel-Auflösung",
"transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.",
"transcoding_temporal_aq": "Temporäre AQ",
"transcoding_temporal_aq_description": "Gilt nur für NVENC. Verbessert die Qualität von Szenen mit hohem Detailreichtum und geringen Bewegungen. Dies ist möglicherweise nicht mit älteren Geräten kompatibel.",
"transcoding_threads": "Threads",
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.",
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Kodierung, lassen dem Server jedoch weniger Spielraum für die Verarbeitung anderer Aufgaben im aktiven Zustand. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Maximiert die Auslastung, wenn der Wert auf 0 gesetzt wird.",
"transcoding_tone_mapping": "Farbton-Mapping",
"transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.",
"transcoding_transcode_policy": "Transcodierungsrichtlinie",
@@ -324,11 +328,11 @@
"transcoding_two_pass_encoding_setting_description": "Führt eine Transkodierung in zwei Durchgängen durch, um besser kodierte Videos zu erzeugen. Wenn die maximale Bitrate aktiviert ist (erforderlich für die Verwendung mit H.264 und HEVC), verwendet dieser Modus einen Bitratenbereich, der auf der maximalen Bitrate basiert, und ignoriert CRF. Für VP9 kann CRF verwendet werden, wenn die maximale Bitrate deaktiviert ist.",
"transcoding_video_codec": "Video-Codec",
"transcoding_video_codec_description": "VP9 hat eine hohe Effizienz und Webkompatibilität, braucht aber länger für die Transkodierung. HEVC bietet eine ähnliche Leistung, ist aber weniger web-kompatibel. H.264 ist weitgehend kompatibel und lässt sich schnell transkodieren, erzeugt aber viel größere Dateien. AV1 ist der effizienteste Codec, wird aber von älteren Geräten nicht unterstützt.",
"trash_enabled_description": "Papierkorb-Funktionen aktivieren",
"trash_enabled_description": "Papierkorbfunktionen aktivieren",
"trash_number_of_days": "Anzahl der Tage",
"trash_number_of_days_description": "Anzahl der Tage, welche die Objekte im Papierkorb verbleiben, bevor sie endgültig entfernt werden",
"trash_settings": "Papierkorb-Einstellungen",
"trash_settings_description": "Papierkorb-Einstellungen verwalten",
"trash_settings": "Papierkorbeinstellungen",
"trash_settings_description": "Papierkorbeinstellungen verwalten",
"untracked_files": "Unverfolgte Dateien",
"untracked_files_description": "Diese Dateien werden nicht von der Anwendung getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
"user_cleanup_job": "Benutzer aufräumen",
@@ -342,8 +346,8 @@
"user_password_reset_description": "Bitte gib dem Benutzer das temporäre Passwort und informiere ihn, dass das Passwort beim nächsten Login geändert werden muss.",
"user_restore_description": "Das Konto von <b>{user}</b> wird wiederhergestellt.",
"user_restore_scheduled_removal": "Wiederherstellung des Benutzers - geplante Entfernung am {date, date, long}",
"user_settings": "Benutzer-Einstellungen",
"user_settings_description": "Benutzer-Einstellungen verwalten",
"user_settings": "Benutzereinstellungen",
"user_settings_description": "Benutzereinstellungen verwalten",
"user_successfully_removed": "Benutzer {email} wurde erfolgreich entfernt.",
"version_check_enabled_description": "Versionsprüfung aktivieren",
"version_check_implications": "Die Funktion zur Versionsprüfung basiert auf regelmäßiger Kommunikation mit GitHub.com",
@@ -519,6 +523,10 @@
"date_range": "Datumsbereich",
"day": "Tag",
"deduplicate_all": "Alle Duplikate entfernen",
"deduplication_criteria_1": "Bildgröße in Bytes",
"deduplication_criteria_2": "Anzahl der EXIF-Daten",
"deduplication_info": "Deduplizierungsinformationen",
"deduplication_info_description": "Für die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berücksichtigen wir:",
"default_locale": "Standard-Sprache",
"default_locale_description": "Datumsangaben und Zahlen basierend auf dem Gebietsschema des Browsers formatieren",
"delete": "Löschen",
@@ -755,6 +763,7 @@
"get_help": "Hilfe erhalten",
"getting_started": "Erste Schritte",
"go_back": "Zurück",
"go_to_folder": "Gehe zu Ordner",
"go_to_search": "Zur Suche gehen",
"group_albums_by": "Alben gruppieren nach...",
"group_no": "Keine Gruppierung",
@@ -1141,6 +1150,7 @@
"server_version": "Server-Version",
"set": "Speichern",
"set_as_album_cover": "Als Albumcover festlegen",
"set_as_featured_photo": "Als Anzeigebild setzen",
"set_as_profile_picture": "Als Profilbild festlegen",
"set_date_of_birth": "Geburtsdatum festlegen",
"set_profile_picture": "Profilbild einstellen",
@@ -1196,6 +1206,7 @@
"sort_items": "Anzahl der Einträge",
"sort_modified": "Änderungsdatum",
"sort_oldest": "Ältestes Foto",
"sort_people_by_similarity": "Personen nach Ähnlichkeit sortieren",
"sort_recent": "Neustes Foto",
"sort_title": "Titel",
"source": "Quellcode",
@@ -1313,7 +1324,7 @@
"version_history_item": "{version} am {date} installiert",
"video": "Video",
"video_hover_setting": "Videovorschau beim Hovern abspielen",
"video_hover_setting_description": "Video-Miniaturansicht wiedergeben, wenn der Mauszeiger über dem Element verweilt. Auch wenn diese Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem der Mauszeiger auf das Wiedergabesymbol bewegt wird.",
"video_hover_setting_description": "Spiele die Miniaturansicht des Videos ab, wenn sich die Maus über dem Element befindet. Auch wenn die Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem du mit der Maus über das Wiedergabesymbol fährst.",
"videos": "Videos",
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
"view": "Ansicht",

View File

@@ -289,6 +289,8 @@
"transcoding_constant_rate_factor": "Σταθερός παράγοντας ρυθμού (-crf)",
"transcoding_constant_rate_factor_description": "Επίπεδο ποιότητας βίντεο. Οι τυπικές τιμές είναι οι, 23 για το H.264, 28 για το HEVC, 31 για το VP9 και 35 για το AV1. Χαμηλότερες τιμές σημαίνουν καλύτερη ποιότητα, αλλά παράγουν μεγαλύτερα αρχεία.",
"transcoding_disabled_description": "Να μην μετατραπεί κανένα βίντεο γιατί δύναται να προκαλέσει πρόβλημα αναπαραγωγής σε ορισμένες συσκευές/εφαρμογές",
"transcoding_encoding_options": "Επιλογές κωδικοποίησης",
"transcoding_encoding_options_description": "Ορίστε τους κωδικοποιητές, την ανάλυση, την ποιότητα και άλλες επιλογές για τα κωδικοποιημένα βίντεο",
"transcoding_hardware_acceleration": "Επιτάχυνση υλικού",
"transcoding_hardware_acceleration_description": "Πειραματικό· πολύ πιο γρήγορο, αλλά θα έχει χαμηλότερη ποιότητα με τον ίδιο ρυθμό μετάδοσης (bitrate)",
"transcoding_hardware_decoding": "Αποκωδικοποίηση μέσω υλικού",
@@ -301,6 +303,8 @@
"transcoding_max_keyframe_interval": "Μέγιστο χρονικό διάστημα μεταξύ των καρέ αναφοράς (keyframe)",
"transcoding_max_keyframe_interval_description": "Ορίζει το μέγιστο διάστημα μεταξύ των καρέ αναφοράς. Χαμηλότερες τιμές μειώνουν την αποδοτικότητα συμπίεσης, αλλά βελτιώνουν τον χρόνο αναζήτησης και μπορεί να βελτιώσουν την ποιότητα σε σκηνές με γρήγορη κίνηση. Η τιμή 0 ρυθμίζει αυτό το διάστημα αυτόματα.",
"transcoding_optimal_description": "Βίντεο με ανώτερη ανάλυση από την επιθυμητή ή σε μη αποδεκτή μορφή",
"transcoding_policy": "Πολιτική Μετακωδικοποίησης",
"transcoding_policy_description": "Ορίστε πότε θα γίνει η μετακωδικοποίηση ενός βίντεο",
"transcoding_preferred_hardware_device": "Προτιμώμενη συσκευή",
"transcoding_preferred_hardware_device_description": "Ισχύει μόνο για VAAPI και QSV. Ορίζει τον κόμβο DRI που χρησιμοποιείται για την επιτάχυνση υλικού κατά την κωδικοποίηση.",
"transcoding_preset_preset": "Προκαθορισμένη ρύθμιση (-preset)",
@@ -309,7 +313,7 @@
"transcoding_reference_frames_description": "Ο αριθμός των καρέ που χρησιμοποιούνται ως αναφορά κατά τη συμπίεση ενός δεδομένου καρέ. Υψηλότερες τιμές βελτιώνουν την αποδοτικότητα της συμπίεσης, αλλά επιβραδύνουν την κωδικοποίηση. Η τιμή 0 ρυθμίζει αυτό τον αριθμό, αυτόματα.",
"transcoding_required_description": "Μόνο βίντεο που δεν είναι σε αποδεκτή μορφή",
"transcoding_settings": "Ρυθμίσεις μετατροπής βίντεο",
"transcoding_settings_description": "Διαχείριση της ανάλυσης και των πληροφοριών κωδικοποίησης των αρχείων βίντεο",
"transcoding_settings_description": "Διαχείριση των βίντεο που θα μετακωδικοποιηθούν και του τρόπου επεξεργασίας τους",
"transcoding_target_resolution": "Επιθυμητή ανάλυση",
"transcoding_target_resolution_description": "Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά απαιτούν περισσότερο χρόνο για κωδικοποίηση, παράγουν μεγαλύτερα αρχεία και μπορεί να μειώσουν την απόκριση της εφαρμογής.",
"transcoding_temporal_aq": "Χρονική Προσαρμοστική Ποιότητα AQ(Adaptive Quantization)",
@@ -322,7 +326,7 @@
"transcoding_transcode_policy_description": "Πολιτική για το πότε πρέπει να μετατραπεί ένα βίντεο. Τα βίντεο HDR θα μετατρέπονται πάντα (εκτός αν η μετατροπή είναι απενεργοποιημένη).",
"transcoding_two_pass_encoding": "Κωδικοποίηση δύο περασμάτων",
"transcoding_two_pass_encoding_setting_description": "Μετατροπή σε δύο περάσματα για την παραγωγή βίντεο με καλύτερη κωδικοποίηση. Όταν είναι ενεργοποιημένος ο μέγιστος ρυθμός μετάδοσης (απαραίτητος για λειτουργία με H.264 και HEVC), αυτή η λειτουργία χρησιμοποιεί ένα εύρος ρυθμού μετάδοσης βάσει του μέγιστου ρυθμού μετάδοσης και αγνοεί το CRF. Στον κωδικοποιητή VP9, το CRF μπορεί να χρησιμοποιηθεί εάν ο μέγιστος ρυθμός μετάδοσης είναι απενεργοποιημένος.",
"transcoding_video_codec": "Κωδικοποιητής Βίντεο",
"transcoding_video_codec": "Κωδικοποιητής βίντεο",
"transcoding_video_codec_description": "Ο VP9 έχει υψηλή απόδοση και συμβατότητα με τον ιστότοπο, αλλά απαιτεί περισσότερο χρόνο για μετατροπή. Ο HEVC έχει παρόμοια απόδοση, αλλά χαμηλότερη συμβατότητα με τον ιστότοπο. Ο H.264 είναι ευρέως συμβατός και γρήγορος στη μετατροπή, αλλά παράγει πολύ μεγαλύτερα αρχεία. Ο AV1 είναι ο πιο αποδοτικός κωδικοποιητής, αλλά δεν υποστηρίζεται σε παλαιότερες συσκευές.",
"trash_enabled_description": "Ενεργοποίηση λειτουργιών Κάδου Απορριμμάτων",
"trash_number_of_days": "Αριθμός ημερών",
@@ -519,6 +523,10 @@
"date_range": "Εύρος ημερομηνιών",
"day": "Ημέρα",
"deduplicate_all": "Αφαίρεση όλων των διπλότυπων",
"deduplication_criteria_1": "Μέγεθος εικόνας σε byte",
"deduplication_criteria_2": "Αριθμός δεδομένων EXIF",
"deduplication_info": "Πληροφορίες Αφαίρεσης Διπλοτύπων",
"deduplication_info_description": "Για να προεπιλέξουμε αυτόματα τα αρχεία και να αφαιρέσουμε τα διπλότυπα σε μαζική επεξεργασία, εξετάζουμε σε:",
"default_locale": "Προεπιλεγμένη Τοπική Ρύθμιση",
"default_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς με βάση την τοπική ρύθμιση του προγράμματος περιήγησής σας",
"delete": "Διαγραφή",
@@ -755,6 +763,7 @@
"get_help": "Ζητήστε βοήθεια",
"getting_started": "Ξεκινώντας",
"go_back": "Πηγαίνετε πίσω",
"go_to_folder": "Μετάβαση στο φάκελο",
"go_to_search": "Πηγαίνετε στην αναζήτηση",
"group_albums_by": "Ομαδοποίηση άλμπουμ κατά...",
"group_no": "Καμία ομοδοποίηση",
@@ -1141,6 +1150,7 @@
"server_version": "Έκδοση Διακομιστή",
"set": "Ορισμός",
"set_as_album_cover": "Ορισμός ως εξώφυλλο άλμπουμ",
"set_as_featured_photo": "Ορισμός ως χαρακτηριστική φωτογραφία",
"set_as_profile_picture": "Ορισμός ως εικόνα προφίλ",
"set_date_of_birth": "Ορισμός ημερομηνίας γέννησης",
"set_profile_picture": "Ορισμός εικόνας προφίλ",
@@ -1196,6 +1206,7 @@
"sort_items": "Αριθμός αντικειμένων",
"sort_modified": "Ημερομηνία τροποποίησης",
"sort_oldest": "Η πιο παλιά φωτογραφία",
"sort_people_by_similarity": "Ταξινόμηση ατόμων κατά ομοιότητα",
"sort_recent": "Η πιο πρόσφατη φωτογραφία",
"sort_title": "Τίτλος",
"source": "Πηγή",
@@ -1281,7 +1292,7 @@
"up_next": "Ακολουθεί",
"updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε",
"upload": "Μεταφόρτωση",
"upload_concurrency": "Συγχρονισμός μεταφόρτωσης",
"upload_concurrency": "Ταυτόχρονη μεταφόρτωση",
"upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.",
"upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}",
"upload_skipped_duplicates": "Παραλείφθηκαν {count, plural, one {# διπλότυπο στοιχείο} other {# διπλότυπα στοιχεία}}",

View File

@@ -1,4 +1,6 @@
{
"search_by_description_example": "Hiking day in Sapa",
"search_by_description": "Search by description",
"about": "About",
"account": "Account",
"account_settings": "Account Settings",
@@ -434,6 +436,7 @@
"back_close_deselect": "Back, close, or deselect",
"backward": "Backward",
"birthdate_saved": "Date of birth saved successfully",
"show_shared_links": "Show shared links",
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
"blurred_background": "Blurred background",
"bugs_and_feature_requests": "Bugs & Feature Requests",
@@ -523,6 +526,10 @@
"date_range": "Date range",
"day": "Day",
"deduplicate_all": "Deduplicate All",
"deduplication_info": "Deduplication Info",
"deduplication_info_description": "To automatically preselect assets and remove duplicates in bulk, we look at:",
"deduplication_criteria_1": "Image size in bytes",
"deduplication_criteria_2": "Count of EXIF data",
"default_locale": "Default Locale",
"default_locale_description": "Format dates and numbers based on your browser locale",
"delete": "Delete",
@@ -760,9 +767,12 @@
"getting_started": "Getting Started",
"go_back": "Go back",
"go_to_search": "Go to search",
"go_to_folder": "Go to folder",
"group_albums_by": "Group albums by...",
"group_country": "Group by country",
"group_no": "No grouping",
"group_owner": "Group by owner",
"group_places_by": "Group places by...",
"group_year": "Group by year",
"has_quota": "Has quota",
"hi_user": "Hi {name} ({email})",
@@ -795,6 +805,7 @@
"include_shared_albums": "Include shared albums",
"include_shared_partner_assets": "Include shared partner assets",
"individual_share": "Individual share",
"individual_shares": "Individual shares",
"info": "Info",
"interval": {
"day_at_onepm": "Every day at 1pm",
@@ -805,6 +816,7 @@
"invite_people": "Invite People",
"invite_to_album": "Invite to album",
"items_count": "{count, plural, one {# item} other {# items}}",
"views_count": "{count, plural, one {# view} other {# views}}",
"jobs": "Jobs",
"keep": "Keep",
"keep_all": "Keep All",
@@ -817,6 +829,7 @@
"latest_version": "Latest Version",
"latitude": "Latitude",
"leave": "Leave",
"lens_model": "Lens model",
"let_others_respond": "Let others respond",
"level": "Level",
"library": "Library",
@@ -979,6 +992,7 @@
"pick_a_location": "Pick a location",
"place": "Place",
"places": "Places",
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
"play": "Play",
"play_memories": "Play memories",
"play_motion_photo": "Play Motion Photo",
@@ -1108,6 +1122,7 @@
"search_camera_model": "Search camera model...",
"search_city": "Search city...",
"search_country": "Search country...",
"search_for": "Search for",
"search_for_existing_person": "Search for existing person",
"search_no_people": "No people",
"search_no_people_named": "No people named \"{name}\"",
@@ -1160,6 +1175,7 @@
"shared_from_partner": "Photos from {partner}",
"shared_link_options": "Shared link options",
"shared_links": "Shared links",
"shared_links_description": "Share photos and videos with a link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
"shared_with_partner": "Shared with {partner}",
"sharing": "Sharing",
@@ -1269,6 +1285,7 @@
"unfavorite": "Unfavorite",
"unhide_person": "Unhide person",
"unknown": "Unknown",
"unknown_country": "Unknown Country",
"unknown_year": "Unknown Year",
"unlimited": "Unlimited",
"unlink_motion_video": "Unlink motion video",

View File

@@ -1,5 +1,5 @@
{
"about": "Acercade",
"about": "Acerca de",
"account": "Cuenta",
"account_settings": "Ajustes de la cuenta",
"acknowledge": "De acuerdo",
@@ -289,6 +289,8 @@
"transcoding_constant_rate_factor": "Factor de tasa constante (-crf)",
"transcoding_constant_rate_factor_description": "Nivel de calidad del vídeo. Los valores típicos son 23 para H.264, 28 para HEVC, 31 para VP9 y 35 para AV1. Cuanto más bajo es mejor, pero produce archivos más grandes.",
"transcoding_disabled_description": "No transcodifique ningún vídeo; puede interrumpir la reproducción en algunos clientes",
"transcoding_encoding_options": "Opciones de codificación",
"transcoding_encoding_options_description": "Establecer códecs, resolución, calidad y otras opciones para los vídeos codificados",
"transcoding_hardware_acceleration": "Aceleración por Hardware",
"transcoding_hardware_acceleration_description": "Experimental; mucho más rápido, pero tendrá menor calidad con la misma tasa de bits",
"transcoding_hardware_decoding": "Decodificación por hardware",
@@ -301,6 +303,8 @@
"transcoding_max_keyframe_interval": "Intervalo máximo de fotogramas clave",
"transcoding_max_keyframe_interval_description": "Establece la distancia máxima de fotograma entre fotogramas clave. Los valores más bajos empeoran la eficiencia de la compresión, pero mejoran los tiempos de búsqueda y pueden mejorar la calidad en escenas con movimientos rápidos. 0 establece este valor automáticamente.",
"transcoding_optimal_description": "Vídeos con una resolución superior a la fijada o que no están en un formato aceptado",
"transcoding_policy": "Política de transcodificación",
"transcoding_policy_description": "Establecer cuándo se transcodificará un vídeo",
"transcoding_preferred_hardware_device": "Dispositivo de hardware preferido",
"transcoding_preferred_hardware_device_description": "Se aplica únicamente a VAAPI y QSV. Establece el nodo dri utilizado para la transcodificación de hardware.",
"transcoding_preset_preset": "Configuración predefinida (-preset)",
@@ -309,7 +313,7 @@
"transcoding_reference_frames_description": "El número de fotogramas a los que hacer referencia al comprimir un fotograma determinado. Los valores más altos mejoran la eficiencia de la compresión, pero ralentizan la codificación. 0 establece este valor automáticamente.",
"transcoding_required_description": "Sólo vídeos que no estén en un formato soportado",
"transcoding_settings": "Configuración de Transcodificación de Vídeo",
"transcoding_settings_description": "Administrar la resolución y la información de codificación de los archivos de video",
"transcoding_settings_description": "Administrar qué vídeos transcodificar y cómo procesarlos",
"transcoding_target_resolution": "Resolución deseada",
"transcoding_target_resolution_description": "Las resoluciones más altas pueden conservar más detalles, pero la codificación tarda más, tienen tamaños de archivo más grandes y pueden reducir la capacidad de respuesta de la aplicación.",
"transcoding_temporal_aq": "AQ temporal",
@@ -519,6 +523,10 @@
"date_range": "Rango de fechas",
"day": "Día",
"deduplicate_all": "Deduplicar todo",
"deduplication_criteria_1": "Tamaño de imagen en bytes",
"deduplication_criteria_2": "Conteo de datos EXIF",
"deduplication_info": "Información de Deduplicación",
"deduplication_info_description": "Para automáticamente preseleccionar recursos y eliminar duplicados en conjunto, nosotros consideramos lo siguiente:",
"default_locale": "Configuración regional predeterminada",
"default_locale_description": "Formatee fechas y números según la configuración regional de su navegador",
"delete": "Eliminar",
@@ -755,6 +763,7 @@
"get_help": "Solicitar ayuda",
"getting_started": "Comenzamos",
"go_back": "Volver atrás",
"go_to_folder": "Ir al directorio",
"go_to_search": "Ir a búsqueda",
"group_albums_by": "Agrupar albums por...",
"group_no": "Sin agrupación",
@@ -1141,6 +1150,7 @@
"server_version": "Versión del servidor",
"set": "Establecer",
"set_as_album_cover": "Establecer portada del álbum",
"set_as_featured_photo": "Establecer como foto destacada",
"set_as_profile_picture": "Seleccionar como foto de perfil",
"set_date_of_birth": "Establecer fecha de nacimiento",
"set_profile_picture": "Establecer foto de perfil",
@@ -1196,6 +1206,7 @@
"sort_items": "Número de archivos",
"sort_modified": "Fecha de modificación",
"sort_oldest": "Foto más antigua",
"sort_people_by_similarity": "Ordenar personas por similitud",
"sort_recent": "Foto más reciente",
"sort_title": "Título",
"source": "Origen",

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