Compare commits

...

253 Commits

Author SHA1 Message Date
renovate[bot] f8e827ac52 fix(deps): update mobile 2026-05-11 14:24:57 +00:00
bo0tzz b4f719653f fix: indentation and typo (#28349)
* fix: indentation and typo

* neline
2026-05-11 09:17:39 -05:00
bo0tzz f370b4bac6 chore: fold apk comment qr in <details> (#28348) 2026-05-11 09:40:19 -04:00
Mert d788169bf3 chore(server)!: remove libopus enum (#28325) 2026-05-11 08:02:57 -04:00
Mert eea820fa2f chore(server): enable hw decoding by default (#28324) 2026-05-11 08:02:26 -04:00
Timon 271f1cb868 feat(web): Add metadata overlay to slideshow (#24627)
* feat: add slideshow metadata overlay and settings

* Introduced a new SlideshowMetadataOverlay component to display image information during slideshows.
* Updated slideshow settings modal to include options for showing the metadata overlay and selecting its display mode (Description Only or Full).
* Added corresponding translations and store management for the new overlay features.

* remove noisy log

* constant opacity

* 2nd pass

* more

* use text components

* lint
2026-05-11 11:49:12 +02:00
Mert 8c8dc9d32f chore(ml)!: remove deprecated envs (#28326)
remove deprecated envs
2026-05-09 22:40:05 +00:00
Alex fd18e55f7c chore: token extraction for build mobile (#28320)
Co-authored-by: bo0tzz <git@bo0tzz.me>
2026-05-09 15:08:07 +00:00
shenlong faab9e620d refactor: medium repository context helpers (#28311)
* refactor: medium repository context helpers

* test: add regress test for 26723

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-09 08:19:31 -05:00
Matthew Momjian 5ba3efafd8 fix(deployment): remove unneeded volume (#28307)
remove unneeded volume
2026-05-09 09:07:54 -04:00
Alex 8b3c9bf9c3 feat(ci): publish PR Android APK to comment (#28283)
* feat(ci): publish PR Android APK to R2 with installable links

Adds a universal debug APK to PR builds and uploads it to a public
R2 bucket alongside the existing GitHub Actions artifact. Posts a
sticky PR comment with tap-to-install links and a QR code so testers
can install directly on their device without unzipping artifacts.

Required setup:
- Secrets: R2_APK_ACCESS_KEY_ID, R2_APK_SECRET_ACCESS_KEY,
  R2_APK_ACCOUNT_ID, R2_APK_BUCKET
- Optional repo variable: APK_PUBLIC_HOST (defaults to apk.immich.app)
- R2 bucket configured with a public custom domain matching APK_PUBLIC_HOST

* chore(ci): drop R2 upload, link directly to GitHub artifact

Surfaces the existing release-apk-signed artifact in a sticky PR
comment with a QR code. Avoids new infra and secrets — the trade-off
is GitHub login and a zip wrapper instead of tap-to-install.

* feat(ci): build PR APK as release and publish to GitHub Release

PR builds now produce a release APK signed with the release keystore.
The universal APK is published as a GitHub Release asset under tag
'pr-<num>' (prerelease), giving testers a direct, unzipped, tap-to-
install URL plus a QR code in the PR comment. The release-apk-signed
artifact is unchanged.

* chore(ci): drop GitHub Release, publish universal APK as own artifact

Reverts the prerelease publish. Uploads the universal release APK as
a separate single-file artifact so its download URL gives a zip
containing only that APK — no extra files to dig through. The QR in
the PR comment points at this universal-only artifact.

* chore(ci): build only universal APK for PR, drop split artifact

PR builds skip the arm64-only split — release-apk-signed now contains
just the universal app-release.apk, so the download zip is a single
file. Removes the redundant separate universal artifact and points
the PR comment QR at the main artifact URL.

* feat(mobile): suffix PR APK applicationId so it installs alongside production

Each PR build now becomes app.alextran.immich.pr<num> via PR_NUMBER env
read in build.gradle, so testers can install multiple PR builds and the
Play Store version on the same device without uninstalling. Also tags
the version with -pr<num> for visibility.

* feat(ci): allow PR APK build to run on forks

Forks can now run the Android build job. Steps that need repo secrets
(create-workflow-token, Create Keystore) are skipped when the PR is
from a fork, the checkout falls back to GITHUB_TOKEN, and build.gradle
falls back to debug signing if the release keystore isn't materialised.
The PR comment still requires write access, so it's gated to non-fork
PRs — fork APKs are reachable from the workflow run's artifact tab.
2026-05-09 07:46:40 -05:00
Yaros 41f285aa3e feat(mobile): increased tap area on video player overlay (#27269)
* fix(mobile): improved tap area on video player

* fix: back button padding

* chore: use sizedbox.square & button padding

* chore: fixed padding
2026-05-09 10:47:41 +07:00
Sandro fdac6c8bc4 fix(docs): missing colon in config file doc (#28313)
Fix missing colon
2026-05-09 09:44:41 +07:00
Thorsten Winkler d7f05d2510 fix(mobile): Deduplicate assets in person view timeline (#26723)
fix(mobile): deduplicate assets in person view timeline

Previously, assets with multiple face records for the same person (e.g.,
manual Digikam imports and Immich ML detections) appeared multiple times
in the person timeline. This was caused by an inner join on the
assetFaceEntity without proper deduplication.

This commit refactors the timeline queries to use a subquery approach
instead of joins and grouping. This ensures:

- _getPersonBucketAssets: Only unique assets are fetched, even if
  multiple face records exist for a single asset.
- _watchPersonBucket: Asset counts in timeline headers are accurate
  and represent unique assets.
- Performance: Database overhead is reduced by avoiding complex joins
  and explicit groupBy operations on large result sets.

Signed-off-by: thowdev <12428285+thowdev@users.noreply.github.com>
2026-05-09 01:53:18 +00:00
stfn 3100bd5eed fix(mobile): avoid duplicate assets in album view (#28152)
fix(mobile): avoid duplicate assets in remote album timeline

Co-authored-by: Stefan Friedli <stefan@stefanfriedli.ch>
2026-05-09 08:24:54 +07:00
Jason Rasmussen 8a024e2b50 chore: faster web linting (#28303) 2026-05-08 16:55:14 -04:00
Jason Rasmussen 25a6a38b30 chore: use mise (#28298) 2026-05-08 15:21:33 -04:00
Santo Shakil 7c6750941e fix(mobile): mounted check before setState in album sync action (#28300)
_manualSyncAlbums fires a setState 1s after sync via Future.delayed
with no mounted check. if the widget is gone by then, setState throws
null check and the global error logger logs it severe.
2026-05-08 18:55:03 +00:00
renovate[bot] 832ed4d015 chore(deps): update dependency exiftool-vendored to v35.19.0 [security] (#28261)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-08 12:24:31 +02:00
Santo Shakil 238895cad9 fix(mobile): restore notification plugin init (#28284)
#27666 removed LocalNotificationService with the legacy stack, which
was the only place calling FlutterLocalNotificationsPlugin().initialize().
without it, ios never prompts for the notification perm on fresh
installs so background_downloader notifications get dropped silently.

restores the init in the same spot the deleted call used to live.
2026-05-08 10:45:52 +07:00
sakshamchawla e2ec04e86c feat: hide hidden person from memories (#20877)
* hide hidden person from memories

* clean up

* fix united test

* clean up

* moved sql to inline, rebased

* clean up

* clean up again

* chore: sync sql

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-07 19:54:26 +00:00
renovate[bot] 6050526360 fix(deps): update dependency connectivity_plus to v7 (#22921)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:22:26 -04:00
renovate[bot] bfd76570c5 chore(deps): update dependency python-multipart to v0.0.27 [security] (#28286)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:22:03 -04:00
renovate[bot] 37e6a49652 fix(deps): update dependency nestjs-otel to v8 (#27863)
* fix(deps): update dependency nestjs-otel to v8

* fix: apiMetrics

---------

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-05-07 19:14:15 +00:00
Mert 36caeb34ec chore(ml)!: require numpy 2.4 (#28158)
require numpy 2.4
2026-05-07 19:07:39 +00:00
renovate[bot] 87713c7f2f chore(deps): update dependency flutter to v3.41.9 (#28235)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-07 15:00:37 -04:00
Daniel Dietzler 2039c129f2 refactor: settings accordion reactivity (#28281) 2026-05-07 19:00:23 +00:00
Timon 52b00b0bad chore(ml): add mise checklist command (#28267)
* chore(ml): add mise checklist command

* don't depend tests on installing a cpu flavor
2026-05-07 12:28:24 -04:00
shenlong 21af184045 refactor: move cleanup config to metadata table (#28225)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

* migrate primary color

* migrate dynamic theme

* migrate colorfulInterface

* cleanup providers

* migrate cleanup

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 11:27:06 -05:00
Timon 1fcc2b704b feat(server)!: add isOwned filter to albums API (#28213)
* feat(server)!: add owned filter to albums API

BREAKING CHANGE: GET /albums with no parameters now returns all accessible albums (owned + shared-with-me) instead of only owned albums.

* document tri-state matrix

* web impl

* collapse to single method and handover branching to sql

* dedupe

* verify that owned, shared, and notShared counts are mapped independently from their respective queries

* refactor(server): add select:['id'] overload to albumRepository.getAll

Avoid fetching full album rows (with albumUsers/sharedLinks subqueries) in map.service where only album IDs are needed.

* focus relevant test filters

* fmt

* Revert "verify that owned, shared, and notShared counts are mapped independently from their respective queries"

This reverts commit 47aab458192c766de4662aada5a6841b091d2a80.

* sync sql

* Revert "document tri-state matrix"

This reverts commit a5b2355d0c.

* address review comments

* inline shared condition and return as ternary

* sync sql

* use [...albums].sort

Array.toSorted() is not supported in Chrome 109

* use isShared and isOwned nomenclature

* fix e2e tests

* add params to sql query
2026-05-07 12:13:07 -04:00
Timon 7de73dc176 fix(server): hide isFavorite from partner asset sync stream (#28035)
* fix(server): hide isFavorite from partner asset sync stream

* use new column entry instead

* sync sql

* add migration

* use sql.val

* sync sql
2026-05-07 12:00:54 -04:00
TheBestX11 fe2bf0c6dd fix(mobile): correct filter default and UI desync in similar photos search (#27516)
* fix(mobile): view similar defaults to images only

* fix(mobile): reset filter chips when pre-filter is applied

---------

Co-authored-by: shenlong <139912620+shenlong-tanwen@users.noreply.github.com>
2026-05-07 15:22:35 +00:00
shenlong d4a97f2d25 refactor: move theme config to metadata table (#28224)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

* migrate primary color

* migrate dynamic theme

* migrate colorfulInterface

* cleanup providers

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 15:12:14 +00:00
shenlong bd58db4fcc fix: periodically execute pragma optimize (#28241)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-07 09:54:35 -05:00
Luis Nachtigall 7f43c6a3a3 fix(mobile): prevent asset loading issues when changing page or when closing memories (#27596) 2026-05-07 09:13:22 -04:00
Mees Frensel 87175ee56c fix(docs): document upgrade-insecure-requests default (#28279) 2026-05-07 08:40:14 -04:00
Mees Frensel 13587bf13c feat(web): video player i18n (#28192) 2026-05-07 13:39:37 +02:00
Thomas f09769a2f3 chore(mobile): add box shadow to asset details (#27510)
The details widget can blend with the image when they are similar
colours.
2026-05-06 19:43:55 +00:00
bo0tzz bfdff12ee0 chore: use app token for mise in workflows (#28270) 2026-05-06 14:51:25 -04:00
bo0tzz eb6dca6a31 chore: switch push-o-matic auth from app-id to client-id (#28269) 2026-05-06 17:01:14 +00:00
Daniel Dietzler c2e3739a58 chore: run relevant tests for pnpm updates (#28266) 2026-05-06 15:38:00 +00:00
Daniel Dietzler f6bd514cdc chore: silence svelte state referenced locally warning (#28263) 2026-05-06 17:22:39 +02:00
Timon d93ab7707e chore(ml): configure mise for machine-learning directory (#25579) 2026-05-06 11:03:54 -04:00
renovate[bot] 6bb47c802f chore(deps): update github-actions (#28262)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 16:00:03 +02:00
mws-weekend-projects 90a69e2ba6 feat(web): add full-path search mode to UI (#26758)
Co-authored-by: mws-weekend-projects <mws-weekend-projects@users.noreply.github.com>
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2026-05-06 15:45:40 +02:00
Mert 6580394cfe chore(server): simplify sharp edit code (#28249) 2026-05-06 09:32:49 -04:00
Daniel Dietzler 42ff3b705d fix: revert "chore(deps): update dependency exiftool-vendored to v35.19.0 [security]" (#28260) 2026-05-06 15:32:25 +02:00
Mert 0f00053bb1 chore(server): simplify preview extraction (#28250) 2026-05-06 09:32:19 -04:00
Mert c5c59ed040 refactor(server): move video interface fetch to storage core (#28248) 2026-05-06 09:32:03 -04:00
Aigars Mahinovs 576b1eb999 docs: update rocm installation instructions (#25434) 2026-05-06 09:30:42 -04:00
Mees Frensel 24189702da fix(web): shared album avatars opening modal (#26719)
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-06 13:49:12 +02:00
Sky ad0d01005e fix(web): migrate people management component to page, enabling tooltips (#26971) 2026-05-06 13:25:59 +02:00
Brent Hugh 3e6d053f93 chore: enhance documentation on wildcard and exclusion patterns (#27884)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-05-06 12:36:25 +02:00
renovate[bot] 1bb3fd985f chore(deps): update dependency exiftool-vendored to v35.19.0 [security] (#28254)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-06 12:28:47 +02:00
Andreas Heinz f72aa54a1f feat(enhancement): Navigate stack with up and down arrow keys (#27854)
* feat(enhancement): navigate stack with up and down arrow keys

* remove unnecessary code

* move shortcut to section; no need for document level
2026-05-06 11:56:23 +02:00
shenlong dafe9d7966 chore: pump version (#28231)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-05 16:27:04 -05:00
Daniel Dietzler 7acda0572d fix: stale person name after merge (#28222) 2026-05-05 15:34:49 +02:00
renovate[bot] 98bc9f6a6e chore(deps): update dependency terragrunt to v1.0.3 (#28236)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:52:14 +02:00
renovate[bot] 63a3b405c3 chore(deps): update grafana/grafana docker tag to v12.4.3 (#28238)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-05-05 12:51:18 +02:00
Ben Beckford 0058df798d fix(mobile): show lens info without lens name (#28234)
* fix(mobile): always display lens subtitle

* fix(mobile): hide empty lens subtitles
2026-05-05 10:46:09 +07:00
shenlong 97100a4362 refactor: app metadata (#28113)
* refactor: app metadata

* refactor to per row store

* cleanup

* more test

* review changes

* more refactor

* refactor

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-05 10:45:51 +07:00
Alex af39384efb chore: better contrast for highlighted button on control bar (#28217) 2026-05-04 09:39:37 -05:00
Mert 01712cf0a7 fix(server): av typing (#28223)
* fix av typing, move fixtures to stub file

* fix tests
2026-05-04 09:04:29 -04:00
Michel Heusschen 2015f95ff5 fix(web): correct timeline yesterday label across month boundaries (#28183) 2026-05-04 13:46:11 +02:00
Timon d4f29ab6ac fix(server): validate duplicate group ownership before dismissal (#28221) 2026-05-04 12:51:54 +02:00
Timon 3decc864b5 refactor(server)!: structured validation error responses (#28204)
* refactor(server)!: structured validation error responses

* refactor(server): clarify comment on removing duplicate HTTP response fields

* enhance validation error tests

* make path and message required

* fmt

* fix e2e test

* fmt

* feat: enhance error handling in getServerErrorMessage function
2026-05-04 00:00:03 -04:00
David Allen eca0e60db8 fix: librknnrt permissions in machine-learning (#28216)
fix librknnrt permissions in machine-learning
2026-05-03 23:39:27 +00:00
AyaanMAG 8cff5883b5 fix(ml): respect time zone for logs in cuda container (#28155) 2026-05-03 04:19:56 +00:00
Mees Frensel 3d320d9751 fix(web): fix shared link /s/photos.* navigation after password login (#27788)
* fix(web): fix shared link navigation after password login

* use regex after all

* chore: use special case for shared link with slug route

* dont use onMount

* fix lint

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-05-02 09:26:35 -04:00
Daniel Dietzler b9e0e65bdb fix: migration order (#28191) 2026-05-01 20:57:30 +00:00
shenlong 88e5e8d6ea chore: pump dcm to 1.37.0 (#28188)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-01 15:39:29 -05:00
shenlong ee107c98d5 chore: pump flutter to 3.41.9 (#28187)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-05-01 15:39:19 -05:00
Mees Frensel affe0ac5ee feat(web): custom video player controls (#26183)
* feat(web): custom video player controls

* add seek & rate buttons

* wrap memory viewer in media-controller for muted/volume store

* fix memories

* disable video shortcut keys

* re-add playsinline for safari iphone playback

* fix black screen issue

* always display time range

* remove seek buttons and center controls, and put time range above controls

* change ui

* update memory viewer

* fix full width on video player on safari

* enhance video player layout by ensuring full width and maintaining aspect ratio

* layout: don't shrink buttons, tabular time text

---------

Co-authored-by: timonrieger <mail@timonrieger.de>
2026-05-01 17:26:30 +00:00
Mert f1d8ab8aae feat(server): track video metadata (#28023)
* track video metadata

* earlier duration check

* revert colorspace change

* duplicate constant

* formatting

* linting

* add comments

* redundant variable

* simplify tests

* use totalDuration instead of format.duration

* medium tests

* install ffmpeg

* install noble

* update test-assets commit

* make timeBase non-nullable

* linting

* use proper smallint

* add ffmpeg to mise

* simplify duration

* regenerate migration
2026-05-01 17:03:49 +00:00
Timon c0898b96ca refactor(server)!: sanitize error messages to avoid leaking resource details (#28154)
* refactor(server)!: sanitize error messages to avoid leaking resource and permission details

* fix e2e tests

* fix(server): prevent login timing oracle by always running bcrypt

Always call compareBcrypt in the login path regardless of whether the
email is registered. When no user is found, a dummy hash is used so the
bcrypt KDF still runs and response latency is constant, making it
impossible to enumerate valid email addresses by measuring response time.

* fix(server): collapse OAuth callback messages to prevent email-existence oracle

Two distinct error messages in the OAuth callback endpoint revealed
whether an email address was already registered in the database.
An attacker controlling the OAuth provider's email claim could probe
the user table without authentication. Both cases now return the same
generic message.

* fix(server): replace email-in-use messages to prevent user-existence oracle

Error messages on registration and profile-update that named whether an
email address was already taken allowed callers to enumerate registered
accounts. All three sites now return the same generic message regardless
of whether the address is in use.

* fix(server): hide slug uniqueness constraint to prevent shared-link probe

Surfacing the Postgres unique-constraint name in the error response let
any authenticated user brute-force whether a custom slug was already in
use by another user's shared link, leaking the existence of other links.

* fix(server): unify profile image errors to prevent user-existence oracle via status code

GET /users/:id/profile-image returned HTTP 400 for an unknown user ID
but HTTP 404 when the user existed without a photo, letting callers
distinguish the two cases. Both now return 404 so the response is
identical regardless of whether the UUID maps to an account.

* fix(server): replace album user-not-found message to prevent UUID-existence oracle

Album owners could probe arbitrary UUIDs via the add-user endpoint and
determine whether they belonged to registered accounts by receiving
'User not found'. The message is now ambiguous about whether the ID was
unrecognised or the user is inactive.

* Revert "fix e2e tests"

This reverts commit c1bd7a116b.

* Revert "refactor(server)!: sanitize error messages to avoid leaking resource and permission details"

This reverts commit b96421a083.

* fix(server): use 403 instead of 400 for access-denied errors

requireAccess threw BadRequestException which is incorrect HTTP semantics.
Access denial is a client authorization problem (403 Forbidden), not a
malformed request (400 Bad Request). Keep the descriptive permission name
in the message since the full permission set is public API surface.

* Revert "fix(server): use 403 instead of 400 for access-denied errors"

This reverts commit bb06990957.

* shorten comment

* add log messages

* format

* one more
2026-05-01 10:00:18 -04:00
Daniel Dietzler 5e9bda7fab chore: tailwind linting (#28165)
chore: tailwind cannonical classes
2026-05-01 00:18:03 -04:00
Pedro Pinhão b60e9c6771 fix(server): selectively apply metadata bitstream filter for video thumbnails (#28162) 2026-04-30 23:05:08 -04:00
Mert b554664791 chore!: duration in milliseconds (#28003)
* server changes

* openapi

* web changes

* mobile changes

* assume 3.0 client

* deprecate

* review feedback

* update medium tests

* linting
2026-04-30 09:44:27 -04:00
Mert 97c62136b7 chore(server)!: drop pgvecto.rs support (#28159)
drop pgvecto.rs
2026-04-30 09:40:38 -04:00
白隐Hakuin c1051c7ed2 fix(docs): Update Tailscale free tier user and device limits (#28151)
* docs: Update Tailscale free tier user and device limits

* chore: generalize

Updated the description of the Tailscale free tier for clarity.

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-30 10:19:49 +00:00
Snowknight26 65bd0a9320 fix(web): timeline scroll when pressing back from stacked asset (#28163) 2026-04-30 11:39:34 +02:00
Mert bf32864644 feat(server): video streaming table definitions (#28147)
* video streaming table definitions

Co-authored-by: Copilot <copilot@github.com>

* update sql

* tetris

* use enum

Co-authored-by: Copilot <copilot@github.com>

* fix column name

---------

Co-authored-by: Copilot <copilot@github.com>
2026-04-29 15:48:15 +00:00
renovate[bot] 7ef7ecec5b chore(deps): update dependency flutter to v3.41.7 (#28124)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-29 10:15:40 -05:00
Alex bc4abd18e4 feat: update iOS CI/CD with FUTO build credential (#28146)
* update email

* Update fastfile

* use different apple id

* debug build

* build only
2026-04-29 09:06:35 -05:00
Peter Ombodi b74cfd4424 fix(mobile): suppress asset stack UI in trash timeline (#26536)
* fix(mobile): suppress asset stack UI in trash timeline

* refactor(mobile): apply review suggestions

* fix(mobile): hide unstack action in the trash timeline

* fix(mobile): move stack indicator out of asset type icons

---------

Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com>
2026-04-29 17:49:47 +07:00
Yaros 7dc84f56c0 fix(web): double video playback on map timeline (#28090) 2026-04-29 12:11:33 +02:00
Timon 92634f923b refactor(server)!: remove redundant error and statusCode fields from error responses (#28140)
* refactor(server)!: remove redundant error and statusCode fields from error responses

* use enum

* enhance response management

* chore: clean up header

* fix: chaining

* refactor: handle error

* fix e2e tests

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-28 17:54:54 -04:00
Timon 96b6165bd3 refactor(server)!: move correlationId to X-Correlation-ID response header (#28139) 2026-04-28 13:07:39 -04:00
Mees Frensel 2624f3884f fix(web): large files: better handling of asset deletions (#28117) 2026-04-28 18:18:39 +02:00
Timon f9b7ce9407 fix(web): convert shared link expiry to UTC before serialising (#28135) 2026-04-28 16:10:08 +00:00
Timon 013ea37a0d refactor!: change number to integer types (#27912)
* refactor!: change number to integer types

* fix oversight
2026-04-28 11:25:03 -04:00
Mees Frensel b2b4385271 chore(web): refactor people panel (#28136) 2026-04-28 11:22:22 -04:00
Mees Frensel 081c75bb21 fix(web): refresh memories hourly (#28114) 2026-04-28 11:18:51 -04:00
renovate[bot] da337578fb fix(deps): update typescript-projects (#28132)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-28 13:18:29 +02:00
renovate[bot] acf4109171 chore(deps): update dependency exiftool-vendored to v35.18.0 (#28133)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 12:46:54 +02:00
renovate[bot] 66601a1fdc chore(deps): update dependency terragrunt to v1.0.2 (#28125)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 12:00:44 +02:00
renovate[bot] 02ff077367 chore(deps): update prom/prometheus docker digest to e425440 (#28120)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-28 12:00:11 +02:00
renovate[bot] 94bb6c1a5e chore(deps): update dependency @immich/ui to v0.76.2 (#28121)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-27 21:54:12 -05:00
Daniel Dietzler fe9e5afcf4 fix: do not emit AlbumInvite event for owner (#28110) 2026-04-27 17:59:46 +00:00
Yosi Taguri 5e89efba64 fix(ml): handle empty/corrupt images in face detection (#27391)
* fix(ml): handle empty/corrupt images in face detection

When a corrupt or degenerate image with zero-dimension (0 width or 0 height)
reaches the face detection pipeline, insightface's RetinaFace.detect() calls
cv2.resize() with a target size of 0, triggering an OpenCV assertion failure:

  error: (-215:Assertion failed) inv_scale_x > 0 in function 'resize'

This crashes the ML worker and returns a 500 error to the server.

Add an early return in FaceDetector._predict() that checks for zero-dimension
images after decoding and returns empty detection results instead of passing
them to the insightface model.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ml): move empty image validation to request level

Per review feedback, validate image dimensions in the predict endpoint
(returning 400) rather than in each model's _predict method. This
catches all zero-dimension images before they reach any model task.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(ml): resolve mypy strict type error in predict endpoint

Use intermediate `decoded` variable so mypy knows `.width` and `.height`
are accessed on `Image`, not on `Image | str`.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-27 11:14:34 -04:00
Peter Ombodi 5a457d72c9 fix(mobile): delete assets on trash empty, Android (#26070)
* fix(mobile): improve trash sync flow
- trash local assets on remote delete events
- unify remote trash handling and support assetDelete cleanup by remote asset id
- update sync stream tests

* fix(mobile): revert pubspec.lock

* refactor(mobile): remove helper
remove unused columns from results

* refactor(mobile): use remoteIds in getAssetsFromBackupAlbums and remove getAssetsFromBackupAlbumsByRemoteIds
refactor tests

---------

Co-authored-by: Peter Ombodi <peter.ombodi@gmail.com>
2026-04-27 18:46:49 +05:30
Min Idzelis 45ccdb37fb refactor(web): replace asset-viewer listener based face hover with overlay elements (#27400) 2026-04-27 12:08:34 +02:00
Savely Krasovsky 9263e2f2e1 feat(ml): update Intel graphics compiler and compute runtime (#28076)
feat(ml): update Intel graphics compiler and compute runtime to latest versions
2026-04-25 08:49:57 -04:00
Aaron Liu a3ee615c5b chore(ml): update huggingfacehub and pillow (#27552) 2026-04-24 19:44:01 -04:00
Yaros 39cfad7136 feat(mobile): action bottom sheet on map timeline (#27515) 2026-04-24 09:30:10 -05:00
renovate[bot] 350056dd1a fix(deps): update dependency uuid to v14 [security] (#28046)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-23 11:24:33 +02:00
Alex f0835d06f8 chore: migrate to FUTO Apple's account (#28020)
* chore: migrate to FUTO Apple's account

* chore: migrate to FUTO Apple's account

* chore: match widget and share extension

* chore: update app share group

* reuse group.app.immich.share
2026-04-22 11:53:20 -05:00
Alex 03b70cf029 fix: jump to timeline on new auto_router update (#28022) 2026-04-22 10:21:48 -05:00
Daniel Dietzler 4bfb8b36c2 chore!: migrate album owner to album_user (#27467)
Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-22 16:52:23 +02:00
renovate[bot] dfacde5af8 fix(deps): update typescript-projects (#28025)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-22 16:49:28 +02:00
Junghwan 317afe9e3b fix(web): normalize underscore locale codes in dynamic language selection (#27900)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-22 13:28:33 +00:00
Jason Rasmussen 1fb5f13237 fix: oauth prompt (#28021) 2026-04-22 09:19:28 -04:00
Luis Nachtigall 793a7054fb fix(mobile): thumbnail transition to asset viewer (#27850) 2026-04-21 15:54:40 -05:00
Luis Nachtigall 3a874dd441 fix(mobile): enable autoplay for motion photos in video viewer (#27961) 2026-04-21 15:53:21 -05:00
Luis Nachtigall 3dc7dc93d8 fix(mobile): clear local data on forced logout (#27957) 2026-04-21 15:52:00 -05:00
Yaros 70397dc5a6 fix(mobile): zero exposure (#28017) 2026-04-21 15:47:27 -05:00
Jason Rasmussen a16d233a0c chore(web): sort imports (#27922)
* feat: sort imports

* fix: something?
2026-04-21 14:51:38 -04:00
Daniel Dietzler bb0872afef chore: upgrade sql-tools (#27885) 2026-04-21 17:42:27 +00:00
Freddie Floydd b9ca68f6e4 chore(web): rename components to PascalCase (#28013)
chore: rename components to PascalCase
2026-04-21 12:29:42 -04:00
Daniel Dietzler 837305da7e chore: un-skip tests (#28012) 2026-04-21 12:08:23 -04:00
Daniel Dietzler e20fb44142 fix: web navigation/animation regression (#28011) 2026-04-21 14:51:37 +00:00
renovate[bot] c2786978cd fix(deps): update typescript-projects (#28008)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-21 15:29:34 +02:00
renovate[bot] 312bb91a4f chore(deps): update github-actions (#28005)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:35:55 +02:00
renovate[bot] c1934b904c chore(deps): update dependency opentofu to v1.11.6 (#27999)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:31:16 +02:00
renovate[bot] 47752d158a fix(deps): update dependency @mapbox/mapbox-gl-rtl-text to v0.4.0 (#28007)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:25:30 +02:00
renovate[bot] 6267322b9c chore(deps): update dependency exiftool-vendored to v35.17.0 (#28004)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 13:08:12 +02:00
renovate[bot] 93c3cd49f3 chore(deps): update node.js to v24.15.0 (#28006)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 09:47:32 +00:00
renovate[bot] f52825ab08 chore(deps): update prom/prometheus docker digest to 5550dc6 (#27998)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 11:23:24 +02:00
renovate[bot] d74dc74f92 chore(deps): update dependency terragrunt to v1.0.1 (#28002)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-21 11:23:07 +02:00
Luis Nachtigall 539a39ae49 refactor(mobile): Migrate durationInSeconds to durationMs (#26615) 2026-04-20 23:28:11 -04:00
Daniel Dietzler f68cd424a7 chore: tags styling (#27984) 2026-04-20 22:06:43 -05:00
Alex 20c0cc7e73 fix: show neon light (#27994) 2026-04-20 20:12:54 -04:00
Aki Hakune be1b9a5f67 feat(server): add MPO file type support (#27963)
* feat(server): add MPO file type support

* fix(server): document description error
2026-04-20 17:45:53 -04:00
shenlong d9011c0829 refactor: test organisation and service test (#27991)
* refactor: test organisation

# Conflicts:
#	mobile/test/unit/utils/editor_test.dart

* regroup hash_service_test

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-20 17:45:20 -04:00
shenlong f909648bce chore: pump flutter to 3.41.7 (#27990)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-20 14:51:27 +00:00
Min Idzelis c78b1d8ab4 fix(web): prevent interaction with detail panel behind person side panel (#27309) 2026-04-20 15:26:06 +02:00
Jason Rasmussen 94a34436a3 chore: remove unused packages & code (#27925) 2026-04-20 08:39:46 -04:00
shenlong 0eef15a3ab chore(mobile): minor dependency updates (#27949)
* chore: minor dependency updates

* regenerate pod and remove unused imports

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-19 11:56:39 -05:00
shenlong 6982896549 feat: android periodic work manager task (#23563)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-19 11:55:07 -05:00
Matthew Momjian 2c812a2561 fix(docs): helmet file affected containers (#27939)
fix helmet file
2026-04-18 12:19:39 -04:00
Mert 0b1188e42e chore(server): separate ffmpeg arguments (#27937)
separate arguments
2026-04-18 15:33:44 +00:00
Freddie Floydd be20cd2bf9 chore(web): bump svelte-check version to silence big warning stack trace (#27935)
chore: bump svelte-check version to silence big warning log
2026-04-18 14:42:47 +00:00
LJspice b8591cb591 feat(server): add OIDC logout URL override option (#27389)
* feat(server): add OIDC logout URL override option
- Added toggle and field consistent with existing mobile redirect URI override.
- Existing auto-discovery remains default.
- Update tests and docs for new feature.

* fix(server): changes from review for OIDC logout URL override
- Rename 'logoutUri' to 'endSessionEndpoint'
- Remove toggle, just use override if provided
- Moved field in settings UI
2026-04-18 04:18:21 +00:00
Freddie Floydd 384d3a0984 fix(web): fix stale album page load (#27825)
* invalidate album data on album update to fix stale page load

* invalidate album data on album update to fix stale page load

* factor out callback, make async and await invalidate

* chore: formatting

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-17 21:24:33 -04:00
Freddie Floydd 03af669856 refactor(web): co-locate single-use components in /routes (#27921)
* co-locate single use components to /routes

* revert accidentally changed paths

* fix mangled path

* fmt

* fix accidentally moved multi-use components
2026-04-17 21:21:36 -04:00
renovate[bot] b0e4850d76 chore(deps): update dependency flutter to v3.41.6 (#27915)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-18 05:14:44 +05:30
Freddie Floydd 36ebcaf00c fix(web): compute hashes for uploads in chunks (#27878)
* add @noble/hashes as a dep for web

* hash files in chunks

* drop old reference to crypto in test code

* use web worker for file hashing
2026-04-17 19:08:46 -04:00
shenlong 7a86f2b7b9 chore: remove stale mobile/.isar submodule entry (#27913)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-18 04:29:13 +05:30
sparsh985 55f2b3b6a0 feat(server): add configurable OAuth prompt parameter (#26755)
* feat(server): add configurable OAuth prompt parameter

Add a `prompt` field to the OAuth system config, allowing admins to
configure the OIDC `prompt` parameter (e.g. `select_account`, `login`,
`consent`). Defaults to empty string (no prompt sent), preserving
backward compatibility.

This is useful for providers like Google where users want to be prompted
to select an account when multiple accounts are signed in.

Discussed in #20762

* chore: regenerate OpenAPI spec and clients for OAuth prompt field

* Adding e2e test cases

* feat: web setting

* feat: docs

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-17 21:20:07 +00:00
shenlong fd5e8d6521 chore: pump auto_route (#27876)
* chore: pump auto_route

* make build

* chore: use drift from pubdev (#27877)

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-17 20:28:36 +00:00
Freddie Floydd 6798d5df32 fix(server): require at least one field to be set when updating memory (#27842)
* add zod util to require one field is set in some schemas. appy to update memory endpoint

* add test
2026-04-17 20:18:48 +00:00
Min Idzelis 9d33853544 fix(web): respect abort signal after timeline bucket fetches (#27563)
Change-Id: I4bf7c7458883b50bd21484b1029d62526a6a6964
2026-04-17 16:18:14 -04:00
bo0tzz a46e46452c fix: run profile picture through thumbnail pipeline (#27890)
* fix: run profile picture through thumbnail pipeline

* fix: format
2026-04-17 16:15:59 -04:00
santanoce dbf30b77bf feat(server): added backchannel logout api endpoint (#26235)
* feat(server): added backchannel logout api endpoint

* test(server): fixed e2e tests

* fix(server): fixed suggested changes by reviewer

* feat(server): created function invalidateOAuth

* fix(server): fixed session.repository.sql

* test(server): added unit tests for backchannelLogout function

* test(server): added e2e tests for oidc backchnnel logout

* docs(server): added documentation on backchannel logout url

* docs(server): fixed typo

* feat(server): minor improvements of the oidc backchannel logout

* test(server): fixed tests after merge with main

* fix(server): fixed e2e test file

* refactor(server): tiny refactor of validateLogoutToken

* chore: cleanup

* fix: tests

* fix: make jwks extractable

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-17 18:45:33 +00:00
bo0tzz 8afca348ff fix: sanitize filenames before adding to zip (#27893)
* fix: sanitize filenames before adding to zip

* fix: lints

* chore: drop split()
2026-04-17 13:05:53 -04:00
shenlong 2070f775d6 refactor: remove riverpod generator (#27874)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-17 20:01:44 +05:30
bo0tzz a456a05052 chore: make filesystem backup docs even more explicit (#27013) 2026-04-17 15:42:40 +02:00
Sergey Katsubo b7eff33f90 chore(web): refactor date section of asset viewer (#24514)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-17 12:56:39 +00:00
Mees Frensel 18c0228f1b fix(web): remove json title from duplicate tool thumbnail (#27886) 2026-04-17 08:37:04 -04:00
Jason Rasmussen 2f8be45fe0 chore!: remove /api/server/theme endpoint (#27880)
chore: remove server/theme endpoint
2026-04-17 08:30:03 -04:00
Daniel Dietzler 41968fdcac feat: cache shared link (#27889) 2026-04-17 08:17:39 -04:00
Yaros 79c392ceba fix(docs): instructions on how to use local immich ui (#27813) 2026-04-17 12:29:14 +02:00
Mees Frensel 8fbeb64c59 fix(web): use event for zooming out after opening face editor (#27789) 2026-04-17 12:14:24 +02:00
Mees Frensel 7d181f0686 fix!: set duration to null when not present (#26982) 2026-04-17 11:57:10 +02:00
shenlong 2172dde7dc chore: remove immich lint (#27873)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-16 15:55:40 -05:00
Luis Nachtigall fce220b1d7 chore(mobile): update special format column detection (#27867) 2026-04-16 14:01:59 -04:00
shenlong 2a47c35eb7 chore: pump flutter to 3.41.6 (#27834)
* chore: pump flutter to 3.41.6

* more isar cleanup

* ignore experimental use for TableMigration

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-16 22:26:46 +05:30
Jason Rasmussen 6aadb7b5bd feat: dynamic languages (#27869)
Co-authored-by: xantin <github@xantin.be>
2026-04-16 12:37:37 -04:00
renovate[bot] 88bce52042 fix(deps): update dependency jose to v6 (#27862)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-16 18:36:07 +02:00
Timothy Dobras d046f16860 fix(oauth): normalize email claim to lowercase and trim before account lookup and registration (#26841)
* fix(oauth): normalize email claim to lowercase before account lookup and registration

* test(auth): add test for OAuth email case normalization

* chore: clean up

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-16 15:41:42 +00:00
renovate[bot] 88815a0345 chore(deps): update base-image to v202604141125 (major) (#27858)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 15:40:40 +00:00
Jason Rasmussen 57212f29bf chore: bump (#27866) 2026-04-16 11:32:33 -04:00
renovate[bot] 95fa8fbdab chore(deps): update machine-learning (#26970)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 11:32:08 -04:00
renovate[bot] 687b7cad6f chore(deps): update dependency terragrunt to v1 (#27860)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 10:36:15 -04:00
Steven Massaro ac2ebcee37 chore: improve randomness of /search/random endpoint (#27531) 2026-04-16 14:36:05 +00:00
bo0tzz 3356e81c85 fix!: do not allow insecure oauth requests by default (#27844)
* fix!: do not allow insecure oauth requests by default

* fix: format

* fix: make open-api

* fix: tests

* nit: casing

* chore: migration to allow insecure if current oauth uses http
2026-04-16 10:11:58 -04:00
renovate[bot] 9c642bd6fc chore(deps): update github-actions (#27857)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 15:29:48 +02:00
bo0tzz 9da0cb3cf4 chore: link to PR template in auto-close message (#27756) 2026-04-16 09:14:36 -04:00
renovate[bot] 4ff6cca4da fix(deps): update dependency pillow to >=12.2,<12.3 [security] (#27773)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:13:39 -04:00
renovate[bot] 2b7ae4981f chore(deps): update dependency pytest to v9.0.3 [security] (#27777)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 09:12:49 -04:00
renovate[bot] e63df4121a chore(deps): update dependency @types/node to ^24.12.2 (#27856)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 13:12:38 +00:00
renovate[bot] 03b4ab2935 fix(deps): update dependency simple-icons to v16 (#27855)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 13:07:35 +00:00
bo0tzz facd3bd331 fix: oauth issuerUrl validation (#27848) 2026-04-16 09:06:55 -04:00
renovate[bot] 20ddf2e7d2 fix(deps): update dependency nestjs-cls to v6 (#27852)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 13:45:42 +02:00
renovate[bot] 7f0025b3fc chore(deps): update dependency @types/nodemailer to v8 (#27851)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-16 13:28:23 +02:00
Mees Frensel 60f4dedb29 chore(web): small fixes for location picker modal (#27822) 2026-04-16 11:49:24 +02:00
Min Idzelis d5d2ebd9bf fix(web): close edit faces panel on Escape key press (#27519)
Move `showEditFaces` state to `assetViewerManager` so the edit faces
panel open/close state is globally accessible. Add Escape key handling
to `PersonSidePanel` that closes the assign-face sub-panel first, then
the edit faces panel. Guard the asset viewer's global Escape-to-close
action so it doesn't fire while either face panel is open.

Change-Id: I0c947fe0758aef0eac473f4cc72f6a3b6a6a6964
2026-04-15 20:22:20 -05:00
Yaros 37abbeba52 fix(mobile): readonly redirect when not logged in (#27728) 2026-04-15 20:20:08 -05:00
Min Idzelis 50557002b7 fix(web): stale adaptive image when original overlays preview (#27621)
AdaptiveImage stacks quality layers (thumbnail → preview → original) as they load. Without compositor layer promotion on the container, the browser could render a stale frame when the original-quality layer was overlaid on top of the preview-quality layer.

Add `will-change: transform` as a class on AdaptiveImage's root element so it gets a dedicated compositor layer from first paint. This also subsumes the imperative `style.willChange = 'transform'` that zoomImageAction was applying to the same element (the zoom target from photo-viewer is the AdaptiveImage root), so drop that now-redundant code.

Change-Id: Icd866a2bb5a5fce299c36404547fa0546a6a6964
2026-04-15 20:19:18 -05:00
Min Idzelis 4aa31d38bf fix(web): svelte regression - cancel video preview fetch when bind:this is cleared early (#27713)
fix(web): cancel video preview fetch when bind:this is cleared early

In Svelte 5.53.9, `bind:this` is now cleared earlier in the unmount
sequence ("better bind:this cleanup timing"). The video thumbnail's
$effect was relying on the old order to read the bound `player` element
and clear its `src` to abort the in-flight `/api/assets/{id}/video/playback`
range request — but the bind is now `undefined` by the time the effect
runs, so the cleanup is silently skipped. The detached <video> keeps
its src, and Firefox does not abort an in-flight media fetch when the
element is detached/GC'd. Long-lived 206 range requests then saturate
Firefox's 6-connection HTTP/1.1 per-host limit and freeze the timeline
(see #27585).

Capture the player reference inside the effect and tear down via the
effect cleanup return — Svelte runs the prior cleanup (with the captured
ref) before `bind:this` is cleared. Use the canonical
`pause() / removeAttribute('src') / load()` sequence which actually aborts
the fetch in Firefox, even on a detached element.

Fixes #27585

Change-Id: I4d9fba859955f5c54f603c345e61d4206a6a6964
2026-04-15 20:18:59 -05:00
Min Idzelis 3d8df74b43 refactor(web): turn thumbhash action into Thumbhash component (#27741)
refactor(web): extract thumbhash canvas into Thumbhash component

Change-Id: If78955bed48b6e690df398e5e2ae61fb6a6a6964
2026-04-15 20:18:49 -05:00
renovate[bot] 2ff9f95527 chore(deps): update dependency python-multipart to v0.0.26 [security] (#27838)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-15 19:06:27 -04:00
Jason Rasmussen a69eecf3bc chore!: remove without assets (#27835)
* chore!: remove without assets

* fix: linting and e2e

---------

Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-15 18:34:22 -04:00
Jason Rasmussen 4ffa26c969 feat: auth logout page (#27831)
* feat: auth logout page

* feat: skip login if already logged in
2026-04-15 16:33:52 -04:00
Jason Rasmussen ac06514db5 feat: album map markers endpoint (#27830) 2026-04-15 15:58:34 -04:00
Jason Rasmussen 792cb9148b chore!: rename API key schemas (#27828)
chore!: rename API schemas
2026-04-15 15:58:26 -04:00
Daniel Dietzler 8ee5d3039a chore!: remove deviceId and deviceAssetId (#27818)
chore: remove deviceId and deviceAssetId
2026-04-15 15:00:33 -04:00
Jason Rasmussen d410131312 chore!: remove old timeline sync endpoints (#27804) 2026-04-15 13:58:48 -04:00
bo0tzz 5334a6254a fix: make web build stage deterministic (#27823) 2026-04-15 19:31:23 +02:00
shenlong 79fccdbee0 refactor: yeet old timeline (#27666)
* refactor: yank old timeline

# Conflicts:
#	mobile/lib/presentation/pages/editing/drift_edit.page.dart
#	mobile/lib/providers/websocket.provider.dart
#	mobile/lib/routing/router.dart

* more cleanup

* remove native code

* chore: bump sqlite-data version

* remove old background tasks from BGTaskSchedulerPermittedIdentifiers

* rebase

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-15 23:00:27 +05:30
Brandon Wees 6dd6053222 feat: mobile editing (#25397)
* feat: mobile editing

fix: openapi patch

this sucks :pepehands:

chore: migrate some changes from the filtering PR

chore: color tweak

fix: hide edit button on server versions

chore: translation

* chore: code review changes

chore: code review

* sealed class

* const constant

* enum

* concurrent queries

* chore: major cleanup to use riverpod provider

* fix: aspect ratio selection

* chore: typesafe websocket event parsing

* fix: wrong disable state for save button

* chore: remove isCancelled shim

* chore: cleanup postframe callback usage

* chore: clean import

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
2026-04-15 09:26:40 -05:00
Mees Frensel 8454cb2631 chore: exit open-api script on errors (#27815) 2026-04-15 10:09:51 -04:00
Daniel Dietzler 603fc7401f fix: redirect original (#27759) 2026-04-15 15:43:09 +02:00
Jason Rasmussen ed70e0febf chore: stop using legacy partner create endpoint (#27806) 2026-04-15 16:16:43 +05:30
Jason Rasmussen 5f5e3344d5 chore!: remove unused token response param (#27805) 2026-04-15 00:58:00 -04:00
Brandon Wees 6da2d3d587 chore!: remove getRandom api endpoint (#27780)
* chore!: remove getRandom api endpoint

* chore: sync openapi

* fix: test

* chore: more cleanup
2026-04-14 21:32:12 -04:00
Jason Rasmussen 41d2d84b21 chore!: remove deprecated env variables (#27802) 2026-04-14 21:30:31 -04:00
Jason Rasmussen 6ba17bb86f refactor!: remove my shared link dto (#27023)
refactor!: remove deprecated shared link apis
2026-04-14 20:58:02 -04:00
Jason Rasmussen e1a84d3ab6 refactor!: remove replace asset (#27022) 2026-04-14 20:21:05 -04:00
Timon 7d8f843be6 refactor!: migrate class-validator to zod (#26597) 2026-04-14 23:39:03 +02:00
OdinOxin 3753b7a4d1 feat: sort users alphabetically when adding to album (#27731) 2026-04-14 21:21:22 +02:00
Jonathan Jogenfors 84a1fb27ca feat(web): lazy load library and server statistics (#26406)
* feat: add offline library statistics

* fix comments

* feat: add offline library statistics

* fix comments

* fix Daniel's comments

* fix Daniels comment 2
2026-04-14 12:54:09 -04:00
Yaros 81780b0cc0 fix(web): add partner photo to album from multiselect (#27767)
* fix(web): add partner photo to album

* chore: fix formatting

* fix: run-job assets

---------

Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-14 16:48:39 +00:00
Min Idzelis 5e81a5a054 feat(web): remove delay from Skeleton (#27580)
Change-Id: I95a37f1af832c005a8f009d6f07df8ac6a6a6964
2026-04-14 12:47:37 -04:00
Miguel Raposo e4e2f586b5 fix(server): render storage template date/time tokens in UTC (#24350) (#26917) 2026-04-14 18:45:14 +02:00
OdinOxin a001adf14a feat: filter users on share (#27732)
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-14 16:43:45 +00:00
Daniel Dietzler 136814540a fix: asset multi select download shortcut (#27784) 2026-04-14 12:29:55 -04:00
Jason Rasmussen fed5cc1ae1 feat: upgrade immich/ui (#27792) 2026-04-14 16:18:12 +00:00
Yaros 641ab51b80 fix(web): selection clearing on preview (#27702)
* fix(web): selection clearing on preview

* chore: remove unnecessary checks
2026-04-14 10:06:32 -05:00
Yaros 3b47ca1c37 fix(mobile): add keys for person tiles in search (#27689)
* fix(mobile): key for person tiles in search

* chore: add key to avatar

* chore: use simple personId

* chore: rename key in person page
2026-04-14 10:05:09 -05:00
Jason Rasmussen 8fb2c7755d feat: commands (#27546) 2026-04-14 09:34:46 -04:00
Jason Rasmussen 1ba0989e15 refactor: auth manager (#27638) 2026-04-14 08:49:24 -04:00
renovate[bot] daed3f0966 chore(deps): update dependency @sveltejs/kit to v2.57.1 [security] (#27762)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 11:50:25 +02:00
renovate[bot] 46d612ad8c chore(deps): update dependency nodemailer to v8.0.5 [security] (#27623)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 11:49:26 +02:00
renovate[bot] 513dead2c2 chore(deps): update dependency @nestjs/core to v11.1.18 [security] (#27544)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 09:49:03 +00:00
renovate[bot] ca006c1569 fix(deps): update typescript-projects (#27573)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-14 11:41:09 +02:00
renovate[bot] 4e8e8304fd fix(deps): update react-email monorepo (major) (#27572)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-14 11:40:58 +02:00
Nicolas-micuda-becker d377d2e145 fix(web): center images in RTL layouts (#27678) (#27753) 2026-04-13 13:29:35 -05:00
shenlong 9c9feddf7d refactor: folder page to use new models (#27657)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2026-04-13 12:19:44 -05:00
Andreas Heinz bfcf34d8b5 feat(web): persist state of file path information in details panel (#27770)
feat(enhancement): persist state of file path info in details panel
2026-04-13 12:18:34 -05:00
github-actions 95e57a24cb chore: version v2.7.5 2026-04-13 14:27:31 +00:00
Weblate (bot) eada662981 chore(web): update translations (#27589)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/de_CH/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/id/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/lv/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/th/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Aliyss Snow <mangoworksbeta@gmail.com>
Co-authored-by: Bannawat Thongbai <kaji.kanlapat99@gmail.com>
Co-authored-by: Carlo Beltrame <weblate@pendantmusic.ch>
Co-authored-by: Dawnsink <dai@cosmopeace.com>
Co-authored-by: Edmundas <edmius@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Jeppe Nellemann <jepnel@proton.me>
Co-authored-by: Linerly <linerly@proton.me>
Co-authored-by: Link Notig <TestMailProtonWhyNot@protonmail.com>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: Mārtiņš Bruņenieks <martinsb@gmail.com>
Co-authored-by: Osama <laptooxz@proton.me>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: UDP <udp@users.noreply.hosted.weblate.org>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Volodymyr Sakharov <savolodya@gmail.com>
Co-authored-by: Vykintas Vyšniauskas <vykintasv@gmail.com>
Co-authored-by: WellsTsai <dan50907@gmail.com>
Co-authored-by: brainheart95 <josephdm4d@gmail.com>
Co-authored-by: dvbthien <dvbthien@users.noreply.hosted.weblate.org>
Co-authored-by: 이찬웅 <lcw7527@gmail.com>
2026-04-13 14:25:01 +00:00
Zack Pollard 352f6ecc28 fix(server): add rate limit and deduplication to version check (#27747) 2026-04-13 12:35:46 +00:00
github-actions bee49cef02 chore: version v2.7.4 2026-04-10 16:32:26 +00:00
shenlong 6d0c6a4008 chore: pump cronet version (#27685)
Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-10 16:29:05 +00:00
Luis Nachtigall 8a975e5ea9 refactor(mobile): cleanup iOS image loading pipeline (#27672)
* refactor: replace DispatchQueue + DispatchSemaphore with OperationQueue for image processing

* implement RequestRegistry and UnfairLock for managing cancellable requests

* implement requests registry for local and remote image processing

* remove Cancellable protocol and cancel method from request registry

* refactor: introduce ImageRequest base class with unified cancellation and finish helpers

* refactor: add get method to RequestRegistry and streamline request removal in image processing

* add guard to cancel to prevent double onCancel calls

* fix duplicate code merge issue

* refactor(ios): enhance finish method to return callback status

* remove unfitting methods form ImageRequest.swift and fix memory issue

* revert bad merge

* refactor(ios): resolve cancellation issues

* refactor(ios): streamline image request completion handling

* add return statements

* refactor(ios): simplify image request cancellation and registry handling

---------

Co-authored-by: Alex <alex.tran1502@gmail.com>
2026-04-10 10:56:35 -05:00
Luis Nachtigall d39e7da10d fix(mobile): fix flutter cache eviction on thumbnails (#27663)
* fix: add markFinished parameter to loadRequest and loadCodecRequest methods

* update loadRequest and loadCodecRequest methods to use isFinal

* Apply suggestions from code review

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

* remove redundant check

* fix: ensure isFinished is set correctly during cache eviction

* formatting

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2026-04-10 10:28:55 -05:00
Daniel Dietzler bc400d68ac chore: move .tsbuildinfo file to dist folder (#27682) 2026-04-10 16:02:25 +02:00
renovate[bot] d7f038ec60 chore(deps): update dependency eslint-plugin-unicorn to v64 (#27575)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: Daniel Dietzler <mail@ddietzler.dev>
2026-04-10 10:23:42 +00:00
Mees Frensel 26957f37ce fix(server): hide original filename when not showing metadata (#27581) 2026-04-10 12:07:18 +02:00
github-actions 3254d31cd2 chore: version v2.7.3 2026-04-09 17:51:40 +00:00
Jason Rasmussen 7b269d1638 fix: ssr open graph tags (#27639)
fix: SSR open graph tags
2026-04-09 12:16:41 -04:00
Luis Nachtigall b5bed02300 fix(mobile): get provider refs before async gaps in backup page (#27597)
* fix(mobile): get provider refs before async gaps in backup page

* fix(mobile): use previously created provider refs in start backup function
2026-04-08 20:55:53 -05:00
Zack Pollard 5553910236 fix(web): don't cache empty search results for people search (#27632) 2026-04-09 02:33:04 +01:00
Zack Pollard 8d67c1f820 fix(server): people search not showing for 3 or less characters (#27629)
Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2026-04-09 01:56:07 +01:00
Matthew Momjian ed0ec30917 fix(docs): updated docker deprecation link (#27633)
new link
2026-04-08 20:33:11 -04:00
Luis Nachtigall 2b0f6c9202 fix(mobile): improve image load cancellation handling (#27624)
fix(image): improve image load cancellation handling
2026-04-08 17:23:42 -04:00
André Erasmus 55ab8c65b6 fix(server): avoid false restore failures on large database imports (#27420)
* fix(server): increase restore health check timeout and reject with Error

* chore: clean up

---------

Co-authored-by: André Erasmus <25480506+NoBadDays@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jason@rasm.me>
2026-04-08 16:03:41 -04:00
Cullen Jennings 781d568f29 fix(docs): typo 'Start rating' to 'Star rating' (#27606) 2026-04-08 18:25:45 +00:00
Zack Pollard 6a361dae72 fix(server): use randomized cron for version check scheduling (#27626)
Also removes unnecessary rate limit
2026-04-08 19:15:38 +01:00
renovate[bot] 64766c8c06 chore(deps): update github-actions (#27560)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-04-08 17:32:54 +02:00
github-actions 6a63e814a5 chore: version v2.7.2 2026-04-07 20:58:38 +00:00
Jason Rasmussen 6441c3b77c fix: server build (#27599) 2026-04-07 20:53:04 +00:00
1782 changed files with 74404 additions and 82806 deletions
+6
View File
@@ -6,6 +6,12 @@ mobile/openapi/**/*.dart linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
mobile/android/**/*.g.kt -diff -merge
mobile/android/**/*.g.kt linguist-generated=true
mobile/ios/**/*.g.swift -diff -merge
mobile/ios/**/*.g.swift linguist-generated=true
mobile/lib/**/*.drift.dart -diff -merge
mobile/lib/**/*.drift.dart linguist-generated=true
-1
View File
@@ -1 +0,0 @@
24.14.1
+1 -1
View File
@@ -51,7 +51,7 @@ jobs:
run: |
gh api graphql \
-f prId="$NODE_ID" \
-f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \
-f query='
mutation CommentAndClosePR($prId: ID!, $body: String!) {
addComment(input: {
+39 -15
View File
@@ -51,14 +51,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -73,24 +73,25 @@ jobs:
needs: pre-job
permissions:
contents: read
# Skip when PR from a fork
if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
pull-requests: write
if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
ref: ${{ inputs.ref }}
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Create the Keystore
if: ${{ !github.event.pull_request.head.repo.fork }}
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
@@ -103,7 +104,7 @@ jobs:
- name: Restore Gradle Cache
id: cache-gradle-restore
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
@@ -121,7 +122,7 @@ jobs:
cache: true
- name: Setup Android SDK
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
with:
packages: ''
@@ -144,23 +145,46 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
flutter build apk --debug --split-per-abi --target-platform android-arm64
flutter build apk --release
fi
- name: Publish Android Artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
id: upload-apk
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
- name: Comment APK download link on PR
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
env:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'mobile-android-apk'
message: |
📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
Download: ${{ env.APK_URL }}
<details>
<summary>QR code</summary>
<img src="https://api.qrserver.com/v1/create-qr-code/?size=240x240&data=${{ env.APK_URL }}" alt="QR code" />
</details>
Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
if: github.ref == 'refs/heads/main'
with:
path: |
@@ -210,7 +234,7 @@ jobs:
working-directory: ./mobile
- name: Setup Ruby
uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: '3.3'
bundler-cache: true
@@ -291,7 +315,7 @@ jobs:
security delete-keychain build.keychain || true
- name: Upload IPA artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ios-release-ipa
path: mobile/ios/Runner.ipa
+2 -2
View File
@@ -19,9 +19,9 @@ jobs:
actions: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check out code
+1 -1
View File
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
uses: oasdiff/oasdiff-action/breaking@1f38ea5ea0b4a2e4e49901c3bcdf4386a05e9ea1 # v0.0.37
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
+13 -23
View File
@@ -31,35 +31,25 @@ jobs:
working-directory: ./cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './cli/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- run: pnpm install --frozen-lockfile
- run: pnpm build
- run: pnpm publish --provenance --no-git-checks
- name: Publish
if: ${{ github.event_name == 'release' }}
run: mise run ci-publish
docker:
name: Docker
@@ -71,9 +61,9 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
@@ -89,7 +79,7 @@ jobs:
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
registry: ghcr.io
@@ -115,7 +105,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
- name: Build and push image
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64
+1 -1
View File
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:
+5 -5
View File
@@ -44,9 +44,9 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout repository
@@ -57,7 +57,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -70,7 +70,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
# ️ Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@@ -83,6 +83,6 @@ jobs:
# ./location_of_script_within_repo/buildscript.sh
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
with:
category: '/language:${{matrix.language}}'
+9 -9
View File
@@ -23,14 +23,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -60,7 +60,7 @@ jobs:
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -90,7 +90,7 @@ jobs:
suffix: ['']
steps:
- name: Login to GitHub Container Registry
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
@@ -132,7 +132,7 @@ jobs:
suffixes: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}'
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions:
contents: read
actions: read
@@ -155,7 +155,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions:
contents: read
actions: read
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
@@ -189,6 +189,6 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
+9 -15
View File
@@ -21,14 +21,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -54,9 +54,9 @@ jobs:
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -64,17 +64,11 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
fetch-depth: 0
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './docs/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Run install
run: pnpm install
@@ -86,7 +80,7 @@ jobs:
run: pnpm build
- name: Upload build output
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: docs-build-output
path: docs/build/
+12 -10
View File
@@ -20,16 +20,16 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
run: echo 'The triggering workflow did not succeed' && exit 1
- name: Get artifact
id: get-artifact
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.token.outputs.token }}
script: |
@@ -48,7 +48,7 @@ jobs:
return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
with:
@@ -119,9 +119,9 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -131,11 +131,13 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
github_token: ${{ steps.token.outputs.token }}
- name: Load parameters
id: parameters
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
with:
@@ -147,7 +149,7 @@ jobs:
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- name: Download artifact
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
with:
@@ -211,7 +213,7 @@ jobs:
run: 'mise run //deployment:tf apply'
- name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
token: ${{ steps.token.outputs.token }}
+6 -4
View File
@@ -17,9 +17,9 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -29,7 +29,9 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
github_token: ${{ steps.token.outputs.token }}
- name: Destroy Docs Subdomain
env:
@@ -42,7 +44,7 @@ jobs:
run: 'mise run //deployment:tf destroy -- -refresh=false'
- name: Comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
with:
token: ${{ steps.token.outputs.token }}
number: ${{ github.event.number }}
+10 -16
View File
@@ -14,41 +14,35 @@ jobs:
contents: write
pull-requests: write
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: 'Checkout'
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
- name: Commit and push
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
with:
default_author: github_actions
message: 'chore: fix formatting'
- name: Remove label
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
if: always()
with:
github-token: ${{ steps.generate-token.outputs.token }}
+3 -3
View File
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
workflow_call:
secrets:
PUSH_O_MATIC_APP_ID:
PUSH_O_MATIC_APP_CLIENT_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
@@ -31,9 +31,9 @@ jobs:
- name: Generate a token
id: generate_token
if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Find translation PR
+2 -2
View File
@@ -14,9 +14,9 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Require PR to have a changelog label
+2 -2
View File
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
+17 -22
View File
@@ -36,7 +36,7 @@ jobs:
permissions:
pull-requests: write
secrets:
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
@@ -48,32 +48,27 @@ jobs:
version: ${{ steps.output.outputs.version }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
- id: token
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
token: ${{ steps.generate-token.outputs.token }}
token: ${{ steps.token.outputs.token }}
persist-credentials: true
ref: main
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
# TODO move to mise
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Bump version
env:
@@ -86,7 +81,7 @@ jobs:
- name: Commit and tag
id: push-tag
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
with:
default_author: github_actions
message: 'chore: version ${{ steps.output.outputs.version }}'
@@ -124,9 +119,9 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
@@ -142,7 +137,7 @@ jobs:
github-token: ${{ steps.generate-token.outputs.token }}
- name: Create draft release
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
with:
draft: true
tag_name: ${{ needs.bump_version.outputs.version }}
+8 -8
View File
@@ -14,12 +14,12 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
@@ -32,12 +32,12 @@ jobs:
pull-requests: write
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
with:
github-token: ${{ steps.token.outputs.token }}
script: |
@@ -48,14 +48,14 @@ jobs:
name: 'preview'
})
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
message: 'PRs from forks cannot have preview environments.'
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
+10 -12
View File
@@ -19,29 +19,27 @@ jobs:
working-directory: ./open-api/typescript-sdk
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './open-api/typescript-sdk/.nvmrc'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install deps
run: pnpm install --frozen-lockfile
- name: Build
run: pnpm build
- name: Publish
run: pnpm publish --provenance --no-git-checks
+5 -5
View File
@@ -20,14 +20,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -49,9 +49,9 @@ jobs:
working-directory: ./mobile
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
+183 -207
View File
@@ -17,14 +17,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -34,13 +34,17 @@ jobs:
- 'web/**'
- 'i18n/**'
- 'open-api/typescript-sdk/**'
- 'pnpm-lock.yaml'
server:
- 'server/**'
- 'pnpm-lock.yaml'
cli:
- 'cli/**'
- 'open-api/typescript-sdk/**'
- 'pnpm-lock.yaml'
e2e:
- 'e2e/**'
- 'pnpm-lock.yaml'
mobile:
- 'mobile/**'
machine-learning:
@@ -63,9 +67,9 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -74,28 +78,14 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run package manager install
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run small tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
cli-unit-tests:
name: Unit Test CLI
needs: pre-job
@@ -108,9 +98,9 @@ jobs:
working-directory: ./cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -118,31 +108,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './cli/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install && pnpm run build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: pnpm install
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
cli-unit-tests-win:
name: Unit Test CLI (Windows)
needs: pre-job
@@ -155,9 +129,9 @@ jobs:
working-directory: ./cli
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -165,26 +139,33 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './cli/.nvmrc'
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Install deps
run: pnpm install --frozen-lockfile
# Skip linter & formatter in Windows test.
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
web-lint:
name: Lint Web
needs: pre-job
@@ -197,9 +178,9 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -207,28 +188,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
run: mise run //:sdk:install && mise run //:sdk:build
- name: Run pnpm install
run: pnpm rebuild && pnpm install --frozen-lockfile
run: pnpm install --frozen-lockfile
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run svelte checks
run: pnpm check:svelte
if: ${{ !cancelled() }}
web-unit-tests:
name: Test Web
needs: pre-job
@@ -241,9 +216,9 @@ jobs:
working-directory: ./web
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -251,25 +226,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- name: Run npm install
run: pnpm install --frozen-lockfile
- name: Run tsc
run: pnpm check:typescript
if: ${{ !cancelled() }}
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
i18n-tests:
name: Test i18n
needs: pre-job
@@ -279,9 +244,9 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -289,24 +254,25 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './web/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile
- name: Format
run: pnpm --filter=immich-i18n format:fix
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -315,6 +281,7 @@ jobs:
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
@@ -327,9 +294,9 @@ jobs:
working-directory: ./e2e
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -337,30 +304,16 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './e2e/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
- name: Run tsc
run: pnpm check
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
if: ${{ !cancelled() }}
server-medium-tests:
name: Medium Tests (Server)
needs: pre-job
@@ -373,9 +326,9 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -384,19 +337,16 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run pnpm install
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Run medium tests
run: pnpm test:medium
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-medium
run: mise run ci-medium
if: ${{ !cancelled() }}
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
@@ -412,9 +362,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -423,52 +373,63 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './e2e/.nvmrc'
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
- name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test
if: ${{ !cancelled() }}
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
e2e-tests-web:
name: End-to-End Tests (Web)
needs: pre-job
@@ -484,9 +445,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -495,70 +456,85 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version-file: './e2e/.nvmrc'
node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
- name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
- name: Run e2e tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web
if: ${{ !cancelled() }}
- name: Archive e2e test (web) results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Run ui tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui
if: ${{ !cancelled() }}
- name: Archive ui test (web) results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Run maintenance tests
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance
if: ${{ !cancelled() }}
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
- name: Archive Docker logs
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
@@ -566,7 +542,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
mobile-unit-tests:
@@ -578,9 +554,9 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -610,34 +586,24 @@ jobs:
working-directory: ./machine-learning
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Install uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
python-version: 3.11
- name: Install dependencies
run: |
uv sync --extra cpu
- name: Lint with ruff
run: |
uv run ruff check --output-format=github immich_ml
- name: Format with ruff
run: |
uv run ruff format --check immich_ml
- name: Run mypy type checking
run: |
uv run mypy --strict immich_ml/
- name: Run tests and coverage
run: |
uv run pytest --cov=immich_ml --cov-report term-missing
github_token: ${{ steps.token.outputs.token }}
- name: Run ci-unit
run: mise run ci-unit
github-files-formatting:
name: .github Files Formatting
needs: pre-job
@@ -650,9 +616,9 @@ jobs:
working-directory: ./.github
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -660,19 +626,19 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './.github/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Run pnpm install
run: pnpm install --frozen-lockfile
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
@@ -680,9 +646,9 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -701,9 +667,9 @@ jobs:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -711,21 +677,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
- name: Build the app
run: pnpm --filter immich build
- name: Run API generation
run: ./bin/generate-open-api.sh
working-directory: open-api
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
@@ -734,6 +701,7 @@ jobs:
mobile/openapi
open-api/typescript-sdk
open-api/immich-openapi-specs.json
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -742,6 +710,7 @@ jobs:
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
sql-schema-up-to-date:
name: SQL Schema Checks
runs-on: ubuntu-latest
@@ -763,9 +732,9 @@ jobs:
working-directory: ./server
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -773,31 +742,35 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- name: Setup Node
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
node-version-file: './server/.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
github_token: ${{ steps.token.outputs.token }}
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- name: Build the app
run: pnpm build
- name: Run existing migrations
run: pnpm migrations:run
- name: Test npm run schema:reset command works
run: pnpm schema:reset
- name: Generate new migrations
continue-on-error: true
run: pnpm migrations:generate src/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
server/src
- name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -807,16 +780,19 @@ jobs:
echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts
exit 1
- name: Run SQL generation
run: pnpm sync:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-sql-files
with:
files: |
server/src/queries
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
+6 -6
View File
@@ -24,14 +24,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -47,9 +47,9 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Bot review status
@@ -68,6 +68,6 @@ jobs:
permissions: {}
if: always()
steps:
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
+1
View File
@@ -28,4 +28,5 @@ vite.config.js.timestamp-*
.pnpm-store
.devcontainer/library
.devcontainer/.env*
*.tsbuildinfo
*.tsbuildInfo
-3
View File
@@ -1,6 +1,3 @@
[submodule "mobile/.isar"]
path = mobile/.isar
url = https://github.com/isar/isar
[submodule "e2e/test-assets"]
path = e2e/test-assets
url = https://github.com/immich-app/test-assets
+1
View File
@@ -0,0 +1 @@
24.15.0
+5 -13
View File
@@ -13,10 +13,6 @@
"editor.wordBasedSuggestions": "off"
},
"[javascript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
@@ -29,18 +25,14 @@
"editor.formatOnSave": true
},
"[svelte]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "svelte.svelte-vscode",
"editor.formatOnSave": true
"editor.formatOnSave": true,
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
},
"svelte.plugin.svelte.compilerWarnings": {
"state_referenced_locally": "ignore"
},
"[typescript]": {
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.removeUnusedImports": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true
},
-1
View File
@@ -1 +0,0 @@
24.14.1
+24 -1
View File
@@ -7,7 +7,7 @@ run = "vite build"
[tasks.test]
env._.path = "./node_modules/.bin"
run = "vite"
run = "vitest"
[tasks.lint]
env._.path = "./node_modules/.bin"
@@ -27,3 +27,26 @@ run = "prettier --write ."
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
[tasks.ci-publish]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":build" },
"pnpm publish --provenance --no-git-checks",
]
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
{ task = ":test --run" },
]
[tasks.checklist]
run = [
{ task = ":ci-unit" },
]
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
"version": "2.7.1",
"version": "2.7.5",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -20,7 +20,7 @@
"@types/lodash-es": "^4.17.12",
"@types/micromatch": "^4.0.9",
"@types/mock-fs": "^4.13.1",
"@types/node": "^24.12.0",
"@types/node": "^24.12.2",
"@vitest/coverage-v8": "^4.0.0",
"byte-size": "^9.0.0",
"cli-progress": "^3.12.0",
@@ -28,7 +28,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"globals": "^17.0.0",
"mock-fs": "^5.2.0",
"prettier": "^3.7.4",
@@ -66,8 +66,5 @@
"fastq": "^1.17.1",
"lodash-es": "^4.17.21",
"micromatch": "^4.0.8"
},
"volta": {
"node": "24.14.1"
}
}
+6 -6
View File
@@ -4,7 +4,7 @@ import path from 'node:path';
import { setTimeout as sleep } from 'node:timers/promises';
import { describe, expect, it, MockedFunction, vi } from 'vitest';
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk';
import createFetchMock from 'vitest-fetch-mock';
import {
@@ -120,7 +120,7 @@ describe('checkForDuplicates', () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
action: AssetUploadAction.Accept,
id: testFilePath,
},
],
@@ -144,10 +144,10 @@ describe('checkForDuplicates', () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Reject,
action: AssetUploadAction.Reject,
id: testFilePath,
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
reason: Reason.Duplicate,
reason: AssetRejectReason.Duplicate,
},
],
});
@@ -167,7 +167,7 @@ describe('checkForDuplicates', () => {
vi.mocked(checkBulkUpload).mockResolvedValue({
results: [
{
action: Action.Accept,
action: AssetUploadAction.Accept,
id: testFilePath,
},
],
@@ -187,7 +187,7 @@ describe('checkForDuplicates', () => {
mocked.mockResolvedValue({
results: [
{
action: Action.Accept,
action: AssetUploadAction.Accept,
id: testFilePath,
},
],
+2 -4
View File
@@ -1,9 +1,9 @@
import {
Action,
AssetBulkUploadCheckItem,
AssetBulkUploadCheckResult,
AssetMediaResponseDto,
AssetMediaStatus,
AssetUploadAction,
Permission,
addAssetsToAlbum,
checkBulkUpload,
@@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
const results = response.results as AssetBulkUploadCheckResults;
for (const { id: filepath, assetId, action } of results) {
if (action === Action.Accept) {
if (action === AssetUploadAction.Accept) {
newFiles.push(filepath);
} else {
// rejects are always duplicates
@@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
const { baseUrl, headers } = defaults;
const formData = new FormData();
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
formData.append('deviceId', 'CLI');
formData.append('fileCreatedAt', stats.mtime.toISOString());
formData.append('fileModifiedAt', stats.mtime.toISOString());
formData.append('fileSize', String(stats.size));
+1
View File
@@ -19,6 +19,7 @@
"paths": {
"src/*": ["./src/*"],
},
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
"types": ["vitest/globals"]
},
"exclude": ["dist", "node_modules", "vite.config.ts"]
+2 -2
View File
@@ -1,6 +1,6 @@
[tools]
terragrunt = "0.99.5"
opentofu = "1.11.5"
terragrunt = "1.0.3"
opentofu = "1.11.6"
[tasks."tg:fmt"]
run = "terragrunt hclfmt"
+1
View File
@@ -20,6 +20,7 @@ services:
- /tmp
volumes:
- ..:/usr/src/app
# - ../../ui:/usr/src/ui
- pnpm_cache:/buildcache/pnpm_cache
- server_node_modules:/usr/src/app/server/node_modules
- web_node_modules:/usr/src/app/web/node_modules
+2 -2
View File
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
image: prom/prometheus@sha256:dda13e28bf95a5e5ca5b8ed56852006094c1c8e8871d9c9dbeed30aa6e55271f
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -97,7 +97,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
image: grafana/grafana:12.4.2-ubuntu@sha256:78839fe49e1425c02416fa8072591533a72bd9598e563b54a07d78f9e27fb5d3
image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
volumes:
- grafana-data:/var/lib/grafana
-3
View File
@@ -95,6 +95,3 @@ services:
restart: always
healthcheck:
disable: false
volumes:
model-cache:
-1
View File
@@ -1 +0,0 @@
24.14.1
@@ -210,7 +210,7 @@ The provided restore process ensures your database is never in a broken state by
## Filesystem
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:
Immich does not handle filesystem backups for you. You have to arrange these yourself! 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`
+7
View File
@@ -50,6 +50,10 @@ Before enabling OAuth in Immich, a new client application needs to be configured
- `https://immich.example.com/auth/login`
- `https://immich.example.com/user-settings`
3. Configure Backchannel logout URL
If the authentication server supports it, the **Backchannel logout URL** can be specified, and it is of the form: `http://DOMAIN:PORT/api/oauth/backchannel-logout`.
## Enable OAuth
Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
@@ -63,6 +67,8 @@ Once you have a new OAuth client application configured, Immich can be configure
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
| `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) |
| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) |
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
@@ -181,6 +187,7 @@ Configuration of OAuth in Immich System Settings
| Scope | openid email profile immich_scope |
| ID Token Signed Response Algorithm | RS256 |
| Userinfo Signed Response Algorithm | RS256 |
| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ |
| Storage Label Claim | uid |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
@@ -81,7 +81,7 @@ VectorChord is the successor extension to pgvecto.rs, allowing for higher perfor
### Migrating from pgvecto.rs
Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so.
Support for pgvecto.rs has been dropped as of 3.0, hence all users currently using pgvecto.rs should migrate to VectorChord. There are two primary approaches to do so.
The easiest option is to have both extensions installed during the migration:
+3 -3
View File
@@ -80,9 +80,9 @@ 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`
2. Build the `@immich/ui` project via `pnpm run build`
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`)
4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`)
5. Uncomment the import statement in `web/src/app.css` file `@import '../../../ui/packages/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
6. Start up the stack via `make dev`
7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`)
+2
View File
@@ -50,6 +50,8 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
Note that `*` is a wildcard matching zero or more characters (i.e., withinin a filename or single directory name). `**` matches zero or more subdirectories, recursively. It also includes any/all files within a subdirectory, i.e., when used at the end of a pattern. For example, `**/exclude_me/**` will exclude all files in any directory named `exclude_me`, as well as all files in any subdirectories of `exclude_me`, recursively.
Special characters such as @ should be escaped, for instance:
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
@@ -47,6 +47,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### ROCm
- On Linux, The [AMDGPU driver module](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) needs to be installed on the server and, if secure boot is used, the signing key of DKMS [needs to be enrolled in UEFI BIOS](https://wiki.debian.org/SecureBoot)
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
+8 -1
View File
@@ -18,6 +18,7 @@ You can search the following types of content:
| People | Faces that are recognized in your photos/videos. |
| Contextual | Content of the photos and videos. |
| File name or extension | Full or partial file's name, or file's extension |
| Full path or folder | Full or partial folder names from the original path. |
| Description | Description added to assets. |
| Optical Character Recognition (OCR) | Text in images |
| Locations | Cities, states, and countries from reverse geocoding. |
@@ -26,10 +27,16 @@ You can search the following types of content:
| Time frame | Start and end date of a specific time bucket |
| Media type | Image or video or both |
| Display options | In Archive, in Favorites or Not in any album |
| Start rating | User-assigned start rating |
| Star rating | User-assigned star rating |
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
### Full path or folder
Use this mode when you know a folder name or part of the original asset path.
Example: for /John/Projects/3D_Printing/2026-07-01/IMG_0001.jpg, searches like Projects, 3D, Printing, or 2026 match the asset.
## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
+1
View File
@@ -18,6 +18,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
| `JPEG XL` | `.jxl` | :white_check_mark: | |
| `MPO` | `.mpo` | :white_check_mark: | Multi-Picture |
| `PNG` | `.png` | :white_check_mark: | |
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
| `RAW` | `.raw` | :white_check_mark: | |
-2
View File
@@ -20,8 +20,6 @@ def upload(file):
}
data = {
'deviceAssetId': f'{file}-{stats.st_mtime}',
'deviceId': 'python',
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
'isFavorite': 'false',
+1 -1
View File
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
### Cons
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use.
- Tailscale needs to be installed and running on both server-side and client-side.
## Option 3: Reverse Proxy
+3 -2
View File
@@ -26,7 +26,7 @@ The default configuration looks like this:
},
"ffmpeg": {
"accel": "disabled",
"accelDecode": false,
"accelDecode": true,
"acceptedAudioCodecs": ["aac", "mp3", "opus"],
"acceptedContainers": ["mov", "ogg", "webm"],
"acceptedVideoCodecs": ["h264"],
@@ -193,6 +193,7 @@ The default configuration looks like this:
"defaultStorageQuota": null,
"enabled": false,
"issuerUrl": "",
"endSessionEndpoint": "",
"mobileOverrideEnabled": false,
"mobileRedirectUri": "",
"profileSigningAlgorithm": "none",
@@ -263,4 +264,4 @@ volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
```
::
:::
+20 -18
View File
@@ -29,29 +29,31 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## General
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api, microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
| Variable | Description | Default | Containers | Workers |
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`<sup>\*3</sup>. | `false` | server | api |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
\*3: The [default configuration](https://helmetjs.github.io/#content-security-policy) sets `upgrade-insecure-requests`, which tells the browser to upgrade all requests to HTTPS. This breaks on HTTP-only deployments. If you cannot use HTTPS, you should use a custom helmet config file with `"upgrade-insecure-requests": null`.
## Workers
| Variable | Description | Default | Containers |
@@ -81,7 +83,7 @@ Information on the current workers can be found [here](/administration/jobs-work
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
| `DB_SSL_MODE` | Database SSL mode | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
+1 -1
View File
@@ -49,7 +49,7 @@ Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) wi
The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
:::note
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich.
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/retired/#docker-compose-v1-replaced-by-compose-v2) and is no longer supported by Immich.
:::
### Special requirements for Windows users
-4
View File
@@ -130,7 +130,3 @@ These storage mediums have different performance characteristics. As a result, t
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
Its a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
+2
View File
@@ -6,6 +6,8 @@ You can read more about the differences between storage template engine on and o
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.
Date and time variables in storage templates are rendered in the server's local timezone.
```bash title="Default template"
Year/Year-Month-Day/Filename.Extension
```
+6 -9
View File
@@ -17,10 +17,10 @@
"write-heading-ids": "docusaurus write-heading-ids"
},
"dependencies": {
"@docusaurus/core": "~3.9.0",
"@docusaurus/preset-classic": "~3.9.0",
"@docusaurus/theme-common": "~3.9.0",
"@docusaurus/theme-mermaid": "~3.9.0",
"@docusaurus/core": "~3.10.0",
"@docusaurus/preset-classic": "~3.10.0",
"@docusaurus/theme-common": "~3.10.0",
"@docusaurus/theme-mermaid": "~3.10.0",
"@mdi/js": "^7.3.67",
"@mdi/react": "^1.6.1",
"@mdx-js/react": "^3.0.0",
@@ -36,9 +36,9 @@
"url": "^0.11.0"
},
"devDependencies": {
"@docusaurus/module-type-aliases": "~3.9.0",
"@docusaurus/module-type-aliases": "~3.10.0",
"@docusaurus/tsconfig": "^3.10.0",
"@docusaurus/types": "^3.7.0",
"@docusaurus/types": "^3.10.0",
"prettier": "^3.7.4",
"typescript": "^6.0.0"
},
@@ -56,8 +56,5 @@
},
"engines": {
"node": ">=20"
},
"volta": {
"node": "24.14.1"
}
}
+2 -2
View File
@@ -1,7 +1,7 @@
[
{
"label": "v2.7.1",
"url": "https://docs.v2.7.1.archive.immich.app"
"label": "v2.7.5",
"url": "https://docs.v2.7.5.archive.immich.app"
},
{
"label": "v2.6.3",
+31 -3
View File
@@ -1,5 +1,12 @@
import { exportJWK, generateKeyPair } from 'jose';
import {
calculateJwkThumbprint,
exportJWK,
importPKCS8,
importSPKI,
SignJWT,
} from 'jose';
import Provider from 'oidc-provider';
import { PRIVATE_KEY_PEM, PUBLIC_KEY_PEM } from './test-keys';
export enum OAuthClient {
DEFAULT = 'client-default',
@@ -44,6 +51,29 @@ const claims = [
},
];
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256', {
extractable: true,
});
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'RS256', {
extractable: true,
});
const kid = await calculateJwkThumbprint(await exportJWK(publicKey));
export async function generateLogoutToken(iss: string, sub: string) {
return await new SignJWT({
iss: iss,
aud: OAuthClient.DEFAULT,
iat: Math.floor(Date.now() / 1000),
jti: crypto.randomUUID(),
sub: sub,
events: {
'http://schemas.openid.net/event/backchannel-logout': {},
},
})
.setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt', kid: kid })
.sign(privateKey);
}
const withDefaultClaims = (sub: string) => ({
sub,
email: `${sub}@immich.app`,
@@ -66,8 +96,6 @@ const getClaims = (sub: string, use?: string) => {
};
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',
+3 -2
View File
@@ -7,9 +7,10 @@
"start": "tsx startup.ts"
},
"devDependencies": {
"jose": "^5.6.3",
"jose": "^6.0.0",
"@types/oidc-provider": "^9.0.0",
"oidc-provider": "^9.0.0",
"tsx": "^4.20.6"
}
},
"packageManager": "pnpm@10.33.1"
}
+38
View File
@@ -0,0 +1,38 @@
export const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVj5C7hzN3E2HO
TcJ+DN/e2NSTQFj4rPylz4J8xjm8Es7l0k2kK5EEGvUNVGZbw7s055c+6kwP9eqg
B5XFE7+26Fcq1sou6Tbm310kU4dnMW5l2CgwrhaGyb1pNysao0AMLT60dFYqtUwn
ha9ceCsa+ZU1JrknVf3rONtppBvhWoI7CO9XX1keVQ0unHPzCWUjpXTzC8OGEbmB
2w7ZIUf8OfJkd5RZ4OtIpML71W9n13aDxT50x2/EW/pFLFtQ/oaleOKHpvlRXDRX
W86G4moUJym3gHMXMUj2aOcFG2UJnpLruKz3i5qZwYiTRlBP6O9EIQNCVtYxchuN
V1CCcBU1AgMBAAECggEAJLfXMu8Nx89ynPVyyUMMaFfoEpHC9iR0L5obQVpiPMYK
VRqVVLecdftPS9s7eQ58BNBRzdC0ZVu841aRYs3HLNbsZZhPkYZQpAxU//Dg5okY
fzj7Hv5yidt4HN9+Pd8z/3lRMnj4WapifLaBt8xJ2ujJBMBRxzJBsXDnT0+Kx7+y
bYDeuVfyUTEikaK3QZTbuRF3D3eiuN16GG+hv8UqTF2eYbPxdiLjYpTSHa4mH88C
qfJz2Xt4SEzmyeo3G+MO17wDFOwtEe8ojlJfULHnHJSFdUwTfYIFM1bg5/fJ9MOS
/fO3TSG+wkQqjQa6eoGssAzP87fL2XNLzlDtGY/7uQKBgQDHuJHOtf1EjOvNYiP7
EN+8QGs41ghzt9CQRQxWbHpusR3IW3P83KMXwYmrlG70oOUXBRGSB/ESXUofXc5W
pu5+Y55S44aUnu/a9yOBttYW0dtHZSL0zFT+PlVASwUzFZ2zcH1KXlUkSpfL5OAD
PyDDTnBZ2AWh45fRO9wLo6PPuQKBgQC/tI03RqU3mOjqukKbquYeIpXHfRU5Z0DM
u9ru1THYEl6fmkMXycxo/mvW3awyFuyKy/VodqIgKnFgumEqCHZh6OAMm/LC7TfA
l9tjFSs/MyOqQVD4kbX+z6Oq4c4GccDoXfsQ3gzECoBapegi/F+6/25y+/C8ghXb
J/Jg1GQXXQKBgQDFgWbfzuVZZyrBfu4qGLPJDMN7/114YizknwPma3xf/tN/EcGQ
K/k1QvWMMkvPq1UiAKcxjJ0AFjV482FcG9T6NDWbrtmmG88C8Sex3Ue2ZW2+GuwI
vhDHJIlV/Vp0/Elp7DJa2xLDwuh+gCZvz3vs6KL+ljxrrhCyn8mp0PfsMQKBgFFZ
KnuETOO0zVGdzFoGQTQUdP58A5+iQwsdxB+I9Ge+E80iRso3ZbhADj7VPhbbR3D2
b6LuhImluQrUzBpsEOAnU7vGCVPSGdBuIDiBaSKebsn2gYeZPWNtdQQ0YZq2dqek
Cb/0mfIuipzsvf7qnSza62F7q4IyqVegMegI+Jg5AoGATM3NMy7JZeKzSkm+3ohU
3xZOwgqKV9SH+0OeYWpuBxT7D7FlrKKI4NJ3XN3hg2f/DJAF6dH11CPe7pk94yol
HMbh+PQUQ6GYvAzxIOvagWboQ3lzeyubNMpyFjfOrIE/WOQCUBZ9tIwCHIarIuyi
QRuNOj3+U8T/n1Ww352HBdw=
-----END PRIVATE KEY-----`;
export const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlY+Qu4czdxNhzk3Cfgzf
3tjUk0BY+Kz8pc+CfMY5vBLO5dJNpCuRBBr1DVRmW8O7NOeXPupMD/XqoAeVxRO/
tuhXKtbKLuk25t9dJFOHZzFuZdgoMK4Whsm9aTcrGqNADC0+tHRWKrVMJ4WvXHgr
GvmVNSa5J1X96zjbaaQb4VqCOwjvV19ZHlUNLpxz8wllI6V08wvDhhG5gdsO2SFH
/DnyZHeUWeDrSKTC+9VvZ9d2g8U+dMdvxFv6RSxbUP6GpXjih6b5UVw0V1vOhuJq
FCcpt4BzFzFI9mjnBRtlCZ6S67is94uamcGIk0ZQT+jvRCEDQlbWMXIbjVdQgnAV
NQIDAQAB
-----END PUBLIC KEY-----`;
-1
View File
@@ -1 +0,0 @@
24.14.1
+15
View File
@@ -27,3 +27,18 @@ run = { task = "lint --fix" }
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
[tasks.ci-setup]
depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
run = { task = ":install" }
[tasks.ci-unit]
depends = ["//:sdk:install", "//:sdk:build"]
run = [
{ task = ":install" },
{ task = ":format" },
{ task = ":lint" },
{ task = ":check" },
]
+3 -6
View File
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
"version": "2.7.1",
"version": "2.7.5",
"description": "",
"main": "index.js",
"type": "module",
@@ -32,7 +32,7 @@
"@playwright/test": "^1.44.1",
"@socket.io/component-emitter": "^3.1.2",
"@types/luxon": "^3.4.2",
"@types/node": "^24.12.0",
"@types/node": "^24.12.2",
"@types/pg": "^8.15.1",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^7.0.0",
@@ -40,7 +40,7 @@
"eslint": "^10.0.0",
"eslint-config-prettier": "^10.1.8",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^63.0.0",
"eslint-plugin-unicorn": "^64.0.0",
"exiftool-vendored": "^35.0.0",
"globals": "^17.0.0",
"luxon": "^3.4.4",
@@ -56,8 +56,5 @@
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0"
},
"volta": {
"node": "24.14.1"
}
}
+4 -42
View File
@@ -2,82 +2,44 @@ import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
error: 'Unauthorized',
statusCode: 401,
message: 'Authentication required',
correlationId: expect.any(String),
},
unauthorizedWithMessage: (message: string) => ({
error: 'Unauthorized',
statusCode: 401,
message,
correlationId: expect.any(String),
}),
forbidden: {
error: 'Forbidden',
statusCode: 403,
message: expect.any(String),
correlationId: expect.any(String),
},
missingPermission: (permission: string) => ({
error: 'Forbidden',
statusCode: 403,
message: `Missing required permission: ${permission}`,
correlationId: expect.any(String),
}),
wrongPassword: {
error: 'Bad Request',
statusCode: 400,
message: 'Wrong password',
correlationId: expect.any(String),
},
invalidToken: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid user token',
correlationId: expect.any(String),
},
invalidShareKey: {
error: 'Unauthorized',
statusCode: 401,
message: 'Invalid share key',
correlationId: expect.any(String),
},
passwordRequired: {
error: 'Unauthorized',
statusCode: 401,
message: 'Password required',
correlationId: expect.any(String),
},
badRequest: (message: any = null) => ({
error: 'Bad Request',
statusCode: 400,
message: message ?? expect.anything(),
correlationId: expect.any(String),
}),
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
message: 'Validation failed',
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
}),
noPermission: {
error: 'Bad Request',
statusCode: 400,
message: expect.stringContaining('Not found or no'),
correlationId: expect.any(String),
},
incorrectLogin: {
error: 'Unauthorized',
statusCode: 401,
message: 'Incorrect email or password',
correlationId: expect.any(String),
},
alreadyHasAdmin: {
error: 'Bad Request',
statusCode: 400,
message: 'The server already has an admin',
correlationId: expect.any(String),
},
invalidEmail: {
error: 'Bad Request',
statusCode: 400,
message: ['email must be an email'],
correlationId: expect.any(String),
},
};
+146 -73
View File
@@ -130,12 +130,11 @@ describe('/albums', () => {
describe('GET /albums', () => {
it("should not show other users' favorites", async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining({ isFavorite: false })],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
@@ -147,7 +146,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
.get('/albums?shared=true')
.get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -155,23 +154,31 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]),
@@ -181,82 +188,164 @@ describe('/albums', () => {
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: false,
}),
expect.objectContaining({
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]),
);
});
it('should return the album collection filtered by shared', async () => {
it('should return the album collection filtered by isShared', async () => {
const { status, body } = await request(app)
.get('/albums?shared=true')
.get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedEditorUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedViewerUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user1.userId,
albumName: user1SharedLink,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: true,
}),
expect.objectContaining({
ownerId: user2.userId,
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
shared: true,
}),
]),
);
});
it('should return the album collection filtered by NOT shared', async () => {
it('should return the album collection filtered by NOT isShared', async () => {
const { status, body } = await request(app)
.get('/albums?shared=false')
.get('/albums?isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user1.userId,
albumName: user1NotShared,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
]),
shared: false,
}),
]),
);
});
it('should return only owned albums when filtered by isOwned=true', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ albumName: user1SharedEditorUser }),
expect.objectContaining({ albumName: user1SharedViewerUser }),
expect.objectContaining({ albumName: user1SharedLink }),
expect.objectContaining({ albumName: user1NotShared }),
]),
);
});
it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
albumName: user2SharedUser,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
]),
}),
]),
);
});
it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=true&isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(3);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({ albumName: user1SharedEditorUser }),
expect.objectContaining({ albumName: user1SharedViewerUser }),
expect.objectContaining({ albumName: user1SharedLink }),
]),
);
});
it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
const { status, body } = await request(app)
.get('/albums?isOwned=false&isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(0);
});
it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`)
@@ -265,17 +354,17 @@ describe('/albums', () => {
expect(body).toHaveLength(2);
});
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
const { status, body } = await request(app)
.get(`/albums?shared=true&assetId=${user1Asset1.id}`)
.get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
it('should return the album collection filtered by assetId and ignores shared=false', async () => {
it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
const { status, body } = await request(app)
.get(`/albums?shared=false&assetId=${user1Asset1.id}`)
.get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
@@ -287,13 +376,17 @@ describe('/albums', () => {
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: user4.userId,
albumName: user4DeletedAsset,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
]),
shared: false,
}),
expect.objectContaining({
ownerId: user4.userId,
albumName: user4Empty,
albumUsers: expect.arrayContaining([
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
]),
shared: false,
}),
]),
@@ -304,13 +397,12 @@ describe('/albums', () => {
describe('GET /albums/:id', () => {
it('should return album info for own album', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
@@ -322,7 +414,7 @@ describe('/albums', () => {
it('should return album info for shared album (editor)', async () => {
const { status, body } = await request(app)
.get(`/albums/${user2Albums[0].id}?withoutAssets=false`)
.get(`/albums/${user2Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -331,14 +423,14 @@ describe('/albums', () => {
it('should return album info for shared album (viewer)', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[3].id}?withoutAssets=false`)
.get(`/albums/${user1Albums[3].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Albums[3].id });
});
it('should return album info with assets when withoutAssets is undefined', async () => {
it('should return album info', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
@@ -346,25 +438,6 @@ describe('/albums', () => {
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
lastModifiedAssetTimestamp: expect.any(String),
startDate: expect.any(String),
endDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
});
it('should return album info without assets when withoutAssets is true', async () => {
const { status, body } = await request(app)
.get(`/albums/${user1Albums[0].id}?withoutAssets=true`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
@@ -379,21 +452,21 @@ describe('/albums', () => {
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
const { status, body } = await request(app)
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
.get(`/albums/${user2Albums[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
assets: [],
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
});
expect(body).toEqual(
expect.objectContaining({
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
assetCount: 1,
lastModifiedAssetTimestamp: expect.any(String),
endDate: expect.any(String),
startDate: expect.any(String),
albumUsers: expect.any(Array),
shared: true,
}),
);
});
});
@@ -419,16 +492,13 @@ describe('/albums', () => {
id: expect.any(String),
createdAt: expect.any(String),
updatedAt: expect.any(String),
ownerId: user1.userId,
albumName: 'New album',
description: '',
albumThumbnailAssetId: null,
shared: false,
albumUsers: [],
albumUsers: [{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) }],
hasSharedLink: false,
assets: [],
assetCount: 0,
owner: expect.objectContaining({ email: user1.userEmail }),
isActivityEnabled: true,
order: AssetOrder.Desc,
});
@@ -644,11 +714,11 @@ describe('/albums', () => {
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [
albumUsers: expect.arrayContaining([
expect.objectContaining({
user: expect.objectContaining({ id: user2.userId }),
}),
],
]),
}),
);
});
@@ -660,7 +730,7 @@ describe('/albums', () => {
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
expect(body).toEqual(errorDto.badRequest('User already added'));
});
it('should not be able to add existing user to shared album', async () => {
@@ -686,7 +756,7 @@ describe('/albums', () => {
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
const { status } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)
@@ -701,7 +771,10 @@ describe('/albums', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(body).toEqual(
expect.objectContaining({
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
albumUsers: [
expect.objectContaining({ role: AlbumUserRole.Owner }),
expect.objectContaining({ role: AlbumUserRole.Editor }),
],
}),
);
});
@@ -712,7 +785,7 @@ describe('/albums', () => {
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
});
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
const { status, body } = await request(app)
.put(`/albums/${album.id}/user/${user2.userId}`)
+5 -85
View File
@@ -1,7 +1,6 @@
import {
AssetMediaResponseDto,
AssetMediaStatus,
AssetResponseDto,
AssetTypeEnum,
AssetVisibility,
getAssetInfo,
@@ -19,7 +18,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, TEN_TIMES, testAssetDir, utils } from 'src/utils';
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -95,8 +94,8 @@ describe('/asset', () => {
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken, {
isFavorite: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
fileCreatedAt: yesterday.toUTC().toISO(),
fileModifiedAt: yesterday.toUTC().toISO(),
assetData: { filename: 'example.mp4' },
}),
utils.createAsset(user1.accessToken),
@@ -380,62 +379,12 @@ describe('/asset', () => {
});
});
describe('GET /assets/random', () => {
beforeAll(async () => {
await Promise.all([
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
});
it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app)
.get('/assets/random')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(1);
expect(assets[0].ownerId).toBe(user1.userId);
});
it.each(TEN_TIMES)('should return 2 random assets', async () => {
const { status, body } = await request(app)
.get('/assets/random?count=2')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(2);
for (const asset of assets) {
expect(asset.ownerId).toBe(user1.userId);
}
});
it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => {
const { status, body } = await request(app)
.get('/assets/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
});
});
describe('PUT /assets/:id', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/assets/${user2Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
});
@@ -1142,8 +1091,6 @@ describe('/asset', () => {
const { body, status } = await request(app)
.post('/assets')
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.attach('assetData', makeRandomImage(), 'example.jpg');
@@ -1160,8 +1107,6 @@ describe('/asset', () => {
const { body, status } = await request(app)
.post('/assets')
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
.field('fileCreatedAt', new Date().toISOString())
.field('fileModifiedAt', new Date().toISOString())
.attach('assetData', randomBytes(2014), 'example.jpg');
@@ -1215,29 +1160,4 @@ describe('/asset', () => {
expect(video.checksum).toStrictEqual(checksum);
});
});
describe('POST /assets/exist', () => {
it('ignores invalid deviceAssetIds', async () => {
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['invalid', 'INVALID'],
});
expect(response.existingIds).toHaveLength(0);
});
it('returns the IDs of existing assets', async () => {
await utils.createAsset(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetId: 'test-asset-0',
});
const response = await utils.checkExistingAssets(user1.accessToken, {
deviceId: 'test-assets-exist',
deviceAssetIds: ['test-asset-0'],
});
expect(response.existingIds).toEqual(['test-asset-0']);
});
});
});
+21 -7
View File
@@ -110,7 +110,9 @@ describe('/libraries', () => {
});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
});
it('should not create an external library with duplicate exclusion patterns', async () => {
@@ -125,7 +127,9 @@ describe('/libraries', () => {
});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
});
});
@@ -157,7 +161,9 @@ describe('/libraries', () => {
.send({ name: '' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty']));
expect(body).toEqual(
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
);
});
it('should change the import paths', async () => {
@@ -181,7 +187,9 @@ describe('/libraries', () => {
.send({ importPaths: [''] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty']));
expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
);
});
it('should reject duplicate import paths', async () => {
@@ -191,7 +199,9 @@ describe('/libraries', () => {
.send({ importPaths: ['/path', '/path'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
expect(body).toEqual(
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
);
});
it('should change the exclusion pattern', async () => {
@@ -215,7 +225,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
);
});
it('should reject an empty exclusion pattern', async () => {
@@ -225,7 +237,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: [''] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty']));
expect(body).toEqual(
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
);
});
});
+12 -4
View File
@@ -109,7 +109,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
});
it('should throw an error if a lat is not a number', async () => {
@@ -117,7 +119,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
);
});
it('should throw an error if a lat is out of range', async () => {
@@ -125,7 +129,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
expect(body).toEqual(
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
);
});
it('should throw an error if a lon is not provided', async () => {
@@ -133,7 +139,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180']));
expect(body).toEqual(
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
);
});
const reverseGeocodeTestCases = [
+130 -18
View File
@@ -1,9 +1,10 @@
import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server';
import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server';
import {
LoginResponseDto,
SystemConfigOAuthDto,
getConfigDefaults,
getMyUser,
getSessions,
startOAuth,
updateConfig,
} from '@immich/sdk';
@@ -76,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) =>
...defaults.oauth,
buttonText: 'Login with Immich',
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
allowInsecureRequests: true,
...dto,
};
await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options);
@@ -87,21 +89,27 @@ describe(`/oauth`, () => {
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
describe('POST /oauth/authorize', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
expect(body).toEqual(
errorDto.validationError([
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
]),
);
});
it('should return a redirect uri', async () => {
@@ -117,19 +125,60 @@ describe(`/oauth`, () => {
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
expect(params.get('state')).toBeDefined();
});
it('should not include the prompt parameter when not configured', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
const params = new URL(body.url).searchParams;
expect(params.get('prompt')).toBeNull();
});
it('should include the prompt parameter when configured', async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
prompt: 'select_account',
});
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
const params = new URL(body.url).searchParams;
expect(params.get('prompt')).toBe('select_account');
});
});
describe('POST /oauth/callback', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
buttonText: 'Login with Immich',
storageLabelClaim: 'immich_username',
});
});
it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty']));
expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
);
});
it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['url should not be empty']));
expect(body).toEqual(
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
);
});
it(`should throw an error if the state is not provided`, async () => {
@@ -158,10 +207,9 @@ describe(`/oauth`, () => {
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
const callbackParams = await loginWithOAuth('oauth-auto-register');
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
const { status, body } = await request(app)
const { status } = await request(app)
.post('/oauth/callback')
.send({ ...callbackParams, codeVerifier });
console.log(body);
expect(status).toBeGreaterThanOrEqual(400);
});
@@ -258,7 +306,7 @@ describe(`/oauth`, () => {
accessToken: expect.any(String),
isAdmin: false,
name: 'OAuth User',
userEmail: 'oauth-RS256-token@immich.app',
userEmail: 'oauth-rs256-token@immich.app',
userId: expect.any(String),
});
});
@@ -292,9 +340,7 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500);
expect(body).toMatchObject({
error: 'Internal Server Error',
message: 'Failed to finish oauth',
statusCode: 500,
});
});
@@ -313,7 +359,7 @@ describe(`/oauth`, () => {
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
expect(body).toEqual(errorDto.badRequest('OAuth authentication failed'));
});
it('should link to an existing user by email', async () => {
@@ -333,6 +379,54 @@ describe(`/oauth`, () => {
});
});
describe(`POST /oauth/backchannel-logout`, () => {
it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400);
expect(body).toEqual(
errorDto.validationError([
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
]),
);
});
it(`should throw an error if an invalid logout token is provided`, async () => {
const { status, body } = await request(app)
.post('/oauth/backchannel-logout')
.send({ logout_token: 'invalid token' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Error backchannel logout: token validation failed'));
});
it(`should logout user if a valid logout token is provided`, async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
autoRegister: true,
signingAlgorithm: 'RS256',
buttonText: 'Login with Immich',
});
const callbackParams = await loginWithOAuth('backchannel-logout-user');
const { status: callbackStatus, body: callbackBody } = await request(app)
.post('/oauth/callback')
.send(callbackParams);
expect(callbackStatus).toBe(201);
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).resolves.toHaveLength(1);
const logoutToken = await generateLogoutToken('http://0.0.0.0:2286', 'backchannel-logout-user');
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({ logout_token: logoutToken });
expect(status).toBe(200);
expect(body).toMatchObject({});
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).rejects.toMatchObject({
status: 401,
});
});
});
describe('mobile redirect override', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
@@ -399,4 +493,22 @@ describe(`/oauth`, () => {
});
});
});
describe('allowInsecureRequests: false', () => {
beforeAll(async () => {
await setupOAuth(admin.accessToken, {
enabled: true,
clientId: OAuthClient.DEFAULT,
clientSecret: OAuthClient.DEFAULT,
allowInsecureRequests: false,
});
});
it('should reject OAuth discovery over HTTP', async () => {
const { status } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500);
});
});
});
+1 -2
View File
@@ -74,7 +74,6 @@ describe('/search', () => {
const bytes = await readFile(join(testAssetDir, filename));
assets.push(
await utils.createAsset(admin.accessToken, {
deviceAssetId: `test-${filename}`,
assetData: { bytes, filename },
...dto,
}),
@@ -458,7 +457,7 @@ describe('/search', () => {
expect(Array.isArray(body)).toBe(true);
if (Array.isArray(body)) {
expect(body.length).toBeGreaterThan(10);
expect(body[0].name).toEqual(name);
expect(body[0].name).toEqual(expect.stringContaining(name));
expect(body[0].admin2name).toEqual(name);
}
});
@@ -207,16 +207,6 @@ describe('/server', () => {
});
});
describe('GET /server/theme', () => {
it('should respond with the server theme', async () => {
const { status, body } = await request(app).get('/server/theme');
expect(status).toBe(200);
expect(body).toEqual({
customCss: '',
});
});
});
describe('GET /server/license', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/server/license');
@@ -243,9 +243,21 @@ describe('/shared-links', () => {
});
it('should get data for correct password protected link', async () => {
const response = await request(app)
.post('/shared-links/login')
.send({ password: 'foo' })
.query({ key: linkWithPassword.key });
expect(response.status).toBe(201);
const cookies = response.get('Set-Cookie') ?? [];
expect(cookies).toHaveLength(1);
expect(cookies[0]).toContain('immich_shared_link_token');
const { status, body } = await request(app)
.get('/shared-links/me')
.query({ key: linkWithPassword.key, password: 'foo' });
.query({ key: linkWithPassword.key })
.set('Cookie', cookies);
expect(status).toBe(200);
expect(body).toEqual(
@@ -329,7 +341,9 @@ describe('/shared-links', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
expect(body).toEqual(
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
);
});
it('should require an asset/album id', async () => {
+9 -2
View File
@@ -41,7 +41,9 @@ describe('/stacks', () => {
.send({ assetIds: [asset.id] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
expect(body).toEqual(
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
);
});
it('should require a valid id', async () => {
@@ -51,7 +53,12 @@ describe('/stacks', () => {
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
expect(body).toEqual(
errorDto.validationError([
{ path: ['assetIds', 0], message: 'Invalid UUID' },
{ path: ['assetIds', 1], message: 'Invalid UUID' },
]),
);
});
it('should require access', async () => {
+2 -2
View File
@@ -309,7 +309,7 @@ describe('/tags', () => {
.get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should get tag details', async () => {
@@ -427,7 +427,7 @@ describe('/tags', () => {
.delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should delete a tag', async () => {
@@ -108,14 +108,20 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
['notify', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
@@ -153,14 +159,19 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
for (const [key, message] of [
['password', 'Invalid input: expected string, received null'],
['email', 'Invalid input: expected email, received object'],
['name', 'Invalid input: expected string, received null'],
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
@@ -287,7 +298,8 @@ describe('/admin/users', () => {
it('should delete user', async () => {
const { status, body } = await request(app)
.delete(`/admin/users/${userToDelete.userId}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({});
expect(status).toBe(200);
expect(body).toMatchObject({
+11 -3
View File
@@ -120,7 +120,7 @@ describe('/users', () => {
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
expect(body).toMatchObject(errorDto.badRequest('Email is not available'));
});
it('should update my email', async () => {
@@ -178,7 +178,11 @@ describe('/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number']));
expect(body).toEqual(
errorDto.validationError([
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
]),
);
});
it('should update download archive size', async () => {
@@ -204,7 +208,11 @@ describe('/users', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value']));
expect(body).toEqual(
errorDto.validationError([
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
]),
);
});
it('should update download include embedded videos', async () => {
@@ -1,7 +1,9 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import type { Socket } from 'socket.io-client';
import { utils } from 'src/utils';
import { testAssetDir, utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
@@ -83,4 +85,42 @@ test.describe('Detail Panel', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
await expect(textarea).toHaveValue('new description');
});
test.describe('Date editor', () => {
test('displays inferred asset timezone', async ({ context, page }) => {
const test = {
filepath: 'metadata/dates/datetimeoriginal-gps.jpg',
expected: {
dateTime: '2025-12-01T11:30',
// Test with a timezone which is NOT the first among timezones with the same offset
// This is to check that the editor does not simply fall back to the first available timezone with that offset
// America/Denver (-07:00) is not the first among timezones with offset -07:00
timeZoneWithOffset: 'America/Denver (-07:00)',
},
};
const asset = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, test.filepath)),
filename: basename(test.filepath),
},
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
// asset viewer -> detail panel -> date editor
await utils.setAuthCookies(context, admin.accessToken);
await page.goto(`/photos/${asset.id}`);
await page.waitForSelector('#immich-asset-viewer');
await page.getByRole('button', { name: 'Info' }).click();
await page.getByTestId('detail-panel-edit-date-button').click();
await page.waitForSelector('[role="dialog"]');
const datetime = page.locator('#datetime');
await expect(datetime).toHaveValue(test.expected.dateTime);
const timezone = page.getByRole('combobox', { name: 'Timezone' });
await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset);
});
});
});
+2 -2
View File
@@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => {
test.beforeEach(async ({ context }) => {
[firstAsset, secondAsset] = await Promise.all([
utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }),
utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }),
utils.createAsset(admin.accessToken, {}),
utils.createAsset(admin.accessToken, {}),
]);
await updateAssets(
@@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => {
});
expect(tagAtCenter).toBe('IMG');
});
test('reloads photo when checksum changes', async ({ page }) => {
await page.goto(`/photos/${asset.id}`);
const preview = page.getByTestId('preview').filter({ visible: true });
await expect(preview).toHaveAttribute('src', /.+/);
const initialSrc = await preview.getAttribute('src');
const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
await utils.replaceAsset(admin.accessToken, asset.id);
await websocketEvent;
await expect(preview).not.toHaveAttribute('src', initialSrc!);
});
});
@@ -32,8 +32,12 @@ export function generateThumbhash(rng: SeededRandom): string {
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
}
export function generateDuration(rng: SeededRandom): string {
return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
export function generateDuration(rng: SeededRandom): number {
return (
rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
1000 +
rng.nextInt(0, 1000)
);
}
export function generateUUID(): string {
@@ -3,6 +3,7 @@
*/
import {
AlbumUserRole,
AssetTypeEnum,
AssetVisibility,
UserAvatarColor,
@@ -315,11 +316,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
return {
id: asset.id,
deviceAssetId: `device-${asset.id}`,
ownerId: asset.ownerId,
owner: owner || defaultOwner,
libraryId: `library-${asset.ownerId}`,
deviceId: `device-${asset.ownerId}`,
type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image,
originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
@@ -334,7 +333,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
isArchived: false,
isTrashed: asset.isTrashed,
visibility: asset.visibility,
duration: asset.duration || '0:00:00.00000',
duration: asset.duration,
exifInfo,
livePhotoVideoId: asset.livePhotoVideoId,
tags: [],
@@ -422,14 +421,11 @@ export function getAlbum(
albumThumbnailAssetId: album.thumbnailAssetId,
createdAt: album.createdAt,
updatedAt: album.updatedAt,
ownerId: albumOwner.id,
owner: albumOwner,
albumUsers: [], // Empty array for non-shared album
albumUsers: [{ user: albumOwner, role: AlbumUserRole.Owner }],
shared: false,
hasSharedLink: false,
isActivityEnabled: true,
assetCount: albumAssets.length,
assets: albumAssets,
startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined,
endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
isTrashed: boolean;
isVideo: boolean;
isImage: boolean;
duration: string | null;
duration: number | null;
projectionType: string | null;
livePhotoVideoId: string | null;
city: string | null;
+3 -1
View File
@@ -223,6 +223,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
'.jp2',
'.jpe',
'.jxl',
'.mpo',
'.svg',
'.tif',
'.tiff',
@@ -239,7 +240,8 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
});
});
await context.route('**/api/albums*', async (route, request) => {
if (request.url().endsWith('albums?shared=true') || request.url().endsWith('albums')) {
const url = request.url();
if (url.endsWith('albums?isShared=true') || url.endsWith('albums?isOwned=true') || url.endsWith('albums')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
@@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
const now = new Date().toISOString();
return {
id: assetId,
deviceAssetId: `device-${assetId}`,
ownerId,
owner: {
id: ownerId,
@@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
avatarColor: 'blue' as never,
},
libraryId: `library-${ownerId}`,
deviceId: `device-${ownerId}`,
type: AssetTypeEnum.Image,
originalPath: `/original/${assetId}.jpg`,
originalFileName: `${assetId}.jpg`,
@@ -42,7 +40,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
isArchived: false,
isTrashed: false,
visibility: AssetVisibility.Timeline,
duration: '0:00:00.00000',
duration: null,
exifInfo: {
make: null,
model: null,
@@ -69,7 +67,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
tags: [],
people: [],
unassignedFaces: [],
stack: null,
stack: undefined,
isOffline: false,
hasMetadata: true,
duplicateId: null,
@@ -304,7 +304,7 @@ test.describe('Timeline', () => {
await page.keyboard.down('Shift');
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
await expect(
thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'),
).toHaveCount(3);
await thumbnailUtils.selectButton(page, assets[2].id).click();
await page.keyboard.up('Shift');
@@ -349,7 +349,7 @@ test.describe('Timeline', () => {
expect(visibleMockAssetsYearMonths).toContain(month);
}
});
test('Deep link to last photo, scroll up', async ({ page }) => {
test.skip('Deep link to last photo, scroll up', async ({ page }) => {
const lastAsset = assets.at(-1)!;
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
@@ -361,7 +361,7 @@ test.describe('Timeline', () => {
await thumbnailUtils.expectInViewport(page, '14e5901f-fd7f-40c0-b186-4d7e7fc67968');
});
test('Deep link to first bucket, scroll down', async ({ page }) => {
test.skip('Deep link to first bucket, scroll down', async ({ page }) => {
const lastAsset = assets.at(0)!;
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
await timelineUtils.locator(page).hover();
@@ -440,7 +440,7 @@ test.describe('Timeline', () => {
await thumbnailUtils.expectInViewport(page, asset.id);
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
});
test('Add photos to album', async ({ page }) => {
test.skip('Add photos to album', async ({ page }) => {
const album = timelineRestData.album;
await pageUtils.openAlbumPage(page, album.id);
await page.locator('nav button[aria-label="Add photos"]').click();
@@ -752,7 +752,7 @@ test.describe('Timeline', () => {
await page.getByText('Photos', { exact: true }).click();
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
});
test('open /favorites, archive photo, unarchive photo', async ({ page }) => {
test.skip('open /favorites, archive photo, unarchive photo', async ({ page }) => {
await pageUtils.openFavorites(page);
const assetToArchive = getAsset(timelineRestData, 'ad31e29f-2069-4574-b9a9-ad86523c92cb')!;
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();
-41
View File
@@ -3,7 +3,6 @@ import {
AssetMediaResponseDto,
AssetResponseDto,
AssetVisibility,
CheckExistingAssetsDto,
CreateAlbumDto,
CreateLibraryDto,
JobCreateDto,
@@ -20,7 +19,6 @@ import {
UserAdminCreateDto,
UserPreferencesUpdateDto,
ValidateLibraryDto,
checkExistingAssets,
createAlbum,
createApiKey,
createJob,
@@ -343,8 +341,6 @@ export const utils = {
},
) => {
const _dto = {
deviceAssetId: 'test-1',
deviceId: 'test',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
...dto,
@@ -375,40 +371,6 @@ export const utils = {
return body as AssetMediaResponseDto;
},
replaceAsset: async (
accessToken: string,
assetId: string,
dto?: Partial<Omit<AssetMediaCreateDto, 'assetData'>> & { assetData?: FileData },
) => {
const _dto = {
deviceAssetId: 'test-1',
deviceId: 'test',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
...dto,
};
const assetData = dto?.assetData?.bytes || makeRandomImage();
const filename = dto?.assetData?.filename || 'example.png';
if (dto?.assetData?.bytes) {
console.log(`Uploading ${filename}`);
}
const builder = request(app)
.put(`/assets/${assetId}/original`)
.attach('assetData', assetData, filename)
.set('Authorization', `Bearer ${accessToken}`);
for (const [key, value] of Object.entries(_dto)) {
void builder.field(key, String(value));
}
const { body } = await builder;
return body as AssetMediaResponseDto;
},
createImageFile: (path: string) => {
if (!existsSync(dirname(path))) {
mkdirSync(dirname(path), { recursive: true });
@@ -450,9 +412,6 @@ export const utils = {
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
searchAssets: async (accessToken: string, dto: MetadataSearchDto) => {
return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},
+8 -8
View File
@@ -3,7 +3,7 @@
"account": "حساب",
"account_settings": "إعدادات الحساب",
"acknowledge": "أُدرك ذلك",
"action": "عملية",
"action": "إجراء",
"action_common_update": "تحديث",
"action_description": "مجموعة من الفعاليات التي ستنفذ على الأصول التي تم تصفيتها",
"actions": "عمليات",
@@ -61,8 +61,8 @@
"backup_onboarding_1_description": "نسخة خارج الموقع في موقع آخر.",
"backup_onboarding_2_description": "نسخ محلية على أجهزة مختلفة. يشمل ذلك الملفات الرئيسية ونسخة احتياطية محلية منها.",
"backup_onboarding_3_description": "إجمالي نُسخ بياناتك، بما في ذلك الملفات الأصلية. يشمل ذلك نسخةً واحدةً خارج الموقع ونسختين محليتين.",
"backup_onboarding_description": "يُنصح باتباع <backblaze-link>استراتيجية النسخ الاحتياطي 3-2-1</backblaze-link> لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.",
"backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ Immich، يرجى الرجوع إلى <link> التعليمات </link>.",
"backup_onboarding_description": "يُنصح باتباع <backblaze-link>استراتيجية النسخ الاحتياطي 3-2- 1</backblaze-link> لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.",
"backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ <link>Immich</link>، يرجى الرجوع إلى <link>الوثائق</link>.",
"backup_onboarding_parts_title": "يتضمن النسخ الاحتياطي 3-2-1 ما يلي:",
"backup_onboarding_title": "النسخ الاحتياطية",
"backup_settings": "إعدادات تفريغ قاعدة البيانات",
@@ -333,7 +333,7 @@
"storage_template_migration_description": "قم بتطبيق القالب الحالي <link>{template}</link> على المحتويات التي تم رفعها سابقًا",
"storage_template_migration_info": "تغييرات النموذج الخزني ستغير جميع الصيغ الى احرف صغيرة. تغييرات النموذج ستنطبق فقط على المحتويات الجديدة. لتطبيق النموذج على المحتويات التي تم رفعها سابقًا، قم بتشغيل <link>{job}</link>.",
"storage_template_migration_job": "وظيفة تهجير قالب التخزين",
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و<implications-link>implications</implications-link>",
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و <implications-link>implications</implications-link>.",
"storage_template_onboarding_description_v2": "عند التفعيل. هذه الخاصية ستقوم بالترتيب التلقائي للملفات بناء على نموذج معرف من قبل المستخدم. رجاء اطلع على <link>التوثيق</link>.",
"storage_template_path_length": "الحد التقريبي لطول المسار: <b>{length, number}</b>/{limit, number}",
"storage_template_settings": "قالب التخزين",
@@ -372,7 +372,7 @@
"transcoding_audio_codec": "كود الصوت",
"transcoding_audio_codec_description": "Opus هو الخيار ذو أعلى جودة، ولكنه يتمتع بتوافق أقل مع الأجهزة أو البرمجيات القديمة.",
"transcoding_bitrate_description": "مقاطع الفيديو التي يتجاوز معدل البت أقصى قيمة أو التي لا تكون في تنسيق مقبول",
"transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg لل<h264-link>H.264 codec</h264-link>, <hevc-link>HEVC codec</hevc-link> and <vp9-link>VP9 codec</vp9-link>.",
"transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg لـ <h264-link>H.264 codec</h264-link>، و <hevc-link>HEVC codec</hevc-link> و <vp9-link>VP9 codec</vp9-link>.",
"transcoding_constant_quality_mode": "وضع الجودة الثابتة",
"transcoding_constant_quality_mode_description": "ICQ أفضل من CQP، ولكن بعض أجهزة عتاد التسريع لا تدعم هذا الوضع. تعيين هذا الخيار يسجعل الأفضلية للوضع المحدد عند استخدام الترميز بناءً على الجودة. يتم تجاهله بواسطة NVENC لأنه لا يدعم ICQ.",
"transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)",
@@ -2392,7 +2392,7 @@
"view_name": "عرض",
"view_next_asset": "عرض المحتوى التالي",
"view_previous_asset": "عرض المحتوى السابق",
"view_qr_code": "­عرض رمز الاستجابة السريعة",
"view_qr_code": "عرض رمز الاستجابة السريعة",
"view_similar_photos": "عرض صور مشابهة",
"view_stack": "عرض التكديس",
"view_user": "عرض المستخدم",
@@ -2411,14 +2411,14 @@
"welcome_to_immich": "مرحباً بك في Immich",
"width": "عُرض",
"wifi_name": "اسم شبكة Wi-Fi",
"workflow_delete_prompt": "هل أنت متأكد من حذف سير العمل هذا؟",
"workflow_delete_prompt": "متأكد من حذف سير العمل هذا؟",
"workflow_deleted": "تم حذف سير العمل",
"workflow_description": "وصف سير العمل",
"workflow_info": "معلومات سير العمل",
"workflow_json": "ملف JSON لسير العمل",
"workflow_json_help": "قم بتعديل إعدادات سير العمل بصيغة JSON. ستتم مزامنة التغييرات مع أداة الإنشاء المرئية.",
"workflow_name": "اسم سير العمل",
"workflow_navigation_prompt": "هل انت متاكد من المغادرة بدون حفظ التغييرات؟",
"workflow_navigation_prompt": "متاكد من المغادرة بدون حفظ التغييرات؟",
"workflow_summary": "ملخص سير العمل",
"workflow_update_success": "تم تحديث سير العمل بنجاح",
"workflow_updated": "تم تحديث سير العمل",
+5
View File
@@ -849,9 +849,12 @@
"create_link_to_share": "Opret link for at dele",
"create_link_to_share_description": "Tillad alle med linket at se de(t) valgte billede(r)",
"create_new": "OPRET NY",
"create_new_face": "Opret nyt ansigt",
"create_new_person": "Opret ny person",
"create_new_person_hint": "Tildel valgte aktiver til en ny person",
"create_new_user": "Opret ny bruger",
"create_person": "Opret person",
"create_person_subtitle": "Tilføj et navn til det valgte ansigt for at oprette og tagge den nye person",
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
"create_shared_album_page_share_select_photos": "Vælg Billeder",
"create_shared_link": "Opret delt link",
@@ -892,6 +895,7 @@
"day": "Dag",
"days": "Dage",
"deduplicate_all": "Dedubliker alle",
"default_locale": "Standard sprog",
"default_locale_description": "Formatér datoer og tal baseret på din browsers landestandard",
"delete": "Slet",
"delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt",
@@ -2213,6 +2217,7 @@
"tag": "Tag",
"tag_assets": "Tag mediefiler",
"tag_created": "Oprettet tag: {tag}",
"tag_face": "Tag ansigt",
"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_people": "Tag personer",
+111 -111
View File
@@ -1,132 +1,132 @@
{
"about": "Über",
"account": "Konto",
"account_settings": "Konto Istelligä",
"acknowledge": "Bestätige",
"account_settings": "Konto Einstellungen",
"acknowledge": "Bestätigä",
"action": "Aktion",
"action_common_update": "Update",
"action_description": "Es paar Aktione, wo a de gfilterete Assets usgführt wärde sölled",
"actions": "Aktione",
"action_description": "Aktionä, wo uf de gefilterti Mediä ausgführt werdä solled",
"actions": "Aktionen",
"active": "Aktiv",
"active_count": "Aktivi: {count}",
"active_count": "Aktiv: {count}",
"activity": "Aktivität",
"activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}",
"add": "Hinzuefüe",
"add_a_description": "Beschriibig hinzueege",
"add_a_location": "Standort hinzuefüege",
"add_a_name": "Name hinzuefüege",
"add_a_title": "Titel hinzuefüege",
"add_action": "Aktion hinzuefüege",
"add_action_description": "Aklicke um en Aktion dure zfüehre",
"add_assets": "Assets hinzufüege",
"add_birthday": "Geburtstag hinzuefüege",
"activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}",
"add": "Hinzuefüge",
"add_a_description": "Beschreibung hinzufügen",
"add_a_location": "Standort hinzuefü",
"add_a_name": "Namä hinzefü",
"add_a_title": "Titel hinzufeügä",
"add_action": "Aktion hinzuefü",
"add_action_description": "Klick do zum e Aktion hinzuefüge",
"add_assets": "Mediä hinzuefüge",
"add_birthday": "Geburtstag hinzuefüge",
"add_endpoint": "Endpunkt hinzuefüge",
"add_exclusion_pattern": "Uuschlussmuster hinzueege",
"add_filter": "Filter hinzuefüge",
"add_filter_description": "Klicke, um e Filterbedingig hinzuezfüege",
"add_location": "Standort hinzueege",
"add_more_users": "Meh Benutzer hinzueege",
"add_partner": "Partner hinzueege",
"add_path": "Pfad hinzueege",
"add_photos": "Föteli hinzueege",
"add_tag": "Tag hinzueege",
"add_to": "Hinzueege zu …",
"add_to_album": "Zum Album hinzueege",
"add_to_album_bottom_sheet_added": "Zu {album} hinzuegfüegt",
"add_to_album_bottom_sheet_already_exists": "Scho in {album}",
"add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nöd chöne im Album hinzuegfüegt werde",
"add_to_album_toggle": "Uuswahl umschalte für {album}",
"add_to_albums": "Zu Albe hinzueege",
"add_to_albums_count": "Zu Albe hinzueege ({count})",
"add_to_bottom_bar": "Hinzueege zu",
"add_to_shared_album": "Zum teilte Album hinzueege",
"add_upload_to_stack": "Upload zum Stack hinzueege",
"add_url": "URL hinzueege",
"add_workflow_step": "Workflow-Schritt hinzueege",
"added_to_archive": "Is Archiv verschobe",
"added_to_favorites": "Zu dine Favoritä hinzuegfüegt",
"added_to_favorites_count": "{count, number} zu Favorite hinzuegfüegt",
"add_exclusion_pattern": "Ausschlussmuster hinzufügen",
"add_filter": "Filter hinzufügen",
"add_filter_description": "Klicke hier um eine Filterbedingung hinzuzufügen",
"add_location": "Standort hinzufügen",
"add_more_users": "Mehr Benutzer hinzufügen",
"add_partner": "Partner hinzufügen",
"add_path": "Pfad hinzufügen",
"add_photos": "Fotos hinzufügen",
"add_tag": "Tag hinzufügen",
"add_to": "Hinzufügen zu…",
"add_to_album": "Zu Album hinzufügen",
"add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt",
"add_to_album_bottom_sheet_already_exists": "Bereits in {album}",
"add_to_album_bottom_sheet_some_local_assets": "Einige lokale Dateien konnten nicht zum Album hinzugefügt werden",
"add_to_album_toggle": "Auswahl umschalten für {album}",
"add_to_albums": "Zu Alben hinzufügen",
"add_to_albums_count": "Zu Alben hinzufügen ({count})",
"add_to_bottom_bar": "Hinzufügen zu",
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
"add_upload_to_stack": "Upload zum Stapel hinzufügen",
"add_url": "URL hinzufügen",
"add_workflow_step": "Workflow-Schritt hinzufügen",
"added_to_archive": "Zum Archiv hinzugefügt",
"added_to_favorites": "Zu Favoriten hinzugefügt",
"added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt",
"admin": {
"add_exclusion_pattern_description": "Uusschlussmuster hinzuefüge. Platzhalter, wie *, **, und ? wärded understützt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.",
"admin_user": "Admin Benutzer",
"asset_offline_description": "Die Datei vonere externe Bibliothek isch nümme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, überprüf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.",
"authentication_settings": "Authentifizierigs Iistellige",
"authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte",
"authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.",
"authentication_settings_reenable": "Bruuch ein <link>Server-Befehl</link> zum reaktiviere.",
"background_task_job": "Hintergrund Ufgabä",
"backup_database": "Datenbank-Dump aalege",
"backup_database_enable_description": "Datenbank-Dumps aktiviere",
"backup_keep_last_amount": "Aazahl vo de vorherige Dumps, wo bhalte werde sölle",
"backup_onboarding_1_description": "Offsite-Kopie i dä Cloud oder amene andere physische Standort.",
"backup_onboarding_2_description": "Lokali Kopie uf verschiedene Grät. Das beinhaltet d Hauptdateie und e lokali Sicherig vo dene Dateie.",
"backup_onboarding_3_description": "Total aazahl vo dine Dateikopie, inklusiv d Originaldateie. Das beinhaltet 1 Offsite-Kopie und 2 lokali Kopie.",
"backup_onboarding_description": "E <backblaze-link>3-2-1-Backup-Strategie</backblaze-link> wird empfohle, zum dini Dateie z schütze. Du söttsch sowohl Kopie vo dine ufgeladene Fotos/Videos wie au d Immich-Datenbank bhalte, für e rundum sauberi Backup-Lösig.",
"backup_onboarding_footer": "Für meh Infos zum Backup vo Immich lueg bitte i d <link>Dokumentation</link>.",
"backup_onboarding_parts_title": "Es 3-2-1-Backup beinhaltet:",
"add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw“ zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.",
"admin_user": "Administrator",
"asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.",
"authentication_settings": "Authentifizierungseinstellungen",
"authentication_settings_description": "Passwort-, OAuth- und andere Authentifizierungseinstellungen verwalten",
"authentication_settings_disable_all": "Bist du sicher, dass du alle Loginmethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
"background_task_job": "Hintergrundaufgaben",
"backup_database": "Datenbanksicherung erstellen",
"backup_database_enable_description": "Datenbank regelmässig sichern",
"backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
"backup_onboarding_1_description": "Offsite-Kopie in der Cloud oder an einem anderen physischen Ort.",
"backup_onboarding_2_description": "lokale Kopien auf verschiedenen Geräten. Dazu gehören die Hauptdateien und eine lokale Sicherung dieser Dateien.",
"backup_onboarding_3_description": "Kopien deiner Daten inklusive Originaldateien. Dies umfasst 1 Kopie an einem anderen Ort und 2 lokale Kopien.",
"backup_onboarding_description": "Eine <backblaze-link>3-2-1 Sicherungsstrategie</backblaze-link> wird empfohlen, um deine Daten zu schützen. Du solltest sowohl Kopien deiner hochgeladenen Fotos/Videos als auch der Immich-Datenbank aufbewahren, um eine umfassende Sicherungslösung zu haben.",
"backup_onboarding_footer": "Weitere Informationen zum Sichern von Immich findest du in der <link>Dokumentation</link>.",
"backup_onboarding_parts_title": "Eine 3-2-1-Sicherung umfasst:",
"backup_onboarding_title": "Backups",
"backup_settings": "Iistellige für Datenbank-Dumps",
"backup_settings_description": "Datenbank-Dump-Iistellige verwalte.",
"cleared_jobs": "Jobs glöscht für: {job}",
"config_set_by_file": "D Konfiguration isch aktuell dur e Konfigurationsdatei gsetzt",
"confirm_delete_library": "Bisch sicher, dass du d Bibliothek {library} wotsch lösche?",
"confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lösche? Das löscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nöd rückgängig gmacht werde. D Dateie bliibed uf em Dateträger.",
"confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe",
"confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glöscht.",
"confirm_user_password_reset": "Bisch sicher, dass du s Passwort für {user} möchtisch zruggsetze?",
"confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} möchtisch zruggsetze?",
"copy_config_to_clipboard_description": "Kopier die aktuelli Systemkonfiguration als JSON-Objekt i d'Zwüschenablage",
"create_job": "Uufgabe erstelle",
"cron_expression": "Cron-Ziitagabe",
"cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bütet z. B. der <link>Crontab Guru</link>",
"cron_expression_presets": "Vorlage für Cron-Uusdruck",
"disable_login": "Login deaktiviere",
"duplicate_detection_job_description": "Die Uufgab füehrt s maschinelle Lärne für jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi",
"exclusion_pattern_description": "Mit Uusschlussmuster chönnd Dateie und Ordner bim Scanne vo dinere Bibliothek ignoriert wärde. Das isch nützlich, wenn du Ordner häsch, wo Dateien drin händ, wo d nöd wotsch importiere, wie z. B. RAW-Dateie.",
"export_config_as_json_description": "Lad die aktuelli Systemkonfiguration als JSON-Datei abe",
"external_libraries_page_description": "Externi Bibliothekssiite für Administratore",
"face_detection": "Gsichtserkennig",
"face_detection_description": "Die Uufgab erfasst Gsichter in Dateien dur maschinells Lerne. Bi Video wird nur d'Miniaturasicht brucht. „Aktualisiere“ verarbeitet all Dateie neu. „Zruggsetze“ setzt au no all Gsichter zrugg. „Fehlendistellt nur nöd verarbeiteti Dateie in d'Warteschlange. Erfassti Gsichter wärdet zur Gsichtsidentifizierig in diWarteschlange gstellt, damit sie i bestehendi oder neui Persone z'gruppiere.",
"facial_recognition_job_description": "Die Uufgabe gruppiert im Anschluss an d'Gsichtserfassig die erfasste Gsichter zu Persone. „Zruggsetze“ gruppiert alli Gsichter neu und mit „Fehlendi“ werdet Gsichter ohni Zuordnig i d'Warteschlange gstellt.",
"failed_job_command": "Befehl {command} t für d'Uufgabe {job} nöd funktioniert",
"force_delete_user_warning": "WARNIG: Die Aktion löscht Benutzer und all sini Dateie. Das chann nöd rückgängig gmacht wärde und d'Dateie chönnd nöd wiederhergstellt wärde.",
"backup_settings": "Einstellungen für Datenbanksicherung",
"backup_settings_description": "Einstellungen zur regelmässigen Sicherung der Datenbank.",
"cleared_jobs": "Folgende Aufgaben zurückgesetzt: {job}",
"config_set_by_file": "Die Konfiguration ist aktuell durch eine Konfigurationsdatei gsetzt",
"confirm_delete_library": "Bist du sicher, dass du die Bibliothek {library} löschen willst?",
"confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek löschen willst? Dies löscht {count, plural, one {# enthaltene Datei} other {alle # enthaltenen Dateien}} aus Immich und kann nicht rückgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.",
"confirm_email_below": "Zum Bestätigen, tippe unten \"{email}\" ein",
"confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.",
"confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?",
"confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurücksetzen möchtest?",
"copy_config_to_clipboard_description": "Aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage kopieren",
"create_job": "Aufgabe erstellen",
"cron_expression": "Cron-Ausdruck",
"cron_expression_description": "Setze das Scanintervall im Cron-Format. Für mehr Informationen, siehe z. B. <link>Crontab Guru</link>",
"cron_expression_presets": "Vorlagen für Cron-Ausdrücke",
"disable_login": "Login deaktivieren",
"duplicate_detection_job_description": "Verwendet maschinelles Lernen auf den Dateien, um Duplikate zu finden. Baut auf der intelligenten Suche auf",
"exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen deiner Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.",
"export_config_as_json_description": "Aktuelle Systemkonfiguration als JSON-Datei herunterladen",
"external_libraries_page_description": "Externe Bibliotheksseite für Administratoren",
"face_detection": "Gesichtserkennung",
"face_detection_description": "Diese Aufgabe erkennt mit maschinellem Lernen Gesichter in Dateien. Bei Videos wird nur das Vorschaubild verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „Zurücksetzen“ setzt zusätzlich alle Gesichter zurück. „Fehlendefügt nur nicht verarbeitete Dateien in die Warteschlange ein. Erfasste Gesichter werden zur Gesichtsidentifizierung in die Warteschlange eingefügt, um sie in bestehende oder neue Personen zu gruppieren.",
"facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserkennung die erkannten Gesichter zu Personen. „Zurücksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.",
"failed_job_command": "Befehl {command} ist für Aufgabe {job} fehlgeschlagen",
"force_delete_user_warning": "WARNUNG: Diese Aktion löscht sofort den Benutzer und all seine Dateien. Dies kann nicht rückgängig gemacht werden und die Dateien können nicht wiederhergestellt werden.",
"image_format": "Format",
"image_format_description": "WebP erzeugt chlineri Dateie we JPEG, isch aber es bitz langsamer i de Erstellig.",
"image_fullsize_description": "Hochuflösends Bild mit glöschte Metadate, wo bim Zoome brucht wird",
"image_fullsize_enabled": "Hochuflösendi Vorschaubilder aktiviere",
"image_fullsize_enabled_description": "Generiere hochauflösende Vorschaubilder in Originalauflösung für nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.",
"image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.",
"image_fullsize_description": "Hochauflösendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird",
"image_fullsize_enabled": "Hochauflösende Vorschaubilder aktivieren",
"image_fullsize_enabled_description": "Generiere Vorschaubilder in Originalauflösung für nicht web-kompatible Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.",
"image_fullsize_quality_description": "Qualität der hochauflösenden Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien.",
"image_fullsize_title": "Hochauflösende Vorschaueinstellungen",
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
"image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.",
"image_prefer_wide_gamut": "Breites Spektrum bevorzugen",
"image_prefer_wide_gamut_setting_description": "Bruuch Display P3 für Vorschaubildli. Das erhaltet d'Vitalität von Bildli mit grossem Farbruum besser. Uf alte Grät mit alte Browser chann das aber andersch uusgseh. sRGB-Bildli wärdet als sRGB bhalte zum Farbänderige vermiide.",
"image_preview_description": "Mittelgrossi Bildli ohni Metadate, bruuchts für Einzelaasichte und fürs maschinelle Lärne",
"image_preview_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere. Z tüffi Wert chönnd s maschinelle Lärne beiträchtige.",
"image_preview_title": "Vorschauiistellige",
"image_prefer_wide_gamut_setting_description": "Display P3 (DCI-P3) für Vorschaubilder verwenden. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.",
"image_preview_description": "Mittelgrosses Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und für maschinelles Lernen verwendet wird",
"image_preview_quality_description": "Vorschauqualität von 1-100. Ein höherer Wert ist besser, erzeugt dadurch aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Ein niedriger Wert kann dafür aber die Qualität des maschinellen Lernens beeinträchtigen.",
"image_preview_title": "Vorschaueinstellungen",
"image_progressive": "Fortlaufend",
"image_progressive_description": "Codier fortlaufendi JPEG-Bildi: Sie wärdet bim Lade aufbauend aazeiget. Das hät kei Würkig uf WebP-Bildi.",
"image_progressive_description": "JPEG-Bilder schrittweise kodieren, um ein stufenweises Laden zu ermöglichen. Dies hat keine Auswirkungen auf WebP-Bilder.",
"image_quality": "Qualität",
"image_resolution": "Uuflösig",
"image_resolution_description": "Höcheri Uuflösig erhaltet meh Detail, gaht aber länger zum codiere, macht grösseri Dateie und chan d'App Schuppdizität reduziere.",
"image_settings": "Bild-Iistellige",
"image_settings_description": "Qualität und Uuflösig von erstellte Bildli verwalte",
"image_thumbnail_description": "Chlini Vorschaubildli ohni Metadate, bruuchts für Aasichte mit Gruppe vo Föteli wie i de Hauptziitachse",
"image_thumbnail_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere.",
"image_thumbnail_title": "Iistellige für Vorschaubildli",
"import_config_from_json_description": "Systemkonfiguration importiere durs Ufelade vonere JSON-Datei",
"job_concurrency": "{job} Näbeläufigkeit",
"job_created": "Uufgab erstellt",
"job_not_concurrency_safe": "Die Uufgabe ist nöd für Paralleluusführig gmacht.",
"job_settings": "Uufgabe-Iistellige",
"job_settings_description": "Uufgabe-Näbeläufigkeit verwalte",
"jobs_over_time": "Uufgabe in ziitliche Verlauf",
"image_resolution": "Auflösung",
"image_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Kodierung, haben grössere Dateigrössen und können die Reaktionsfähigkeit der App beeinträchtigen.",
"image_settings": "Bildeinstellungen",
"image_settings_description": "Qualität und Auflösung der generierten Bilder verwalten",
"image_thumbnail_description": "Kleines Vorschaubild mit entfernten Metadaten, die bei der Anzeige von Sammlungen von Fotos wie der Zeitleiste verwendet wird",
"image_thumbnail_quality_description": "Qualität der Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen.",
"image_thumbnail_title": "Einstellungen für Vorschaubilder",
"import_config_from_json_description": "Systemkonfiguration von hochgeladener JSON-Konfigurationsdatei importieren",
"job_concurrency": "{job} (Anzahl gleichzeitig laufende Prozesse)",
"job_created": "Aufgabe erstellt",
"job_not_concurrency_safe": "Diese Aufgabe kann nicht mehrmals parallel laufen gelassen werden.",
"job_settings": "Aufgabeneinstellungen",
"job_settings_description": "Gleichzeitige Ausführung von Aufgaben verwalten",
"jobs_over_time": "Jobs im Laufe der Zeit",
"library_created": "Bibliothek erstellt: {library}",
"library_deleted": "Bibliothek glöscht",
"library_details": "Bibliotheks-Details",
"library_folder_description": "Gib en Order zum Importiere a. Dä Order mit sine Underordner wird nach Bildli und Videos durchsucht.",
"library_remove_exclusion_pattern_prompt": "Bisch sicher, dass das Uuschluss-Muster wotsch lösche?",
"library_remove_folder_prompt": "Bisch sicher, dass dä Import-Ordner wotsch lösche?",
"library_scanning": "Regelmässigi Überprüefig"
"library_deleted": "Bibliothek gelöscht",
"library_details": "Bibliotheksdetails",
"library_folder_description": "Wähle einen Ordner zum Importieren. Dieser Ordner wird inklusive Unterordnern nach Bildern und Videos durchsucht.",
"library_remove_exclusion_pattern_prompt": "Bilst du sicher, dass du dieses Ausschlussmuster entfernen möchtest?",
"library_remove_folder_prompt": "Bist du sicher, dass du diesen Import-Ordner entfernen möchtest?",
"library_scanning": "Regelmässiges Scannen"
}
}
+51
View File
@@ -267,6 +267,8 @@
"notification_enable_email_notifications": "Enable email notifications",
"notification_settings": "Notification Settings",
"notification_settings_description": "Manage notification settings, including email",
"oauth_allow_insecure_requests": "Allow insecure requests",
"oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.",
"oauth_auto_launch": "Auto launch",
"oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page",
"oauth_auto_register": "Auto register",
@@ -274,9 +276,11 @@
"oauth_button_text": "Button text",
"oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.",
"oauth_enable_description": "Login with OAuth",
"oauth_end_session_url_description": "Redirect the user to this URI when they log out.",
"oauth_mobile_redirect_uri": "Mobile redirect URI",
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
"oauth_prompt_description": "Prompt parameter (e.g. select_account, login, consent)",
"oauth_role_claim": "Role Claim",
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
"oauth_settings": "OAuth",
@@ -1236,6 +1240,7 @@
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
"free_up_space_settings_subtitle": "Free up device storage",
"full_path": "Full path: {path}",
"full_path_or_folder": "Full path or folder",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General",
@@ -1392,6 +1397,7 @@
"light_theme": "Switch to light theme",
"like": "Like",
"like_deleted": "Like deleted",
"link": "Link",
"link_motion_video": "Link motion video",
"link_to_docs": "For more information, refer to the <link>documentation</link>.",
"link_to_oauth": "Link to OAuth",
@@ -1518,6 +1524,38 @@
"marked_all_as_read": "Marked all as read",
"matches": "Matches",
"matching_assets": "Matching Assets",
"media_chrome": {
"auto": "Auto",
"captions": "Captions",
"captions_off": "Off",
"closed_captions": "closed captions",
"decode_error": "Decode error",
"disable_captions": "Disable captions",
"enable_captions": "Enable captions",
"enter_fullscreen_mode": "Enter fullscreen mode",
"exit_fullscreen_mode": "Exit fullscreen mode",
"loop": "Loop",
"media_error_description": "A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.",
"media_loading": "media loading",
"mute": "Mute",
"network_error": "Network error",
"network_error_description": "A network error caused the media download to fail.",
"not_supported_error": "Source Not Supported",
"playback_rate": "Playback rate",
"playback_rate_current": "current playback rate",
"playback_rate_value": "Playback rate {playbackRate}",
"playback_time": "playback time",
"quality": "Quality",
"second": "second",
"seconds": "seconds",
"time_value_of_total_time": "{currentTime} of {totalTime}",
"time_value_remaining": "{time} remaining",
"unmute": "Unmute",
"unsupported_error_description": "An unsupported error occurred. The server or network failed, or your browser does not support this format.",
"video_not_loaded_unknown_time": "video not loaded, unknown time.",
"video_player": "video player",
"volume": "volume"
},
"media_type": "Media type",
"memories": "Memories",
"memories_all_caught_up": "All caught up",
@@ -1562,6 +1600,8 @@
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"mute_memories": "Mute Memories",
"my_albums": "My albums",
"my_immich_description": "Copy current page as a My Immich link",
"my_immich_title": "My Immich link",
"name": "Name",
"name_or_nickname": "Name or nickname",
"name_required": "Name is required",
@@ -1926,6 +1966,8 @@
"scan_settings": "Scan Settings",
"scanning": "Scanning",
"scanning_for_album": "Scanning for album...",
"screencast_mode_description": "Show keyboard and mouse event indicators on the screen",
"screencast_mode_title": "Toggle screencast mode",
"search": "Search",
"search_albums": "Search albums",
"search_by_context": "Search by context",
@@ -1933,6 +1975,8 @@
"search_by_description_example": "Hiking day in Sapa",
"search_by_filename": "Search by file name or extension",
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
"search_by_full_path": "Search by full path or folder",
"search_by_full_path_example": "/John/Projects/3D_Printing/2026-07-01 - you can search for Projects, 3D, Printing, 2026 etc.",
"search_by_ocr": "Search by OCR",
"search_by_ocr_example": "Latte",
"search_camera_lens_model": "Search lens model...",
@@ -2148,6 +2192,7 @@
"show_schema": "Show schema",
"show_search_options": "Show search options",
"show_shared_links": "Show shared links",
"show_slideshow_metadata_overlay": "Show image info overlay",
"show_slideshow_transition": "Show slideshow transition",
"show_supporter_badge": "Supporter badge",
"show_supporter_badge_description": "Show a supporter badge",
@@ -2163,6 +2208,9 @@
"skip_to_folders": "Skip to folders",
"skip_to_tags": "Skip to tags",
"slideshow": "Slideshow",
"slideshow_metadata_overlay_mode": "Overlay content",
"slideshow_metadata_overlay_mode_description_only": "Description only",
"slideshow_metadata_overlay_mode_full": "Full",
"slideshow_repeat": "Repeat slideshow",
"slideshow_repeat_description": "Loop back to beginning when slideshow ends",
"slideshow_settings": "Slideshow settings",
@@ -2214,6 +2262,8 @@
"sync_status": "Sync Status",
"sync_status_subtitle": "View and manage the sync system",
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
"system_theme": "System theme",
"system_theme_command_description": "Use the system theme ({value})",
"tag": "Tag",
"tag_assets": "Tag assets",
"tag_created": "Created tag: {tag}",
@@ -2425,6 +2475,7 @@
"workflows": "Workflows",
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
"wrong_pin_code": "Wrong PIN code",
"x_of_total": "{x}/{total}",
"year": "Year",
"years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes",
+32 -1
View File
@@ -849,9 +849,12 @@
"create_link_to_share": "Krei ligilon por dividi",
"create_link_to_share_description": "Permesi, ke iu ajn kun la ligilo povu vidi la elektita(j)n foto(j)n",
"create_new": "KREI NOVAN",
"create_new_face": "Krei novan vizaĝon",
"create_new_person": "Krei novan homon",
"create_new_person_hint": "Atribui elektitajn elementojn al nova homo",
"create_new_user": "Krei novan uzanton",
"create_person": "Krei homon",
"create_person_subtitle": "Aldoni nomon al la elektita vizaĝo por krei kaj etikedi novan homon",
"create_shared_album_page_share_add_assets": "ALDONI ELEMENTOJN",
"create_shared_album_page_share_select_photos": "Elekti fotojn",
"create_shared_link": "Krei dividitan ligilon",
@@ -1043,7 +1046,7 @@
"cannot_navigate_previous_asset": "Ne eblas navigi al antaŭa elemento",
"cant_apply_changes": "Ne eblas apliki ŝanĝojn",
"cant_change_activity": "Ne eblas {enabled, select, true {malŝalti} other {ŝalti}} tiun agon",
"cant_change_asset_favorite": "Ne eblas ŝanĝi preferaton por tiu elemento",
"cant_change_asset_favorite": "Ne eblas ŝanĝi preferon por tiu elemento",
"cant_change_metadata_assets_count": "Ne eblas ŝanĝi metadatumojn de {count, plural, one {# elemento} other {# elementoj}}",
"cant_get_faces": "Ne eblas trovi vizaĝojn",
"cant_get_number_of_comments": "Ne eblas trovi nombron da komentoj",
@@ -1074,7 +1077,18 @@
"incorrect_email_or_password": "Neĝusta retadreso aŭ pasvorto",
"library_folder_already_exists": "Tiu ĉi import-vojo jam ekzistas.",
"page_not_found": "Paĝo ne trovita",
"paths_validation_failed": "Evidentiĝis, ke {paths, plural, one {# vojo estas nevalida} other {# vojoj estas nevalidaj}}",
"profile_picture_transparent_pixels": "Ne eblas havi travideblaj bilderoj en profilbildo. Bonvolu zomi kaj/aŭ ŝovi la bildon al loko sen tiaj bilderoj.",
"quota_higher_than_disk_size": "Vi donis kvoton pli grandan ol la disko mem",
"something_went_wrong": "Io misis",
"unable_to_add_album_users": "Ne eblas aldoni uzantojn al la albumo",
"unable_to_add_assets_to_shared_link": "Ne eblas aldoni elementojn al la dividita ligilo",
"unable_to_add_comment": "Ne eblas aldoni komenton",
"unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo",
"unable_to_add_partners": "Ne eblas aldoni partnerojn",
"unable_to_add_remove_archive": "Ne eblas {archived, select, true {forigi elementon de} other {aldoni elementon al}} la arĥivo",
"unable_to_add_remove_favorites": "Ne eblas {favorite, select, true {aldoni elementon al} other {forigi elementon de}} preferataĵoj",
"unable_to_change_favorite": "Ne eblas ŝanĝi preferon por tiu elemento",
"unable_to_create": "Ne eblis krei laborfluon",
"unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo",
"unable_to_delete_workflow": "Ne eblis forigi laborfluon",
@@ -1088,15 +1102,25 @@
"expand_all": "Etendi ĉiujn",
"explore": "Esplori",
"explorer": "Foliumilo",
"favorite": "Preferataĵo",
"favorite_action_prompt": "{count} aldonita(j) al Preferataĵoj",
"favorite_or_unfavorite_photo": "Aldoni/forigi foton al/de preferataĵoj",
"favorites": "Preferataĵoj",
"favorites_page_no_favorites": "Neniuj preferataj elementoj trovitaj",
"free_up_space": "Liberigi spacon",
"free_up_space_description": "Vi forigos fotojn kaj/aŭ videojn, kiuj havas savkopiojn en la servilo, por liberigi spacon en via aparato. La kopioj en la servilo restos.",
"general": "Ĝeneralaj",
"home_page_favorite_err_local": "Ankoraŭ ne eblas aldoni lokajn elementojn al Preferataĵoj; ignorita(j)",
"home_page_favorite_err_partner": "Ankoraŭ ne eblas aldoni elementojn de partnero al Preferataĵoj; ignorita(j)",
"keep_favorites": "Konservi preferataĵojn",
"manage_media_access_settings": "Malfermi agordaĵaron",
"manage_the_app_settings": "Agordi la apon",
"map_settings_only_show_favorites": "Montri nur preferataĵojn",
"missing": "Netraktitaj",
"networking_subtitle": "Administri agordojn pri finpunktoj de la servilo",
"no_devices": "Neniuj aprobitaj aparatoj",
"no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.",
"no_favorites_message": "Aldoni al Preferataĵoj por rapide retrovi viajn plej bonajn bildojn kaj videojn",
"no_notifications": "Neniuj sciigoj",
"no_results_description": "Provu sinonimon aŭ pli ĝeneralan ŝlosilvorton",
"notification_permission_dialog_content": "Por ŝalti sciigojn, iru al Agordoj kaj elektu 'permesi'.",
@@ -1106,10 +1130,14 @@
"notification_toggle_setting_description": "Ŝalti sciigojn per retmesaĝo",
"notifications": "Sciigoj",
"notifications_setting_description": "Administri sciigojn",
"only_favorites": "Nur preferataĵoj",
"preferences_settings_subtitle": "Administri agordojn pri la apo",
"purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aŭtentikeco por la servilo",
"rating_clear": "Forviŝi pritakson",
"refresh": "Denove",
"remove_from_favorites": "Forigi el preferataĵoj",
"removed_from_favorites": "Forigita(j) el preferataĵoj",
"removed_from_favorites_count": "{count, plural, other {Forigis #}} el Preferataĵoj",
"rescan": "Reanalizi",
"reset": "Restartigi",
"reset_sqlite_clear_app_data": "Forviŝi datumojn",
@@ -1127,7 +1155,10 @@
"setting_notifications_subtitle": "Redakti viajn preferojn pri sciigoj",
"start_date": "Komenca dato",
"start_date_before_end_date": "Komenca dato devas esti antaŭ fina dato",
"to_favorite": "Aldoni al preferataĵoj",
"trigger_description": "Evento, kiu ekfunkciigas la laborfluon",
"unfavorite": "Forigi el preferataĵoj",
"unfavorite_action_prompt": "{count} forigita(j) el Preferataĵoj",
"untitled_workflow": "Sentitola laborfluo",
"upload_concurrency": "Nombro da samtempaj alŝutoj",
"user_pin_code_settings_description": "Administri vian PIN-kodon",
+13 -1
View File
@@ -845,9 +845,12 @@
"create_link_to_share": "Izradite vezu za dijeljenje",
"create_link_to_share_description": "Dopusti svakome s vezom da vidi odabrane fotografije",
"create_new": "KREIRAJ NOVO",
"create_new_face": "Stvori novo lice",
"create_new_person": "Stvorite novu osobu",
"create_new_person_hint": "Dodijelite odabrane stavke novoj osobi",
"create_new_user": "Kreiraj novog korisnika",
"create_person": "Stvori novu osobu",
"create_person_subtitle": "Dodaj ime odabranom licu kako bi stvorio i tagirao novu osobu",
"create_shared_album_page_share_add_assets": "DODAJ STAVKE",
"create_shared_album_page_share_select_photos": "Odaberi fotografije",
"create_shared_link": "Kreiraj dijeljeni link",
@@ -922,6 +925,7 @@
"deselect_all": "Poništi odabir svih",
"details": "Detalji",
"direction": "Smjer",
"disable": "Onesposobi",
"disabled": "Onemogućeno",
"disallow_edits": "Zabrani izmjene",
"discord": "Discord",
@@ -947,6 +951,7 @@
"download_include_embedded_motion_videos": "Ugrađeni videozapisi",
"download_include_embedded_motion_videos_description": "Uključite videozapise ugrađene u fotografije s pokretom kao zasebnu datoteku",
"download_notfound": "Preuzimanje nije pronađeno",
"download_original": "Preuzmi original",
"download_paused": "Preuzimanje pauzirano",
"download_settings": "Preuzmi",
"download_settings_description": "Upravljajte postavkama vezanim uz preuzimanje stavki",
@@ -956,10 +961,11 @@
"download_waiting_to_retry": "Čeka se ponovni pokušaj",
"downloading": "Preuzimanje",
"downloading_asset_filename": "Preuzimanje stavke {filename}",
"downloading_from_icloud": "Preuzmi s iCloud",
"downloading_media": "Preuzimanje medija",
"drop_files_to_upload": "Ispustite datoteke bilo gdje za prijenos",
"duplicates": "Duplikati",
"duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima",
"duplicates_description": "Razriješite svaku grupu tako da naznačite koji su duplikati, ako ih ima.",
"duration": "Trajanje",
"edit": "Izmjena",
"edit_album": "Uredi album",
@@ -987,6 +993,12 @@
"editor": "Urednik",
"editor_close_without_save_prompt": "Promjene neće biti spremljene",
"editor_close_without_save_title": "Zatvoriti uređivač?",
"editor_confirm_reset_all_changes": "Jeste li sigurni da želite resetirati sve opcije?",
"editor_discard_edits_confirm": "Odbaci izmjene",
"editor_discard_edits_prompt": "Imate nesačuvane izmjene. Jeste li sigurni da ih želite odbaciti?",
"editor_discard_edits_title": "Odbaci izmjene?",
"editor_rotate_left": "Rotiraj 90° u suprotnom smjeru kazaljke na satu",
"editor_rotate_right": "Rotiraj 90° u smjeru kazaljke na satu",
"email": "E-pošta",
"email_notifications": "Obavijesti putem e-maila",
"empty_folder": "Ova mapa je prazna",
+4
View File
@@ -849,9 +849,12 @@
"create_link_to_share": "Buat tautan untuk dibagikan",
"create_link_to_share_description": "Biarkan siapa pun dengan tautan melihat foto yang dipilih",
"create_new": "BUAT BARU",
"create_new_face": "Buat wajah baru",
"create_new_person": "Buat orang baru",
"create_new_person_hint": "Tetapkan aset yang dipilih ke orang yang baru",
"create_new_user": "Buat pengguna baru",
"create_person": "Buat orang",
"create_person_subtitle": "Tambahkan nama pada wajah yang dipilih untuk membuat dan menandai orang baru",
"create_shared_album_page_share_add_assets": "TAMBAHKAN ASET",
"create_shared_album_page_share_select_photos": "Pilih Foto",
"create_shared_link": "Buat tautan bersama",
@@ -2214,6 +2217,7 @@
"tag": "Tag",
"tag_assets": "Tag aset",
"tag_created": "Tag yang dibuat: {tag}",
"tag_face": "Tandai wajah",
"tag_feature_description": "Menjelajahi foto dan video yang dikelompokkan berdasarkan topik tag yang logis",
"tag_not_found_question": "Tidak dapat menemukan tag? <link>Buat tag baru.</link>",
"tag_people": "Beri Tag Orang",
+2
View File
@@ -1934,6 +1934,7 @@
"search_by_filename": "파일명 또는 확장자로 검색",
"search_by_filename_example": "예: IMG_1234.JPG 또는 PNG",
"search_by_ocr": "OCR로 검색",
"search_by_ocr_example": "라떼",
"search_camera_lens_model": "렌즈 모델 검색...",
"search_camera_make": "카메라 제조사 검색...",
"search_camera_model": "카메라 모델명 검색...",
@@ -1997,6 +1998,7 @@
"select_all_in": "{group}의 모든 항목 선택",
"select_avatar_color": "아바타 색상 선택",
"select_count": "{count, plural, one {# 선택중} other {# 선택중}}",
"select_cutoff_date": "유지 기간 설정",
"select_face": "얼굴 선택",
"select_featured_photo": "대표 사진 선택",
"select_from_computer": "컴퓨터에서 선택",
+3 -3
View File
@@ -1215,7 +1215,7 @@
"file_name_text": "Failo pavadinimas",
"file_name_with_value": "Failo pavadinimas: {file_name}",
"file_size": "Failo dydis",
"filename": "Failopavadinimas",
"filename": "Failo pavadinimas",
"filetype": "Failo tipas",
"filter": "Filtras",
"filter_description": "Tikslinių elementų filtravimo sąlygos",
@@ -1390,8 +1390,8 @@
"licenses": "Licencijos",
"light": "Šviesi",
"light_theme": "Perjungti į šviesią temą",
"like": "Kaip",
"like_deleted": "Kaip ištrintas",
"like": "Patinka",
"like_deleted": "Patinka panaikintas",
"link_motion_video": "Susieti judesio vaizdo įrašą",
"link_to_docs": "Daugiau informacijos rasite <link>dokumentacijoje</link>.",
"link_to_oauth": "Susieti su OAuth",
+7
View File
@@ -713,9 +713,11 @@
"create_link": "Izveidot saiti",
"create_link_to_share": "Izveidot kopīgošanas saiti",
"create_new": "IZVEIDOT JAUNU",
"create_new_face": "Izveidot jaunu seju",
"create_new_person": "Izveidot jaunu personu",
"create_new_person_hint": "Piesaistīt izvēlētos failus jaunai personai",
"create_new_user": "Izveidot jaunu lietotāju",
"create_person": "Izveidot personu",
"create_shared_album_page_share_add_assets": "PIEVIENOT AKTĪVUS",
"create_shared_album_page_share_select_photos": "Fotoattēlu Izvēle",
"create_user": "Izveidot lietotāju",
@@ -879,6 +881,7 @@
"failed_to_update_notification_status": "Neizdevās mainīt paziņojuma statusu",
"incorrect_email_or_password": "Nepareizs e-pasts vai parole",
"library_folder_already_exists": "Šis importa ceļš jau pastāv.",
"page_not_found": "Lapa nav atrasta",
"profile_picture_transparent_pixels": "Profila attēlos nevar būt caurspīdīgi pikseļi. Lūdzu, palielini un/vai pārvieto attēlu.",
"quota_higher_than_disk_size": "Tu esi iestatījis kvotu, kas pārsniedz diska izmēru",
"something_went_wrong": "Kaut kas nogāja greizi",
@@ -1295,6 +1298,7 @@
"only_favorites": "Tikai izlase",
"open": "Atvērt",
"open_calendar": "Atvērt kalendāru",
"open_in_browser": "Atvērt pārlūkprogrammā",
"open_in_map_view": "Atvērt kartes skatā",
"open_in_openstreetmap": "Atvērt OpenStreetMap",
"open_the_search_filters": "Atvērt meklēšanas filtrus",
@@ -1455,6 +1459,7 @@
"reset_people_visibility": "Atiestatīt personu redzamību",
"reset_pin_code": "Atiestatīt PIN kodu",
"reset_sqlite": "Atiestatīt SQLite datubāzi",
"reset_sqlite_clear_app_data": "Notīrīt datus",
"reset_to_default": "Atiestatīt noklusējuma iestatījumus",
"resolve_duplicates": "Atrisināt dublēšanās gadījumus",
"resolved_all_duplicates": "Visi dublikāti ir atrisināti",
@@ -1705,6 +1710,7 @@
"sync_local": "Sinhronizēt lokāli",
"sync_status": "Sinhronizācijas statuss",
"sync_status_subtitle": "Skatīt un pārvaldīt sinhronizācijas sistēmu",
"tag_face": "Atzīmēt seju",
"text_recognition": "Teksta atpazīšana",
"theme": "Dizains",
"theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz attēliem režga skatā",
@@ -1833,6 +1839,7 @@
"viewer_remove_from_stack": "Noņemt no Steka",
"viewer_stack_use_as_main_asset": "Izmantot kā Galveno Aktīvu",
"viewer_unstack": "At-Stekot",
"visibility": "Redzamība",
"visual": "Vizuāli",
"visual_builder": "Vizuālais veidotājs",
"waiting": "Gaida",
+26 -26
View File
@@ -544,7 +544,7 @@
"appears_in": "Komt voor in",
"apply_count": "Toepassen ({count, number})",
"archive": "Archief",
"archive_action_prompt": "{count} item(s) toegevoegd aan het archief",
"archive_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan het archief",
"archive_or_unarchive_photo": "Foto archiveren of uit het archief halen",
"archive_page_no_archived_assets": "Geen gearchiveerde items gevonden",
"archive_page_title": "Archief ({count})",
@@ -593,20 +593,20 @@
"assets_cannot_be_added_to_album_count": "{count, plural, one {# item} other {# items}} konden niet aan album toegevoegd worden",
"assets_cannot_be_added_to_albums": "{count, plural, one {Item kan} other {Items kunnen}} niet toegevoegd worden aan de albums",
"assets_count": "{count, plural, one {# item} other {# items}}",
"assets_deleted_permanently": "{count} item(s) permanent verwijderd",
"assets_deleted_permanently_from_server": "{count} item(s) permanent verwijderd van de Immich server",
"assets_deleted_permanently": "{count, plural, one {# item} other {# items}} permanent verwijderd",
"assets_deleted_permanently_from_server": "{count, plural, one {# item} other {# items}} permanent verwijderd van de Immich server",
"assets_downloaded_failed": "{count, plural, one {# bestand gedownload - {error} bestand mislukt} other {# bestanden gedownload - {error} bestanden mislukt}}",
"assets_downloaded_successfully": "{count, plural, one {# bestand succesvol gedownload} other {# bestanden succesvol gedownload}}",
"assets_moved_to_trash_count": "{count, plural, one {# item} other {# items}} verplaatst naar prullenbak",
"assets_permanently_deleted_count": "{count, plural, one {# item} other {# items}} permanent verwijderd",
"assets_removed_count": "{count, plural, one {# item} other {# items}} verwijderd",
"assets_removed_permanently_from_device": "{count} item(s) permanent verwijderd van je apparaat",
"assets_removed_permanently_from_device": "{count, plural, one {# item} other {# items}} permanent verwijderd van je apparaat",
"assets_restore_confirmation": "Weet je zeker dat je alle verwijderde items wilt herstellen? Je kunt deze actie niet ongedaan maken! Offline items kunnen op deze manier niet worden hersteld.",
"assets_restored_count": "{count, plural, one {# item} other {# items}} hersteld",
"assets_restored_successfully": "{count} item(s) succesvol hersteld",
"assets_trashed": "{count} item(s) naar de prullenbak verplaatst",
"assets_restored_successfully": "{count, plural, one {# item} other {# items}} succesvol hersteld",
"assets_trashed": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst",
"assets_trashed_count": "{count, plural, one {# item} other {# items}} naar prullenbak verplaatst",
"assets_trashed_from_server": "{count} item(s) naar de prullenbak verplaatst op de Immich server",
"assets_trashed_from_server": "{count, plural, one {# item} other {# items}} naar de prullenbak verplaatst op de Immich server",
"assets_were_part_of_album_count": "{count, plural, one {Item was} other {Items waren}} al onderdeel van het album",
"assets_were_part_of_albums_count": "{count, plural, one {Item is} other {Items zijn}} al onderdeel van de albums",
"authorized_devices": "Geautoriseerde apparaten",
@@ -876,7 +876,7 @@
"current_server_address": "Huidig serveradres",
"custom_date": "Aangepaste datum",
"custom_locale": "Aangepaste landinstelling",
"custom_locale_description": "Formatteer datums, tijden en getallen op basis van de geselecteerde taal en de regio",
"custom_locale_description": "Formatteer datums, tijden, en getallen op basis van de geselecteerde taal en regio",
"custom_url": "Aangepaste URL",
"cutoff_date_description": "Bewaar foto's van de laatste…",
"cutoff_day": "{count, plural, one {dag} other {dagen}}",
@@ -896,10 +896,10 @@
"days": "Dagen",
"deduplicate_all": "Alles dedupliceren",
"default_locale": "Standaard landinstelling",
"default_locale_description": "Formatteer datums en getallen op basis van de taalinstellingen van uw browser",
"default_locale_description": "Formatteer datums en getallen op basis van de taalinstellingen van je browser",
"delete": "Verwijderen",
"delete_action_confirmation_message": "Weet je zeker dat je dit item wilt verwijderen? Deze actie zorgt ervoor dat het item naar de prullenbak van de server wordt verplaatst en je wordt gevraagd of je deze ook lokaal wilt verwijderen",
"delete_action_prompt": "{count} item(s) verwijderd",
"delete_action_prompt": "{count} verwijderd",
"delete_album": "Album verwijderen",
"delete_api_key_prompt": "Weet je zeker dat je deze API-sleutel wilt verwijderen?",
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
@@ -913,12 +913,12 @@
"delete_key": "Verwijder key",
"delete_library": "Verwijder bibliotheek",
"delete_link": "Verwijder link",
"delete_local_action_prompt": "{count} item(s) lokaal verwijderd",
"delete_local_action_prompt": "{count} lokaal verwijderd",
"delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up",
"delete_local_dialog_ok_force": "Toch verwijderen",
"delete_others": "Andere verwijderen",
"delete_permanently": "Permanent verwijderen",
"delete_permanently_action_prompt": "{count} item(s) permanent verwijderd",
"delete_permanently_action_prompt": "{count} permanent verwijderd",
"delete_shared_link": "Verwijder gedeelde link",
"delete_shared_link_dialog_title": "Verwijder gedeelde link",
"delete_tag": "Tag verwijderen",
@@ -948,7 +948,7 @@
"documentation": "Documentatie",
"done": "Klaar",
"download": "Downloaden",
"download_action_prompt": "{count} item(s) aan het downloaden",
"download_action_prompt": "{count, plural, one {# item} other {# items}} aan het downloaden",
"download_canceled": "Download geannuleerd",
"download_complete": "Download voltooid",
"download_enqueue": "Download in wachtrij",
@@ -980,7 +980,7 @@
"edit_birthday": "Wijzig verjaardag",
"edit_date": "Datum bewerken",
"edit_date_and_time": "Datum en tijd bewerken",
"edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count} item(s)",
"edit_date_and_time_action_prompt": "Datum en tijd bijgewerkt van {count, plural, one {# item} other {# items}}",
"edit_date_and_time_by_offset": "Wijzigen datum door verschuiving",
"edit_date_and_time_by_offset_interval": "Nieuw datuminterval: {from}-{to}",
"edit_description": "Beschrijving bewerken",
@@ -990,7 +990,7 @@
"edit_key": "Key bewerken",
"edit_link": "Link bewerken",
"edit_location": "Locatie bewerken",
"edit_location_action_prompt": "Locatie bijgewerkt van {count} item(s)",
"edit_location_action_prompt": "Locatie bijgewerkt van {count, plural, one {# item} other {# items}}",
"edit_location_dialog_title": "Locatie",
"edit_name": "Naam bewerken",
"edit_people": "Mensen bewerken",
@@ -1203,7 +1203,7 @@
"failed_to_load_assets": "Kan items niet laden",
"failed_to_load_folder": "Laden van map mislukt",
"favorite": "Favoriet",
"favorite_action_prompt": "{count} item(s) toegevoegd aan je favorieten",
"favorite_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan je favorieten",
"favorite_or_unfavorite_photo": "Foto markeren als of verwijderen uit favorieten",
"favorites": "Favorieten",
"favorites_page_no_favorites": "Geen favoriete items gevonden",
@@ -1389,7 +1389,7 @@
"library_page_sort_title": "Albumtitel",
"licenses": "Licenties",
"light": "Licht",
"light_theme": "Wissel naar lichte thema",
"light_theme": "Wissel naar licht thema",
"like": "Vind ik leuk",
"like_deleted": "Like verwijderd",
"link_motion_video": "Koppel bewegende video",
@@ -1551,7 +1551,7 @@
"move_off_locked_folder": "Verplaats uit vergrendelde map",
"move_to": "Verplaatsen naar",
"move_to_device_trash": "Naar prullenbak van apparaat",
"move_to_lock_folder_action_prompt": "{count} item(s) toegevoegd aan de vergrendelde map",
"move_to_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} toegevoegd aan de vergrendelde map",
"move_to_locked_folder": "Verplaats naar vergrendelde map",
"move_to_locked_folder_confirmation": "Deze fotos en videos worden uit alle albums verwijderd en zijn alleen te bekijken in de vergrendelde map",
"move_up": "Naar boven verplaatsen",
@@ -1854,9 +1854,9 @@
"remove_custom_date_range": "Aangepast datumbereik verwijderen",
"remove_deleted_assets": "Verwijder offline bestanden",
"remove_from_album": "Verwijderen uit album",
"remove_from_album_action_prompt": "{count} item(s) verwijderd uit het album",
"remove_from_album_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit het album",
"remove_from_favorites": "Verwijderen uit favorieten",
"remove_from_lock_folder_action_prompt": "{count} item(s) verwijderd uit de vergrendelde map",
"remove_from_lock_folder_action_prompt": "{count, plural, one {# item} other {# items}} verwijderd uit de vergrendelde map",
"remove_from_locked_folder": "Verwijder uit de vergrendelde map",
"remove_from_locked_folder_confirmation": "Weet je zeker dat je deze foto's en video's uit de vergrendelde map wilt verplaatsen? Ze zijn dan weer zichtbaar in je bibliotheek.",
"remove_from_shared_link": "Verwijderen uit gedeelde link",
@@ -1899,7 +1899,7 @@
"resolved_all_duplicates": "Alle duplicaten opgelost",
"restore": "Herstellen",
"restore_all": "Herstel alle",
"restore_trash_action_prompt": "{count} item(s) teruggehaald uit de prullenbak",
"restore_trash_action_prompt": "{count, plural, one {# item} other {# items}} teruggehaald uit de prullenbak",
"restore_user": "Gebruiker herstellen",
"restored_asset": "Item hersteld",
"resume": "Hervatten",
@@ -2067,9 +2067,9 @@
"settings_saved": "Instellingen opgeslagen",
"setup_pin_code": "Stel een pincode in",
"share": "Delen",
"share_action_prompt": "{count} item(s) gedeeld",
"share_action_prompt": "{count, plural, one {# item} other {# items}} gedeeld",
"share_add_photos": "Foto's toevoegen",
"share_assets_selected": "{count} item(s) geselecteerd",
"share_assets_selected": "{count, plural, one {# item} other {# items}} geselecteerd",
"share_dialog_preparing": "Voorbereiden...",
"share_link": "Link delen",
"shared": "Gedeeld",
@@ -2177,7 +2177,7 @@
"sort_title": "Titel",
"source": "Bron",
"stack": "Stapel",
"stack_action_prompt": "{count} item(s) gestapeld",
"stack_action_prompt": "{count} items gestapeld",
"stack_duplicates": "Stapel duplicaten",
"stack_select_one_photo": "Selecteer één primaire foto voor de stapel",
"stack_selected_photos": "Geselecteerde foto's stapelen",
@@ -2264,7 +2264,7 @@
"total": "Totaal",
"total_usage": "Totaal gebruik",
"trash": "Prullenbak",
"trash_action_prompt": "{count} item(s) verplaatst naar de prullenbak",
"trash_action_prompt": "{count, plural, one {# item} other {# items}} verplaatst naar de prullenbak",
"trash_all": "Verplaats alle naar prullenbak",
"trash_count": "{count, number} naar prullenbak",
"trash_delete_asset": "Items naar prullenbak verplaatsen of verwijderen",
@@ -2314,7 +2314,7 @@
"unselect_all_duplicates": "Deselecteer alle duplicaten",
"unselect_all_in": "Deselecteer alles in {group}",
"unstack": "Ontstapelen",
"unstack_action_prompt": "{count} item(s) ontstapeld",
"unstack_action_prompt": "{count} items ontstapeld",
"unstacked_assets_count": "{count, plural, one {# item} other {# items}} ontstapeld",
"unsupported_field_type": "Veldtype niet ondersteund",
"unsupported_file_type": "Bestand {file} kan niet worden geüpload omdat het bestandstype {type} niet wordt ondersteund.",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "immich-i18n",
"version": "2.7.1",
"version": "2.7.5",
"private": true,
"scripts": {
"format": "prettier --cache --check .",
+2 -2
View File
@@ -1009,8 +1009,8 @@
"editor_edits_applied_success": "Zmiany zostały pomyślnie zastosowane",
"editor_flip_horizontal": "Odwróć poziomo",
"editor_flip_vertical": "Odwróć pionowo",
"editor_handle_corner": "{corner, select, top_left {Top-left} top_right {Top-right} bottom_left {Bottom-left} bottom_right {Bottom-right} other {A}} uchwyt narożny",
"editor_handle_edge": "{edge, select, top {Top} bottom {Bottom} left {Left} right {Right} other {An}} uchwyt krawędziowy",
"editor_handle_corner": "{corner, select, top_left {Górny lewy} top_right {Górny prawy} bottom_left {Dolny lewy} bottom_right {Dolny prawy} other {Jakiś}} uchwyt narożny",
"editor_handle_edge": "{edge, select, top {Górny} bottom {Dolny} left {Lewy} right {Prawy} other {Jakiś}} uchwyt krawędziowy",
"editor_orientation": "Orientacja",
"editor_reset_all_changes": "Zresetuj zmiany",
"editor_rotate_left": "Obróć o 90° przeciwnie do ruchu wskazówek zegara",
+28 -16
View File
@@ -5,6 +5,7 @@
"acknowledge": "รับทราบ",
"action": "ดำเนินการ",
"action_common_update": "อัปเดต",
"action_description": "ชุดการดำเนินการที่จะปฏิบัติกับรายการที่ผ่านการกรอง",
"actions": "การดำเนินการ",
"active": "กำลังทำงาน",
"active_count": "กำลังทำงาน: {count}",
@@ -16,11 +17,13 @@
"add_a_name": "เพิ่มชื่อ",
"add_a_title": "เพิ่มหัวข้อ",
"add_action": "เพิ่มการดำเนินการ",
"add_action_description": "คลิกเพื่อเพิ่มการดำเนินการ",
"add_assets": "เพิ่มสื่อ",
"add_birthday": "เพิ่มวันเกิด",
"add_endpoint": "เพิ่มปลายทาง",
"add_exclusion_pattern": "เพิ่มข้อยกเว้น",
"add_filter": "เพิ่มตัวกรอง",
"add_filter_description": "คลิกเพื่อเพิ่มการกรอง",
"add_location": "เพิ่มตำแหน่ง",
"add_more_users": "เพิ่มผู้ใช้งาน",
"add_partner": "เพิ่มคู่หู",
@@ -32,12 +35,14 @@
"add_to_album_bottom_sheet_added": "เพิ่มไปยัง {album} แล้ว",
"add_to_album_bottom_sheet_already_exists": "อยู่ใน {album} อยู่แล้ว",
"add_to_album_bottom_sheet_some_local_assets": "ไฟล์บางส่วนไม่สามารถเพิ่มไปยังอัลบั้มได้",
"add_to_album_toggle": "สลับการเลือกสำหรับ {album}",
"add_to_albums": "เพิ่มเข้าในอัลบั้ม",
"add_to_albums_count": "เพิ่มไปยังอัลบั้ม ({count})",
"add_to_bottom_bar": "เพิ่มไปยัง",
"add_to_shared_album": "เพิ่มไปยังอัลบั้มที่แชร์",
"add_upload_to_stack": "เพิ่มที่อัปโหลดเข้า stack",
"add_url": "เพิ่ม URL",
"add_workflow_step": "เพิ่มขั้นตอนการทำงาน",
"added_to_archive": "เพิ่มไปยังที่จัดเก็บถาวร",
"added_to_favorites": "เพิ่มเข้ารายการโปรดแล้ว",
"added_to_favorites_count": "เพิ่ม {count, number} รูปเข้ารายการโปรดแล้ว",
@@ -70,6 +75,7 @@
"confirm_reprocess_all_faces": "คุณแน่ใจว่าคุณต้องการประมวลผลใบหน้าทั้งหมดใหม่? ชื่อคนจะถูกลบไปด้วย",
"confirm_user_password_reset": "คุณแน่ใจว่าต้องการรีเซ็ตรหัสผ่านของ {user} หรือไม่?",
"confirm_user_pin_code_reset": "คุณแน่ใจหรือไม่ว่าต้องการรีเซ็ตรหัส PIN ของ {user}",
"copy_config_to_clipboard_description": "คัดลอกการตั้งค่าระบบปัจจุบันในรูปแบบ JSON ไปยังคลิปบอร์ด",
"create_job": "สร้างงาน",
"cron_expression": "รูปแบบ cron",
"cron_expression_description": "ตั้งช่วงเวลาในการสแกนโดยใช้รูปแบบ cron สำหรับข้อมูลเพิ่มเติมกรุณาอิง <link>Crontab Guru</link>",
@@ -77,6 +83,7 @@
"disable_login": "ปิดการล็อกอิน",
"duplicate_detection_job_description": "ใช้ machine learning กับสี่อเพื่อตรวจจับรูปภาพที่คล้ายกัน โดยใช้การค้นหาอัจฉริยะ",
"exclusion_pattern_description": "ข้อยกเว้นสามารถละเว้นไฟล์และโฟลเดอร์ขณะสแกนคลังภาพของคุณ มีประโยชน์เมื่อโฟลเดอร์มีไฟล์ที่ไม่อยากนำเข้า เช่นไฟล์ RAW",
"export_config_as_json_description": "ดาวน์โหลดการตั้งค่าระบบปัจจุบันไปยังไฟล์ในรูปแบบ JSON",
"external_libraries_page_description": "หน้าต่างคลังแอดมินภายนอก",
"face_detection": "การตรวจจับใบหน้า",
"face_detection_description": "ตรวจจับใบหน้าในสี่อโดยใช้ machine learning วิดีโอจะใช้ภาพตัวอย่างจากวิดีโอเท่านั้น \"ทั้งหมด\" จะประมวลผลสี่อทั้งหมด \"ขาดหาย\" จะประมวลผลสี่อที่ยังไม่ได้ประมวลผล ใบหน้าที่ถูกตรวจจับแล้วจะถูกเข้าคิวประมวลผลการจดจำใบหน้า เพิ่มเข้าไปในกลุ่มที่มีอยู่แล้วหรือคนใหม่",
@@ -97,6 +104,8 @@
"image_preview_description": "ภาพขนาดปานกลางที่ถูกลบข้อมูลเมตา ใช้สำหรับการดูแอสเซ็ตเดี่ยวและสำหรับการเรียนรู้ของเครื่อง (Machine Learning)",
"image_preview_quality_description": "คุณภาพการแสดงตัวอย่างตั้งแต่ 1-100 ยิ่งสูงยิ่งดี แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้นและอาจทำให้แอปตอบสนองช้าลง การตั้งค่าต่ำอาจส่งผลต่อคุณภาพ Machine Learning",
"image_preview_title": "ตั้งค่าพรีวิว",
"image_progressive": "รูปภาพแบบโปรเกรสซีฟ",
"image_progressive_description": "เข้ารหัสรูปภาพ JPEG แบบโปรเกรสซีฟเพื่อให้แสดงผลแบบค่อยๆ ชัดขึ้นขณะโหลด ทั้งนี้จะไม่มีผลกับรูปภาพ WebP",
"image_quality": "คุณภาพ",
"image_resolution": "ความละเอียด",
"image_resolution_description": "ความละเอียดสูกว่าสามารถเก็บรายละเอียดได้มากกว่าแต่ใช้เวลา encode นานกว่า ไฟล์ใหญ่กว่า และลดความตอบสนองของแอป",
@@ -105,6 +114,7 @@
"image_thumbnail_description": "รูปขนาดย่อที่มีการลบข้อมูลเมตาด้าต้า ใช้เมื่อดูภาพถ่ายในกลุ่ม เช่น ในไทม์ไลน์หลัก",
"image_thumbnail_quality_description": "คุณภาพของภาพขนาดย่อตั้งแต่ 1-100 ยิ่งสูงยิ่งดี แต่จะทำให้ไฟล์มีขนาดใหญ่ขึ้นและอาจทำให้แอปตอบสนองช้าลง",
"image_thumbnail_title": "ตั้งค่า Thumbnail",
"import_config_from_json_description": "นำเข้าการตั้งค่าระบบโดยการอัปโหลดไฟล์คอนฟิก JSON",
"job_concurrency": "{job} งานพร้อมกัน",
"job_created": "สร้างงานเรียบร้อย",
"job_not_concurrency_safe": "งานนี้ทำงานพร้อมกันแบบปลอดภัยไม่ได้",
@@ -510,10 +520,10 @@
"always_keep_photos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บรูปภาพทั้งหมดบนอุปกรณ์นี้",
"always_keep_videos_hint": "\"เพิ่มพื้นที่ว่าง\" จะเก็บวิดีโอทั้งหมดบนอุปกรณ์นี้",
"anti_clockwise": "ทวนเข็มนาฬิกา",
"api_key": "API key",
"api_key": "คีย์ API",
"api_key_description": "ค่านี้จะแสดงเพียงครั้งเดียว โปรดคัดลอกก่อนปิดหน้าต่าง",
"api_key_empty": "ชื่อ API Key ของคุณไม่ควรว่างเปล่า",
"api_keys": "API Key",
"api_key_empty": "ชื่อคีย์ API ของคุณไม่ควรว่างเปล่า",
"api_keys": "คีย์ API",
"app_architecture_variant": "รูปแบบ (สถาปัตยกรรม)",
"app_bar_signout_dialog_content": "คุณแน่ใจว่าอยากออกจากระบบ",
"app_bar_signout_dialog_ok": "ใช่",
@@ -867,7 +877,7 @@
"delete": "ลบออก",
"delete_action_prompt": "ลบ {count} รายการแล้ว",
"delete_album": "ลบอัลบั้ม",
"delete_api_key_prompt": "คุณต้องการลบ API คีย์ นี้ใช่ไหม ?",
"delete_api_key_prompt": "คุณต้องการลบคีย์ API นี้หรือไม่?",
"delete_dialog_alert": "รายการดังกล่าวจะถูกลบจาก Immich และเครื่องอย่างถาวร",
"delete_dialog_alert_local": "รายการดังกล่าวจะถูกลบจากเครื่องคุณอย่างถาวร แต่จะยังคงอยู่บนเซิร์ฟเวอร์ Immich",
"delete_dialog_alert_local_non_backed_up": "รายการบางตัวไม่ได้ถูกสำรองบน Immich และจะถูกลบจากเครื่องคุณอย่างถาวร",
@@ -1062,7 +1072,7 @@
"unable_to_connect": "ไม่สามารถเชื่อมต่อได้",
"unable_to_copy_to_clipboard": "ไม่สามารถคัดลอกไปยังคลิปบอร์ดได้ ตรวจสอบให้แน่ใจว่าคุณเข้าถึงหน้าผ่านทาง https",
"unable_to_create_admin_account": "ไม่สามารถสร้างบัญชีผู้ดูแลระบบได้",
"unable_to_create_api_key": "ไม่สามารถสร้าง API คีย์ ได้",
"unable_to_create_api_key": "ไม่สามารถสร้างคีย์ API",
"unable_to_create_library": "ไม่สามารถสร้างคลังภาพได้",
"unable_to_create_user": "ไม่สามารถสร้างผู้ใช้ได้",
"unable_to_delete_album": "ไม่สามารถลบอัลบั้มได้",
@@ -1089,7 +1099,7 @@
"unable_to_reassign_assets_new_person": "ไม่สามารถมอบหมาย ให้กับบุคคลใหม่ได้",
"unable_to_refresh_user": "ไม่สามารถรีเฟรชผู้ใช้ได้",
"unable_to_remove_album_users": "ไม่สามารถลบผู้ใช้ออกจากอัลบั้มได้",
"unable_to_remove_api_key": "ไม่สามารถลบ API Key ได้",
"unable_to_remove_api_key": "ไม่สามารถลบคีย์ API",
"unable_to_remove_assets_from_shared_link": "ไม่สามารถลบออกจากลิงก์ที่แชร์ได้",
"unable_to_remove_library": "ไม่สามารถลบคลังภาพได้",
"unable_to_remove_partner": "ไม่สามารถลบคู่หูได้",
@@ -1101,7 +1111,7 @@
"unable_to_restore_trash": "ไม่สามารถเรียกคืนถังขยะได้",
"unable_to_restore_user": "ไม่สามารถเรียกคืนผู้ใช้ได้",
"unable_to_save_album": "ไม่สามารถบันทึกอัลบั้มได้",
"unable_to_save_api_key": "ไม่สามารถบันทึก API คีย์ ได้",
"unable_to_save_api_key": "ไม่สามารถบันทึกคีย์ API",
"unable_to_save_date_of_birth": "ไม่สามารถบันทึกวันเกิดได้",
"unable_to_save_name": "ไม่สามารถบันทึกชื่อได้",
"unable_to_save_profile": "ไม่สามารถบันทึกโปรไฟล์ได้",
@@ -1195,7 +1205,7 @@
"geolocation_instruction_location": "คลิกบนสื่อที่มีพิกัด GPS เพื่อใช้ตำแหน่งนั้น หรือเลือกตำแหน่งจากแผนที่โดยตรง",
"get_help": "ขอความช่วยเหลือ",
"get_people_error": "ข้อผิดพลาดขณะดึงข้อมูลผู้คน",
"get_wifiname_error": "ไม่สามารถรับชื่อ Wi-Fi กรุณายืนยันการให้อนุญาตแอ และยืนยันว่า Wi-Fi เชื่อมต่ออยู่",
"get_wifiname_error": "ไม่สามารถรับชื่อ Wi-Fi กรุณายืนยันการให้อนุญาตแอ และยืนยันว่าเชื่อมต่อกับเครือข่าย Wi-Fi อยู่",
"getting_started": "เริ่มต้นใช้งาน",
"go_back": "กลับ",
"go_to_folder": "ไปที่โฟล์เดอร์",
@@ -1430,7 +1440,7 @@
"manage_sharing_with_partners": "จัดการการแชร์กับคู่หู",
"manage_the_app_settings": "จัดการการตั้งค่าแอป",
"manage_your_account": "จัดการบัญชีของคุณ",
"manage_your_api_keys": "จัดการกุญแจ API ของคุณ",
"manage_your_api_keys": "จัดการคีย์ API ของคุณ",
"manage_your_devices": "จัดการอุปกรณ์ของคุณ",
"manage_your_oauth_connection": "จัดการการเชื่อมต่อ OAuth ของคุณ",
"map": "แผนที่",
@@ -1516,7 +1526,7 @@
"networking_subtitle": "ตั้งค่าปลายทางเซิร์ฟเวอร์",
"never": "ไม่เคย",
"new_album": "อัลบั้มใหม่",
"new_api_key": "สร้าง API คีย์ใหม่",
"new_api_key": "สร้างคีย์ API ใหม่",
"new_date_range": "ช่วงวันที่ใหม่",
"new_password": "รหัสผ่านใหม่",
"new_person": "คนใหม่",
@@ -1581,7 +1591,7 @@
"on_this_device": "บนอุปกรณ์นี้",
"onboarding": "การเริ่มต้นใช้งาน",
"onboarding_locale_description": "เลือกภาษาที่คุณต้องการ คุณสามารถเปลี่ยนได้ภายหลังในการตั้งค่า",
"onboarding_privacy_description": "ฟีเจอร์ (ตัวเลือก) ต่อไปนี้ต้องอาศัยบริการภายนอก และสามารถปิดใช้งานได้ตลอดเวลาในการตั้งค่าการ",
"onboarding_privacy_description": "คุณสมบัติ (ตัวเลือก) ต่อไปนี้ต้องอาศัยบริการภายนอก และสามารถปิดใช้งานได้ตลอดเวลาในการตั้งค่า",
"onboarding_server_welcome_description": "มาตั้งค่าเซิร์ฟเวอร์ของคุณด้วยการตั้งค่าที่ใช้บ่อยกันเถอะ",
"onboarding_theme_description": "เลือกธีมสี คุณสามารถเปลี่ยนแปลงได้ในภายหลังในการตั้งค่าของคุณ",
"onboarding_user_welcome_description": "มาเริ่มต้นกันเถอะ!",
@@ -1631,7 +1641,7 @@
"pattern": "รูปแบบ",
"pause": "หยุด",
"pause_memories": "หยุดดูความทรงจํา",
"paused": "หยุด",
"paused": "หยุดชั่วคราว",
"pending": "กำลังรอ",
"people": "ผู้คน",
"people_edits_count": "{count, plural, one {# person} other {# people}} ถูกแก้ไข",
@@ -1644,11 +1654,13 @@
"permanently_delete_assets_prompt": "คุณแน่ใจหรือไม่ว่าต้องการลบ {count, plural, one {this asset?} other {these <b>#</b> asset?}}อย่างถาวร การดำเนินการนี้จะลบ {count, plural, one {it from its} other {them from their}} อัลบั้มด้วย",
"permanently_deleted_asset": "ลบสื่อถาวรแล้ว",
"permanently_deleted_assets_count": "ลบ {count, plural, one {# asset} other {# assets}} เรียบร้อยแล้ว",
"permission": "สิทธิ์",
"permission_empty": "สิทธิ์ของคุณต้องไม่เว้นว่าง",
"permission_onboarding_back": "กลับ",
"permission_onboarding_continue_anyway": "ดำเนินการต่อ",
"permission_onboarding_get_started": "เริ่มต้น",
"permission_onboarding_go_to_settings": "ไปยังการตั้งค่า",
"permission_onboarding_permission_denied": "ไม่อนุญาต ตั้งค่าสิทธิ์เข้าถึงรูปภาพและวิดีโอเพื่อใช้งาน Immich",
"permission_onboarding_permission_denied": "สิทธิ์ถูกปฏิเสธ กรุณาให้สิทธิ์เข้าถึงรูปภาพและวิดีโอเพื่อใช้งาน Immich",
"permission_onboarding_permission_granted": "ให้สิทธิ์สำเร็จ คุณพร้อมใช้งานแล้ว",
"permission_onboarding_permission_limited": "สิทธ์จำกัด เพื่อให้ Immich สำรองข้อมูลและจัดการคลังภาพได้ ตั้งค่าสิทธิเข้าถึงรูปภาพและวิดีโอ",
"permission_onboarding_request": "Immich จำเป็นจะต้องได้รับสิทธิ์ดูรูปภาพและวิดีโอ",
@@ -1783,7 +1795,7 @@
"remove_photo_from_memory": "ลบรูปออกจากความทรงจำนี้",
"remove_url": "ลบ URL",
"remove_user": "ลบผู้ใช้",
"removed_api_key": "API คีย์ของ: {name} ถูกลบแล้ว",
"removed_api_key": "ลบคีย์ API แล้ว: {name}",
"removed_from_archive": "ลบจากเก็บถาวรแล้ว",
"removed_from_favorites": "ลบจากรายการโปรดแล้ว",
"removed_from_favorites_count": "{count, plural, other {ถูกลบ#}} จากรายการโปรดแล้ว",
@@ -1831,7 +1843,7 @@
"save": "บันทึก",
"save_to_gallery": "บันทึกไปยังแกลเลอรี",
"saved": "บันทึกแล้ว",
"saved_api_key": "บันทึก API คีย์ แล้ว",
"saved_api_key": "บันทึกคีย์ API แล้ว",
"saved_profile": "แก้ไขโปรไฟล์สำเร็จ",
"saved_settings": "บันทึกการตั้งค่าสำเร็จ",
"say_something": "พูดอะไรสักอย่าง",
@@ -2226,7 +2238,7 @@
"upload_status_duplicates": "รายการซ้ำ",
"upload_status_errors": "ข้อผิดพลาด",
"upload_status_uploaded": "อัปโหลดแล้ว",
"upload_success": "อัปโหลดสำเร็จ รีเฟรชหน้าเพื่อดูสื่อใหม่ที่อัปโหลดล่าสุด",
"upload_success": "อัปโหลดสำเร็จ รีเฟรชหน้าเพื่อดูสื่อที่อัปโหลดใหม่",
"upload_to_immich": "อัปโหลดไปยัง Immich ({count})",
"uploading": "กำลังอัปโหลด",
"uploading_media": "กำลังอัปโหลดสื่อ",
+4
View File
@@ -849,9 +849,12 @@
"create_link_to_share": "Створити посилання спільного доступу",
"create_link_to_share_description": "Дати змогу будь-кому переглядати вибрані фото за посиланням",
"create_new": "СТВОРИТИ НОВИЙ",
"create_new_face": "Створити нове обличчя",
"create_new_person": "Створити нову людину",
"create_new_person_hint": "Призначити вибрані елементи новій людині",
"create_new_user": "Створити нового користувача",
"create_person": "Створити людину",
"create_person_subtitle": "Додайте ім'я до вибраного обличчя, щоб створити та позначити нову особу",
"create_shared_album_page_share_add_assets": "ДОДАТИ ЕЛЕМЕНТИ",
"create_shared_album_page_share_select_photos": "Вибрати фото",
"create_shared_link": "Створити спільне посилання",
@@ -2214,6 +2217,7 @@
"tag": "Тег",
"tag_assets": "Додати теги",
"tag_created": "Створено тег: {tag}",
"tag_face": "Тег обличчя",
"tag_feature_description": "Перегляд фото та відео, згрупованих за логічними темами тегів",
"tag_not_found_question": "Не вдається знайти тег? <link>Створіть новий тег.</link>",
"tag_people": "Позначити людей",

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