Compare commits

...

108 Commits

Author SHA1 Message Date
Alex The Bot
430561d692 Version v1.98.1 2024-03-08 23:44:13 +00:00
Alex
e8fb529026 fix(server): getAllAssets doesn't return all assets (#7752)
* fix(server): getAllAssets doesn't return all assets

* try reverting

* fix: archive and remove unused method

* update sql

* remove unused code

* linting
2024-03-08 17:16:32 -06:00
Sam Holton
7a4ae7d142 feat(server,web): add force delete to immediately remove user (#7681)
* feat(server,web): add force delete to immediately remove user

* update wording on force delete confirmation

* fix force delete css

* PR feedback

* cleanup user service delete for force

* adding user status column

* some cleanup and tests

* more test fixes

* run npm run sql:generate

* chore: cleanup and websocket

* chore: linting

* userRepository.restore

* removed bad color class from delete-confirm-dialoge

* additional confirmation for user force delete

* shorten confirmation message

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-03-08 17:49:39 -05:00
Andrew Roberts
9cb0a1ffbf fix(web): modal password reset modal in dark mode (#7748)
* Fixed dark mode password reset success

* Fixed prettier issue
2024-03-08 14:05:15 -05:00
Michel Heusschen
fa32c6660c fix(web): album state after removing assets (#7745)
* fix(web): album state after removing assets

* refresh album on remove + simplify AlbumSummary
2024-03-08 14:03:37 -05:00
DeclanE
fe8c6b17a6 chore: rename "Library" to "External Library" in system settings (#7744)
* Change "Library" > "External Library" under system settings

This is intended to assist with any confusion regarding standard libraries

* Changed key from "library" to "external-library"

* Updated "Encode Clip" to "Smart Search"
2024-03-08 16:49:44 +00:00
Jason Rasmussen
89f6190fb0 refactor: search e2e (#7732) 2024-03-08 11:20:54 -05:00
Michel Heusschen
ffdd504008 refactor(web): admin and user signup forms (#7739) 2024-03-08 08:45:41 -05:00
dependabot[bot]
46597aac97 chore(deps): bump docker/build-push-action from 5.1.0 to 5.2.0 (#7737)
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5.1.0 to 5.2.0.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5.1.0...v5.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-08 08:41:39 -05:00
mmomjian
9b27a09131 docs: Update DB queries (#7730) 2024-03-07 23:28:51 -05:00
Jason Rasmussen
a50f125dd1 refactor: api validators (boolean and date) (#7709)
* refactor: api validators (boolean and date)

* chore: open api

* revert: time bucket change
2024-03-07 22:59:02 -05:00
Alex
753842745d Localizely: Translations update (#7715)
chore(mobile): translation update
2024-03-07 20:56:05 -06:00
martyfuhry
21caa06fa2 fix(mobile): Fixes large and small image cache (#7726)
Fixes large and small image cache
2024-03-07 20:55:50 -06:00
aviv926
7a7475ed67 docs: Update External Libraries Guide (#7723) 2024-03-07 18:49:30 -05:00
Sam Holton
dbb6a8dc2a fix(server): remove shared links during user delete (#7696)
* fix(server): remove shared links during user delete

* add delete cascade for shared links
2024-03-07 17:21:23 -05:00
Jonathan Jogenfors
a5a27594b8 docs: admin-only library settings (#7716)
* document admin-only library settings

* renew library guide
2024-03-07 21:27:02 +00:00
Jason Rasmussen
661409bac7 feat(server): create a person with optional values (#7706)
* feat: create person dto

* chore: open api

* fix: e2e

* fix: web usage
2024-03-07 15:34:57 -05:00
Jonathan Jogenfors
f1a8e385e9 deps(server): CVE-2024-28176 (#7717)
bumpety bump
2024-03-07 15:34:10 -05:00
renovate[bot]
a623556762 chore(deps): update dependency @playwright/test to v1.42.1 (#7684)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-07 15:31:45 -05:00
Alex The Bot
7dc5e0cc4f Version v1.98.0 2024-03-07 19:22:14 +00:00
Alex Tran
ba5d5256b1 Revert "Version v1.98.0"
This reverts commit 9b1a379fa6.
2024-03-07 12:04:54 -06:00
Alex Tran
307ffc990d fix(server): admin access to edit library 2024-03-07 12:03:21 -06:00
Alex The Bot
9b1a379fa6 Version v1.98.0 2024-03-07 17:40:40 +00:00
Jonathan Jogenfors
4cb0f37918 chore(server): Move library watcher to microservices (#7533)
* move watcher init to micro

* document watcher recovery

* chore: fix lint

* add try lock

* use global library watch lock

* fix: ensure lock stays on

* fix: mocks

* unit test for library watch lock

* move statement to correct test

* fix: correct return type of try lock

* fix: tests

* add library teardown

* add chokidar error handler

* make event strings an enum

* wait for event refactor

* refactor event type mocks

* expect correct error

* don't release lock in teardown

* chore: lint

* use enum

* fix mock

* fix lint

* fix watcher await

* remove await

* simplify typing

* remove async

* Revert "remove async"

This reverts commit 84ab5abac4.

* can now change watch settings at runtime

* fix lint

* only watch libraries if enabled

---------

Co-authored-by: mertalev <101130780+mertalev@users.noreply.github.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-03-07 11:36:53 -06:00
Alex
3278dcbcbe fix(web): save filename search in search filter box (#7704) 2024-03-07 10:16:47 -06:00
Jason Rasmussen
b733a29430 refactor: e2e (#7703)
* refactor: e2e

* fix: submodule check

* chore: extend startup timeout
2024-03-07 10:14:36 -05:00
Michel Heusschen
2dcd0e516f fix(server): add extension to filename migration (#7697) 2024-03-07 09:33:56 -05:00
Alex
e823b39579 fix(server): access face count when the value is undefined (#7694) 2024-03-06 23:21:10 -05:00
Alex
cd058fdafa chore(mobile,web): use originalFilename (#7692)
* chore(mobile,web): use originalFilename

* web

* remove unused code
2024-03-06 23:20:04 -05:00
Alex
1eea547aa2 chore(server): search filename using originalFileName (#7691) 2024-03-06 22:36:08 -05:00
martyfuhry
4323d18387 fix(mobile): Refactors exif bottom sheet to use widgets and fixes slow sliding up exif bottom sheet (#7671)
* Refactors exif bottom sheet to use widgets and fixes slow sliding up experience

format

* Refactors exif bottom sheet to use widgets and fixes slow sliding up experience

format

* Fixes people

* removes wrong exif bottom sheet

format

format

* Moved more widgets out of exit bottom sheet

format

* small styling

---------

Co-authored-by: Marty Fuhry <marty@fuhry.farm>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-03-07 03:27:33 +00:00
Mert
1ec5d612fa perf(server): use queries to refresh library assets (#7685)
* use queries instead of js

* missing await

* add mock methods

* fix test

* update sql

* linting
2024-03-06 21:23:10 -06:00
renovate[bot]
fcb990665c chore(deps): update base-image to v20240305 (major) (#7682)
chore(deps): update base-image to v20240305

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-06 22:22:55 -05:00
Mert
ffaa08e7ea chore(server): lower default max recognition distance for facial recognition (#7689)
lower default to 0.5
2024-03-06 22:20:38 -05:00
Michel Heusschen
5dd11ca17a fix(web): consistent modal escape behavior (#7677)
* fix(web): consistent modal escape behavior

* make onClose optional
2024-03-06 22:18:53 -05:00
Alex
3da2b05428 chore(server): save original file name with extension (#7679)
* chore(server): save original file name with extension

* extract extension

* update e2e test

* update e2e test

* download archive

* fix download archive appending name

* pr feedback

* remove unused code

* test

* unit test

* remove unused code

* migration

* noops

* pr feedback

* Update server/src/domain/download/download.service.ts

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

---------

Co-authored-by: Mert <101130780+mertalev@users.noreply.github.com>
2024-03-07 02:34:55 +00:00
Mert
f88343019d perf(web): optimize response sizes for initial page load (#7594) 2024-03-06 12:05:53 -05:00
Emanuel Bennici
ba12d92af3 feat(mobile): Add people list to exit bottom sheet (#6717)
* feat(mobile): Define constants as 'const'

* feat(mobile): Add people list to asset bottom sheet

Add a list of people per asset in the exif bottom sheet, like on the
web.

Currently the list of people is loaded by making a request each time to
the server. This is the MVP approach.
In the future, the people information can be synced like we're doing
with the assets.

* styling

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-03-06 11:15:54 -05:00
Michel Heusschen
52a52f9f40 fix(web): date input on chrome (#7669) 2024-03-06 05:47:15 -06:00
Sam Holton
9125999d1a feat(server,web): make user deletion delay configurable (#7663)
* feat(server,web): make user deletion delay configurable

* alphabetical order

* add min for user.deleteDelay in SettingInputField

* make config.user.deleteDelay SettingInputField min consistent format

* fix e2e test

* update description on user delete delay
2024-03-05 23:45:40 -06:00
Alex
52dfe5fc92 fix(server): stack info in asset response for mobile (#7346)
* fix(server): stack info in asset response for mobile

* fix(server): getAllAssets - do not filter by stack ID

* tet(server): GET /assets stack e2e

* chore(server): fix checks

* stack asset height

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
2024-03-06 05:44:56 +00:00
renovate[bot]
4c0bb2308c fix(deps): update machine-learning (#7634)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-06 00:24:33 -05:00
martyfuhry
4ef4cc8016 refactor(mobile): Refactor video player page and gallery bottom app bar (#7625)
* Fixes double video auto initialize issue and placeholder for video controller

* WIP unravel stack index

* Refactors video player controller

format

fixing video

format

Working

format

* Fixes hide on pause

* Got hiding when tapped working

* Hides controls when video starts and fixes placeholder for memory card

Remove prints

* Fixes show controls with microtask

* fix LivePhotos not playing

* removes unused function callbacks and moves wakelock

* Update motion video

* Fixing motion photo playing

* Renames to isPlayingVideo

* Fixes playing video on change

* pause on dispose

* fixing issues with sync between controls

* Adds gallery app bar

* Switches to memoized

* Fixes pause

* Revert "Switches to memoized"

This reverts commit 234e6741de.

* uses stateful widget

* Fixes double video play by using provider and new chewie video player

wip

format

Fixes motion photos

format

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-03-05 21:42:22 -06:00
Alex
2f53f6a62c feat(web): search by filename (#7624)
* Toggle to search by filename

* wild card search and pr feedback

* Pr feedback

* naming

* placeholder

* Create index

* pr feedback

* pr feedback

* Update web/src/lib/components/shared-components/search-bar/search-text-section.svelte

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

* pr feedback

* pr feedback

* pr feedback

* pr feedback

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-03-05 17:08:35 -06:00
Jonathan Jogenfors
ae46188753 chore(deps): bump sanitize-html, fixing CVE-2024-21501 (#7662)
* bump sanitize-html

* bump better
2024-03-05 17:35:52 -05:00
renovate[bot]
51f6b8f23b chore(deps): update dependency @types/cookie-parser to v1.4.7 (#7661)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 17:31:26 -05:00
Jonathan Jogenfors
5d377e5b0f chore(server): eslint await-thenable (#7545)
* await-thenable

* fix library watchers

* moar eslint

* fix test

* fix typo

* try to remove check void return

* fix checksVoidReturn

* move to domain utils

* remove eslint ignores

* chore: cleanup types

* chore: use logger

* fix: e2e

---------

Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com>
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-03-05 17:23:06 -05:00
Jason Rasmussen
972d5a3411 feat(server): deterministic download order (#7658) 2024-03-05 15:04:43 -06:00
renovate[bot]
8df63b7c94 fix(deps): update dependency archiver to v7 (#7622)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 12:26:54 -05:00
renovate[bot]
ee3b2a0cf5 chore(deps): update server (#7652)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 12:16:41 -05:00
Jason Rasmussen
8988d3f886 chore: download e2e (#7651) 2024-03-05 12:07:46 -05:00
bo0tzz
4dc0fc45e7 feat(cli): Use well-known endpoint to resolve API (#6733)
* feat(cli): use immich-well-known

* chore: e2e test

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-03-05 12:06:24 -05:00
renovate[bot]
1c93ef1916 chore(deps): update dependency svelte-check to v3.6.6 (#7650)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 17:01:12 +00:00
renovate[bot]
9bf1d87e35 chore(deps): update dependency @types/node to v20.11.24 (#7649)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 16:30:51 +00:00
renovate[bot]
31b823058d chore(deps): update dependency @types/node to v20.11.24 (#7645)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 11:29:04 -05:00
Jason Rasmussen
9a2e0e8962 chore(cli): remove unused packages (#7648) 2024-03-05 16:18:53 +00:00
renovate[bot]
e0ae936496 chore(deps): update dependency @types/node to v20.11.24 (#7646)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 15:58:40 +00:00
Alex
0675389aae Revert "fix(web): prevent duplicate calls to time bucket endpoint" (#7644)
Revert "fix(web): prevent duplicate calls to time bucket endpoint (#7563)"

This reverts commit 8b02f18e99.
2024-03-05 09:44:33 -06:00
Michel Heusschen
facd0bc3a4 Revert "perf(web): optimize date groups" (#7638)
Revert "perf(web): optimize date groups (#7593)"

This reverts commit 762c4684f8.
2024-03-05 09:43:24 -06:00
renovate[bot]
967019d9e0 chore(deps): update dependency @types/node to v20.11.23 (#7641)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 07:57:22 -05:00
renovate[bot]
e5da735918 chore(deps): update @immich/cli (#7640)
chore(deps): update dependency @types/node to v20.11.23

Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 07:57:03 -05:00
waclaw66
9b3f60ffde fix(web): prettify album download filename (#7637)
fix(server): pretify download filename
2024-03-05 07:56:12 -05:00
renovate[bot]
a5d19bc945 fix(deps): update server (#7635)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-05 03:48:00 +00:00
renovate[bot]
9995647d63 chore(deps): update dependency prettier-plugin-svelte to v3.2.2 (#7633)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 22:43:55 -05:00
Nicholas Flamy
70881bc97f Update truenas.md with permission info. (#7606)
* Update truenas.md with permission info.

This is very important information about permissions on datasets used by immich.

* Update truenas.md

* Update truenas.md with proper formatting
2024-03-05 02:51:20 +00:00
renovate[bot]
dbf0ddf3a7 chore(deps): update @immich/cli (#7629)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 21:36:53 -05:00
renovate[bot]
2e62e3a0ca chore(deps): update dependency @types/node to v20.11.22 (#7630)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 21:36:01 -05:00
martin
6ab404597c fix(server): incorrect number of assets for a person (#7602)
* fix: incorrect number of assets

* fix: tests

* pr feedback

* fix: e2e test

* fix: e2e test

* fix: e2e test

* feat: more tests
2024-03-04 18:11:54 -05:00
renovate[bot]
5bc13c49a4 chore(deps): update dependency @types/node to v20.11.22 (#7610)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-04 17:42:26 -05:00
martin
935ddf3fbd fix(server, web): prevent reload when liking an asset (#7589)
pr feedback
2024-03-04 17:41:10 -05:00
Sam Holton
87c3d886ff feat(web): use lib/utils/copyToClipboard for share link (#7603) 2024-03-04 17:39:58 -05:00
Robert Vollmer
de71d8e0a3 fix(server): regular version check (#7620)
`dt.diffNow()` equals `dt.diff(DateTime.now())`, so it returns a
negative number when `dt` is in the past (which it always is in this
case).

Therefore we could only get over the condition during startup (when
`this.releaseVersionCheckedAt` isn't set yet), effectively breaking
update notifications while the server is running.
2024-03-04 08:42:22 -06:00
Sam Holton
7ef202c8b2 feat(server, web): add checkbox to create user screen for shouldChang… (#7598)
feat(server, web): add checkbox to create user screen for shouldChangePassword
2024-03-03 23:40:03 -06:00
DawidPietrykowski
e8b001f62f feat: preloading of machine learning models (#7540) 2024-03-03 19:48:56 -05:00
Mert
762c4684f8 perf(web): optimize date groups (#7593)
* optimize date groups

* remove `.values()`

* remove console.log

* remove if condition

* remove console.log

* remove outdated comment

* revert dynamic import
2024-03-03 16:12:52 -06:00
Mert
2fa10a254c feat(web): improve alt text (#7596)
* alt text

* memory lane alt text

* revert sql generator change

* use getAltText

* oops

* handle large number of people in asset

* nit

* add aria-label to search button

* update api

* fixed tests

* fixed typing

* fixed spacing

* fix displaying null
2024-03-03 16:42:17 -05:00
martin
07c926bb12 fix: bump version pipeline (#7586)
* fix: bump version pipeline

* pr feedback
2024-03-03 15:17:21 -06:00
renovate[bot]
3d410ff7dc chore(deps): update dependency @playwright/test to v1.42.0 (#7601)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-03 15:16:03 -06:00
Sam Holton
2bb7b3e60f feat(web): add share link on asset-viewer (#7595)
* feat(web): add share link on asset-viewer

* PR feedback: move download to context, make share first button
2024-03-03 15:15:35 -06:00
Sam Holton
29a4389aac feat(web): show user quota on server stats page (#7591) 2024-03-03 15:47:00 -05:00
martin
8ce18b3403 feat(machine-learning): support cuda 12 (#7569)
* feat: support cuda12

* fix: group optional

* move to cuda 12

* pr feedback
2024-03-02 23:36:16 -05:00
renovate[bot]
fd3503e77d chore(deps): update dependency @types/pg to v8.11.2 (#7585)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-03-02 18:08:41 -05:00
martin
ebe7a14c14 fix(server): prevent leaking isFavorite and isArchived info (#7580)
* fix: prevent leaking favorites info

* add e2e test

* fix: e2e test

* fix: isArchived

* fix: keep old version
2024-03-02 18:01:24 -05:00
Sam Holton
f03381a5b1 feat(server): allow oauth claim to set 0 for no quota (#7581)
* feat(server): allow oauth claim to set 0 for no quota

* PR feedback to remove extra objects from user.stub.ts
2024-03-02 14:18:56 -06:00
martin
8d44afe915 feat(web): ascending order for slideshow (#7502)
* feat: ascending order for slideshow

* feat: use dropdown

* rename

* fix: size

* pr feedback

* fix: hide text on small screen

* Wording

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-03-02 09:50:02 -06:00
martin
db455060f0 fix(web): show history search box only when needed (#7544)
show history search box only when needed
2024-03-02 09:38:34 -06:00
Ignacy Kajdan
b63b42d3d7 docs: fix the database name env variable (#7576) 2024-03-02 08:58:07 -05:00
Michel Heusschen
a4e6c43823 perf(web): asset delete (#7555)
* perf(web): asset delete

* update asset delete on search page

* don't use arrow function in class
2024-03-01 19:49:31 -05:00
Sam Holton
7303fab9d9 feat(server/web): add oauth defaultStorageQuota and storageQuotaClaim (#7548)
* feat(server/web): add oauth defaultStorageQuota and storageQuotaClaim

* feat(server/web): fix format and use domain.util constants

* address some pr feedback

* simplify oauth storage quota logic

* adding tests and pr feedback

* chore: cleanup

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
2024-03-01 19:46:07 -05:00
Mert
8b02f18e99 fix(web): prevent duplicate calls to time bucket endpoint (#7563) 2024-03-01 14:16:07 -05:00
waclaw66
670a3838a3 fix(mobile): bottom bar Upload translation (#7553)
Co-authored-by: Václav Nováček <waclaw@waclaw.cz>
2024-03-01 11:24:55 -06:00
Simon Séhier
3e06062974 fix(immich-admin): only 1st argument was passed (#7552) 2024-03-01 07:34:59 -05:00
martin
3b772a772c fix(web): immich version (#7541)
* fix: web version

* update package-lock.json

* update typescript-sdk
2024-03-01 01:26:50 -06:00
Ben McCann
55ecfafa82 chore(web): fix eslint setup in VS Code (#7543) 2024-02-29 19:28:54 -05:00
Michel Heusschen
c89d91e006 feat: filter people when using smart search (#7521) 2024-02-29 16:14:48 -05:00
Jason Rasmussen
15a4a4aaaa chore: remove unused upload property (#7535)
* chore: remove isExternal

* chore: open-api
2024-02-29 16:02:08 -05:00
Jason Rasmussen
3d25d91e77 refactor: library e2e (#7538)
* refactor: library e2e

* refactor: remove before each usages
2024-02-29 15:10:08 -05:00
Jonathan Jogenfors
efa6efd200 feat(server,web): remove external path nonsense and make libraries admin-only (#7237)
* remove external path

* open-api

* make sql

* move library settings to admin panel

* Add documentation

* show external libraries only

* fix library list

* make user library settings look good

* fix test

* fix tests

* fix tests

* can pick user for library

* fix tests

* fix e2e

* chore: make sql

* Use unauth exception

* delete user library list

* cleanup

* fix e2e

* fix await lint

* chore: remove unused code

* chore: cleanup

* revert docs

* fix: is admin stuff

* table alignment

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2024-02-29 12:35:37 -06:00
Michel Heusschen
369acc7bea fix(web): asset disappears from album after metadata edit (#7520) 2024-02-29 11:44:30 -06:00
Jason Rasmussen
100363c7be refactor(e2e): use better dummy assets (#7536) 2024-02-29 12:07:01 -05:00
Jason Rasmussen
af0de1a768 chore: linting (#7532)
* chore: linting

* fix: broken tests

* fix: formatting
2024-02-29 11:26:55 -05:00
Jason Rasmussen
09a7291527 refactor(web): drop axios (#7490)
* refactor: downloadApi

* refactor: assetApi

* chore: drop axios

* chore: tidy up

* chore: fix exports

* fix: show notification when download starts
2024-02-29 11:22:39 -05:00
Jason Rasmussen
bb3d81bfc5 chore: build tweaks (#7484) 2024-02-29 09:22:25 -05:00
dependabot[bot]
f1331905f0 chore(deps): bump tj-actions/verify-changed-files from 18 to 19 (#7524)
Bumps [tj-actions/verify-changed-files](https://github.com/tj-actions/verify-changed-files) from 18 to 19.
- [Release notes](https://github.com/tj-actions/verify-changed-files/releases)
- [Changelog](https://github.com/tj-actions/verify-changed-files/blob/main/HISTORY.md)
- [Commits](https://github.com/tj-actions/verify-changed-files/compare/v18...v19)

---
updated-dependencies:
- dependency-name: tj-actions/verify-changed-files
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-29 07:56:20 -05:00
renovate[bot]
7eb8e2ff9c chore(deps): update dependency @types/pg to v8.11.1 (#7522)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2024-02-29 07:54:48 -05:00
Mert
dc7a329cc8 fix(server): only queue ml / transcoding jobs after thumbnail generation on upload (#7516) 2024-02-28 23:23:21 -05:00
Mert
11de526bcf fix(server): re-add mimalloc (#7511)
add mimalloc
2024-02-28 18:23:48 -05:00
Alex
2e56e777ce chore: post release tasks 2024-02-28 16:49:02 -06:00
492 changed files with 12526 additions and 30020 deletions

View File

@@ -1,30 +1,31 @@
.vscode/
.github/
.git/
design/
docker/
docs/
e2e/
fastlane/
machine-learning/
misc/
mobile/
server/node_modules/
cli/coverage/
cli/dist/
cli/node_modules/
open-api/typescript-sdk/build/
open-api/typescript-sdk/node_modules/
server/coverage/
server/.reverse-geocoding-dump/
server/node_modules/
server/upload/
server/dist/
server/www/
server/test/assets/
web/node_modules/
web/coverage/
web/.svelte-kit
web/build/
cli/node_modules/
cli/.reverse-geocoding-dump/
cli/upload/
cli/dist/
e2e/
open-api/typescript-sdk/node_modules/
open-api/typescript-sdk/build/

View File

@@ -16,4 +16,4 @@ max_line_length = off
trim_trailing_whitespace = false
[*.{yml,yaml}]
quote_type = double
quote_type = single

2
.gitattributes vendored
View File

@@ -8,8 +8,6 @@ mobile/openapi/.openapi-generator/FILES linguist-generated=true
mobile/lib/**/*.g.dart -diff -merge
mobile/lib/**/*.g.dart linguist-generated=true
open-api/typescript-sdk/axios-client/**/* -diff -merge
open-api/typescript-sdk/axios-client/**/* linguist-generated=true
open-api/typescript-sdk/fetch-client.ts -diff -merge
open-api/typescript-sdk/fetch-client.ts linguist-generated=true

View File

@@ -87,7 +87,7 @@ jobs:
type=raw,value=latest,enable=${{ github.event_name == 'workflow_dispatch' }}
- name: Build and push image
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v5.2.0
with:
file: cli/Dockerfile
platforms: linux/amd64,linux/arm64

View File

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

View File

@@ -35,7 +35,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: "recursive"
submodules: 'recursive'
- name: Run e2e tests
run: make server-e2e-jobs
@@ -184,7 +184,7 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
with:
submodules: "recursive"
submodules: 'recursive'
- name: Setup Node
uses: actions/setup-node@v4
@@ -194,25 +194,40 @@ jobs:
- name: Run setup typescript-sdk
run: npm ci && npm run build
working-directory: ./open-api/typescript-sdk
if: ${{ !cancelled() }}
- name: Run setup cli
run: npm ci && npm run build
working-directory: ./cli
if: ${{ !cancelled() }}
- name: Install dependencies
run: npm ci
if: ${{ !cancelled() }}
- name: Run linter
run: npm run lint
if: ${{ !cancelled() }}
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
run: npx playwright install --with-deps
run: npx playwright install --with-deps chromium
if: ${{ !cancelled() }}
- name: Docker build
run: docker compose build
if: ${{ !cancelled() }}
- name: Run e2e tests (api & cli)
run: npm run test
if: ${{ !cancelled() }}
- name: Run e2e tests (web)
run: npx playwright test
if: ${{ !cancelled() }}
mobile-unit-tests:
name: Mobile
@@ -222,8 +237,8 @@ jobs:
- name: Setup Flutter SDK
uses: subosito/flutter-action@v2
with:
channel: "stable"
flutter-version: "3.16.9"
channel: 'stable'
flutter-version: '3.16.9'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1
@@ -241,7 +256,7 @@ jobs:
- uses: actions/setup-python@v5
with:
python-version: 3.11
cache: "poetry"
cache: 'poetry'
- name: Install dependencies
run: |
poetry install --with dev --with cpu
@@ -279,7 +294,7 @@ jobs:
- name: Run API generation
run: make open-api
- name: Find file changes
uses: tj-actions/verify-changed-files@v18
uses: tj-actions/verify-changed-files@v19
id: verify-changed-files
with:
files: |
@@ -334,7 +349,7 @@ jobs:
run: npm run typeorm:migrations:generate ./src/infra/migrations/TestMigration
- name: Find file changes
uses: tj-actions/verify-changed-files@v18
uses: tj-actions/verify-changed-files@v19
id: verify-changed-files
with:
files: |
@@ -352,7 +367,7 @@ jobs:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
- name: Find file changes
uses: tj-actions/verify-changed-files@v18
uses: tj-actions/verify-changed-files@v19
id: verify-changed-sql-files
with:
files: |

2085
cli/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -14,7 +14,6 @@
],
"devDependencies": {
"@immich/sdk": "file:../open-api/typescript-sdk",
"@testcontainers/postgresql": "^10.7.1",
"@types/byte-size": "^8.1.0",
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",

View File

@@ -66,8 +66,8 @@ class Asset {
assetData: new File([await fs.openAsBlob(this.path)], basename(this.path)),
deviceAssetId: this.deviceAssetId,
deviceId: 'CLI',
fileCreatedAt: this.fileCreatedAt,
fileModifiedAt: this.fileModifiedAt,
fileCreatedAt: this.fileCreatedAt.toISOString(),
fileModifiedAt: this.fileModifiedAt.toISOString(),
isFavorite: String(false),
};
const formData = new FormData();

View File

@@ -3,6 +3,7 @@ import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/p
import path from 'node:path';
import yaml from 'yaml';
import { ImmichApi } from './api.service';
class LoginError extends Error {
constructor(message: string) {
super(message);
@@ -14,14 +15,12 @@ class LoginError extends Error {
}
export class SessionService {
readonly configDirectory!: string;
readonly authPath!: string;
constructor(configDirectory: string) {
this.configDirectory = configDirectory;
this.authPath = path.join(configDirectory, '/auth.yml');
private get authPath() {
return path.join(this.configDirectory, '/auth.yml');
}
constructor(private configDirectory: string) {}
async connect(): Promise<ImmichApi> {
let instanceUrl = process.env.IMMICH_INSTANCE_URL;
let apiKey = process.env.IMMICH_API_KEY;
@@ -48,6 +47,8 @@ export class SessionService {
}
}
instanceUrl = await this.resolveApiEndpoint(instanceUrl);
const api = new ImmichApi(instanceUrl, apiKey);
const pingResponse = await api.pingServer().catch((error) => {
@@ -62,7 +63,9 @@ export class SessionService {
}
async login(instanceUrl: string, apiKey: string): Promise<ImmichApi> {
console.log('Logging in...');
console.log(`Logging in to ${instanceUrl}`);
instanceUrl = await this.resolveApiEndpoint(instanceUrl);
const api = new ImmichApi(instanceUrl, apiKey);
@@ -83,7 +86,7 @@ export class SessionService {
await writeFile(this.authPath, yaml.stringify({ instanceUrl, apiKey }), { mode: 0o600 });
console.log('Wrote auth info to ' + this.authPath);
console.log(`Wrote auth info to ${this.authPath}`);
return api;
}
@@ -98,4 +101,18 @@ export class SessionService {
console.log('Successfully logged out');
}
private async resolveApiEndpoint(instanceUrl: string): Promise<string> {
const wellKnownUrl = new URL('.well-known/immich', instanceUrl);
try {
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
const endpoint = new URL(wellKnown.api.endpoint, instanceUrl).toString();
if (endpoint !== instanceUrl) {
console.debug(`Discovered API at ${endpoint}`);
}
return endpoint;
} catch {
return instanceUrl;
}
}
}

View File

@@ -2,7 +2,7 @@
# - https://immich.app/docs/developer/setup
# - https://immich.app/docs/developer/troubleshooting
version: "3.8"
version: '3.8'
name: immich-dev
@@ -30,7 +30,7 @@ x-server-build: &server-common
services:
immich-server:
container_name: immich_server
command: [ "/usr/src/app/bin/immich-dev", "immich" ]
command: ['/usr/src/app/bin/immich-dev', 'immich']
<<: *server-common
ports:
- 3001:3001
@@ -41,7 +41,7 @@ services:
immich-microservices:
container_name: immich_microservices
command: [ "/usr/src/app/bin/immich-dev", "microservices" ]
command: ['/usr/src/app/bin/immich-dev', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml
@@ -57,7 +57,7 @@ services:
image: immich-web-dev:latest
build:
context: ../web
command: [ "/usr/src/app/bin/immich-web" ]
command: ['/usr/src/app/bin/immich-web']
env_file:
- .env
ports:

View File

@@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
name: immich-prod
@@ -17,7 +17,7 @@ x-server-build: &server-common
services:
immich-server:
container_name: immich_server
command: [ "start.sh", "immich" ]
command: ['start.sh', 'immich']
<<: *server-common
ports:
- 2283:3001
@@ -27,7 +27,7 @@ services:
immich-microservices:
container_name: immich_microservices
command: [ "start.sh", "microservices" ]
command: ['start.sh', 'microservices']
<<: *server-common
# extends:
# file: hwaccel.transcoding.yml

View File

@@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
#
# WARNING: Make sure to use the docker-compose.yml of the current release:
@@ -14,7 +14,7 @@ services:
immich-server:
container_name: immich_server
image: ghcr.io/immich-app/immich-server:${IMMICH_VERSION:-release}
command: [ "start.sh", "immich" ]
command: ['start.sh', 'immich']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
@@ -33,7 +33,7 @@ services:
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/hardware-transcoding
# file: hwaccel.transcoding.yml
# service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding
command: [ "start.sh", "microservices" ]
command: ['start.sh', 'microservices']
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro

View File

@@ -67,9 +67,11 @@ Once you have a new OAuth client application configured, Immich can be configure
| Client Secret | string | (required) | Required. Client Secret (previous step) |
| Scope | string | openid email profile | Full list of scopes to send with the request (space delimited) |
| Signing Algorithm | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label |
| Storage Quota Claim | string | immich_quota | Claim mapping for the user's storage |
| Default Storage Quota (GiB) | number | 0 | Default quota for user without storage quota claim (Enter 0 for unlimited quota) |
| Button Text | string | Login with OAuth | Text for the OAuth button on the web |
| Auto Register | boolean | true | When true, will automatically register a user the first time they sign in |
| Storage Claim | string | preferred_username | Claim mapping for the user's storage label |
| [Auto Launch](#auto-launch) | boolean | false | When true, will skip the login page and automatically start the OAuth login process |
| [Mobile Redirect URI Override](#mobile-redirect-uri) | URL | (empty) | Http(s) alternative mobile redirect URI |

View File

@@ -38,7 +38,7 @@ Note: Either a manual or scheduled library scan must have been performed to iden
In all above scan methods, Immich will check if any files are missing. This can happen if files are deleted, or if they are on a storage location that is currently unavailable, like a network drive that is not mounted, or a USB drive that has been unplugged. In order to prevent accidental deletion of assets, Immich will not immediately delete an asset from the library if the file is missing. Instead, the asset will be internally marked as offline and will still be visible in the main timeline. If the file is moved back to its original location and the library is scanned again, the asset will be restored.
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under user account settings > libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
Finally, files can be deleted from Immich via the `Remove Offline Files` job. This job can be found by the three dots menu for the associated external storage that was configured under Administration > Libraries (the same location described at [create external libraries](#create-external-libraries)). When this job is run, any assets marked as offline will then be removed from Immich. Run this job whenever files have been deleted from the file system and you want to remove them from Immich.
### Import Paths
@@ -50,8 +50,6 @@ If the import paths are edited in a way that an external file is no longer in an
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
- Is the external path set correctly? Each import path must be contained in the external path.
- Make sure the external path does not contain spaces
- In the docker-compose file, are the volumes mounted correctly?
- Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file?
@@ -61,18 +59,6 @@ Sometimes, an external library will not scan correctly. This can happen if immic
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
### Security Considerations
:::caution
Please read and understand this section before setting external paths, as there are important security considerations.
:::
For security purposes, each Immich user is disallowed to add external files by default. This is to prevent devastating [path traversal attacks](https://owasp.org/www-community/attacks/Path_Traversal). An admin can allow individual users to use external path feature via the `external path` setting found in the admin panel. Without the external path restriction, a user can add any image or video file on the Immich host filesystem to be imported into Immich, potentially allowing sensitive data to be accessed. If you are running Immich as root in your Docker setup (which is the default), all external file reads are done with root privileges. This is particularly dangerous if the Immich host is a shared server.
With the `external path` set, a user is restricted to accessing external files to files or directories within that path. The Immich admin should still be careful not set the external path too generously. For example, `user1` wants to read their photos in to `/home/user1`. A lazy admin sets that user's external path to `/home/` since it "gets the job done". However, that user will then be able to read all photos in `/home/user2/private-photos`, too! Please set the external path as specific as possible. If multiple folders must be added, do this using the docker volume mount feature described below.
### Exclusion Patterns
By default, all files in the import paths will be added to the library. If there are files that should not be added, exclusion patterns can be used to exclude them. Exclusion patterns are glob patterns are matched against the full file path. If a file matches an exclusion pattern, it will not be added to the library. Exclusion patterns can be added in the Scan Settings page for each library. Under the hood, Immich uses the [glob](https://www.npmjs.com/package/glob) package to match patterns, so please refer to [their documentation](https://github.com/isaacs/node-glob#glob-primer) to see what patterns are supported.
@@ -90,6 +76,16 @@ This feature - currently hidden in the config file - is considered experimental
If your photos are on a network drive, automatic file watching likely won't work. In that case, you will have to rely on a periodic library refresh to pull in your changes.
#### Troubleshooting
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
```
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'
```
In rare cases, the library watcher can hang, preventing Immich from starting up. In this case, disable the library watcher in the configuration file. If the watcher is enabled from within Immich, the app must be started without the microservices. Disable the microservices in the docker compose file, start Immich, disable the library watcher in the admin settings, close Immich, re-enable the microservices, and then Immich can be started normally.
### Nightly job
There is an automatic job that's run once a day and refreshes all modified files in all libraries as well as cleans up any libraries stuck in deletion.
@@ -135,27 +131,13 @@ The `ro` flag at the end only gives read-only access to the volumes. While Immic
_Remember to bring the container `docker compose down/up` to register the changes. Make sure you can see the mounted path in the container._
:::
### Set External Path
Only an admin can do this.
- Navigate to `Administration > Users` page on the web.
- Click on the user edit button.
- Set `/mnt/media` to be the external path. This folder will only contain the three folders that we want to import, so nothing else can be accessed.
:::note
Spaces in the internal path aren't currently supported.
You must import it as:
`..:/mnt/media/my-media:ro`
instead of
`..:/mnt/media/my media:ro`
:::
### Create External Libraries
- Click on your user name in the top right corner -> Account Settings
- Click on Libraries
These actions must be performed by the Immich administrator.
- Click on Administration -> Libraries
- Click on Create External Library
- Select which user owns the library, this can not be changed later
- Click the drop-down menu on the newly created library
- Click on Rename Library and rename it to "Christmas Trip"
- Click Edit Import Paths
@@ -166,7 +148,7 @@ NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/
Next, we'll add an exclusion pattern to filter out raw files.
- Click the drop-down menu on the newly christmas library
- Click the drop-down menu on the newly-created Christmas library
- Click on Manage
- Click on Scan Settings
- Click on Add Exclusion Pattern

View File

@@ -13,7 +13,7 @@ Run `docker exec -it immich_postgres psql immich <DB_USERNAME>` to connect to th
## Assets
:::note
The `"originalFileName"` column is the name of the uploaded file _without_ the extension.
The `"originalFileName"` column is the name of the file at time of upload, including the extension.
:::
```sql title="Find by original filename"
@@ -40,6 +40,10 @@ SELECT * FROM "assets" where "livePhotoVideoId" IS NOT NULL;
SELECT "assets".* FROM "exif" LEFT JOIN "assets" ON "assets"."id" = "exif"."assetId" WHERE "exif"."assetId" IS NULL;
```
```sql title="size < 100,000 bytes, smallest to largest"
SELECT * FROM "assets" JOIN "exif" ON "assets"."id" = "exif"."assetId" WHERE "exif"."fileSizeInByte" < 100000 ORDER BY "exif"."fileSizeInByte" ASC;
```
```sql title="Without thumbnails"
SELECT * FROM "assets" WHERE "assets"."resizePath" IS NULL OR "assets"."webpPath" IS NULL;
```

View File

@@ -28,6 +28,10 @@ On my computer, for example, I use this path:
EXTERNAL_PATH=/home/tenino/photos
```
:::info EXTERNAL_PATH design
The design choice to put the EXTERNAL_PATH into .env rather than put two copies of the absolute path in the yml file in order to make everything easier, so if you have two copies of the same path that have to be kept in sync, then someday later when you move the data, update only one of the paths, without everything will break mysteriously.
:::
Restart Immich.
```
@@ -35,47 +39,26 @@ docker compose down
docker compose up -d
```
# Set the External Path
# Create the library
In the Immich web UI:
- click the **Administration** link in the upper right corner.
<img src={require('./img/administration-link.png').default} width="50%" title="Administration link" />
- Select the **Users** tab
<img src={require('./img/users-tab.png').default} width="50%" title="Users tab" />
- Select the **External Libraries** tab
<img src={require('./img/external-libraries.png').default} width="50%" title="External Libraries tab" />
- Select the **pencil** next to your user ID
<img src={require('./img/pencil.png').default} width="50%" title="Pencil" />
- Click the **Create Library** button
<img src={require('./img/create-external-library.png').default} width="50%" title="Create Library button" />
- Fill in the **External Path** field with `/usr/src/app/external`
<img src={require('./img/external-path.png').default} width="50%" title="External Path field" />
Notice this matches the path _inside the container_ where we mounted your photos.
The purpose of the external path field is for administrators who have multiple users
on their Immich instance. It lets you prevent other authorized users from
navigating to your external library.
# Import the library
In the Immich web UI:
- Click your user avatar in the upper-right corner (circle with your initials)
<img src={require('./img/user-avatar.png').default} width="50%" title="User avatar" />
- Click **Account Settings**
<img src={require('./img/account-settings.png').default} width="50%" title="Account Settings button" />
- Click to expand **Libraries**
<img src={require('./img/libraries-dropdown.png').default} width="50%" title="Libraries dropdown" />
- Click the **Create External Library** button
<img src={require('./img/create-external-library-button.png').default} width="50%" title="Create External Library button" />
- In the dialog, select which user should own the new library
<img src={require('./img/library-owner.png').default} width="50%" title="Library owner diaglog" />
- Click the three-dots menu and select **Edit Import Paths**
<img src={require('./img/edit-import-paths.png').default} width="50%" title="Edit Import Paths menu option" />
- Click \*_Add path_
- Click Add path
<img src={require('./img/add-path-button.png').default} width="50%" title="Add Path button" />
- Enter **/usr/src/app/external** as the path and click Add

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -95,13 +95,16 @@ The default configuration looks like this:
"issuerUrl": "",
"clientId": "",
"clientSecret": "",
"mobileOverrideEnabled": false,
"mobileRedirectUri": "",
"scope": "openid email profile",
"signingAlgorithm": "RS256",
"storageLabelClaim": "preferred_username",
"storageQuotaClaim": "immich_quota",
"defaultStorageQuota": 0,
"buttonText": "Login with OAuth",
"autoRegister": true,
"autoLaunch": false
"autoLaunch": false,
"mobileOverrideEnabled": false,
"mobileRedirectUri": ""
},
"passwordLogin": {
"enabled": true
@@ -125,6 +128,9 @@ The default configuration looks like this:
"theme": {
"customCss": ""
},
"user": {
"deleteDelay": 7
},
"library": {
"scan": {
"enabled": true,

View File

@@ -67,7 +67,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
| `DB_PORT` | Database Port | `5432` | server, microservices |
| `DB_USERNAME` | Database User | `postgres` | server, microservices |
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices |
| `DB_DATABASE` | Database Name | `immich` | server, microservices |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices |
| `DB_VECTOR_EXTENSION`<sup>\*1</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
\*1: This setting cannot be changed after the server has successfully started up
@@ -124,16 +124,18 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Services |
| :----------------------------------------------- | :----------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
| Variable | Description | Default | Services |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` | machine learning |
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.

View File

@@ -28,6 +28,10 @@ Or before beginning app installation, [create the datasets](https://www.truenas.
Immich requires seven datasets: **library**, **pgBackup**, **pgData**, **profile**, **thumbs**, **uploads**, and **video**.
You can organize these as one parent with seven child datasets, for example `mnt/tank/immich/library`, `mnt/tank/immich/pgBackup`, and so on.
:::info Permissions
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
:::
## Installing the Immich Application
To install the **Immich** application, go to **Apps**, click **Discover Apps**, either begin typing Immich into the search field or scroll down to locate the **Immich** application widget.

View File

@@ -12231,7 +12231,7 @@
"mime-format": "2.0.0",
"mime-types": "2.1.27",
"postman-url-encoder": "2.1.3",
"sanitize-html": "^2.11.0",
"sanitize-html": "^2.12.1",
"semver": "^7.5.4",
"uuid": "3.4.0"
}
@@ -14762,9 +14762,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
},
"node_modules/sanitize-html": {
"version": "2.11.0",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.11.0.tgz",
"integrity": "sha512-BG68EDHRaGKqlsNjJ2xUB7gpInPA8gVx/mvjO743hZaeMCZ2DwzW7xvsqZ+KNU4QKwj86HJ3uu2liISf2qBBUA==",
"version": "2.12.1",
"resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.12.1.tgz",
"integrity": "sha512-Plh+JAn0UVDpBRP/xEjsk+xDCoOvMBwQUf/K+/cBAVuTbtX8bj2VB7S1sL1dssVpykqp0/KPSesHrqXtokVBpA==",
"dependencies": {
"deepmerge": "^4.2.2",
"escape-string-regexp": "^4.0.0",

31
e2e/.eslintrc.cjs Normal file
View File

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

16
e2e/.prettierignore Normal file
View File

@@ -0,0 +1,16 @@
.DS_Store
node_modules
/build
/package
.env
.env.*
!.env.example
*.md
*.json
coverage
dist
# Ignore files for PNPM, NPM and YARN
pnpm-lock.yaml
package-lock.json
yarn.lock

8
e2e/.prettierrc Normal file
View File

@@ -0,0 +1,8 @@
{
"singleQuote": true,
"trailingComma": "all",
"printWidth": 120,
"semi": true,
"organizeImportsSkipDestructiveCodeActions": true,
"plugins": ["prettier-plugin-organize-imports"]
}

View File

@@ -1,4 +1,4 @@
version: "3.8"
version: '3.8'
name: immich-e2e
@@ -16,6 +16,7 @@ x-server-build: &server-common
- IMMICH_MACHINE_LEARNING_ENABLED=false
volumes:
- upload:/usr/src/app/upload
- ../server/test/assets:/data/assets
depends_on:
- redis
- database
@@ -23,14 +24,14 @@ x-server-build: &server-common
services:
immich-server:
container_name: immich-e2e-server
command: [ "./start.sh", "immich" ]
command: ['./start.sh', 'immich']
<<: *server-common
ports:
- 2283:3001
immich-microservices:
container_name: immich-e2e-microservices
command: [ "./start.sh", "microservices" ]
command: ['./start.sh', 'microservices']
<<: *server-common
redis:

2250
e2e/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,9 +5,14 @@
"main": "index.js",
"type": "module",
"scripts": {
"test": "vitest --config vitest.config.ts",
"test": "vitest --run",
"test:watch": "vitest",
"test:web": "npx playwright test",
"start:web": "npx playwright test --ui"
"start:web": "npx playwright test --ui",
"format": "prettier --check .",
"format:fix": "prettier --write .",
"lint": "eslint \"src/**/*.ts\" --max-warnings 0",
"lint:fix": "npm run lint -- --fix"
},
"keywords": [],
"author": "",
@@ -19,11 +24,21 @@
"@types/luxon": "^3.4.2",
"@types/node": "^20.11.17",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
"@types/supertest": "^6.0.2",
"@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0",
"@vitest/coverage-v8": "^1.3.0",
"eslint": "^8.57.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-unicorn": "^51.0.1",
"exiftool-vendored": "^24.5.0",
"luxon": "^3.4.4",
"pg": "^8.11.3",
"pngjs": "^7.0.0",
"prettier": "^3.2.5",
"prettier-plugin-organize-imports": "^3.2.4",
"socket.io-client": "^4.7.4",
"supertest": "^6.3.4",
"typescript": "^5.3.3",

View File

@@ -9,7 +9,7 @@ import {
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
@@ -20,18 +20,14 @@ describe('/activity', () => {
let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>
create(
{ activityCreateDto: dto },
{ headers: asBearerAuth(accessToken || admin.accessToken) },
);
create({ activityCreateDto: dto }, { headers: asBearerAuth(accessToken || admin.accessToken) });
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await utils.resetDatabase();
admin = await apiUtils.adminSetup();
nonOwner = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
asset = await apiUtils.createAsset(admin.accessToken);
admin = await utils.adminSetup();
nonOwner = await utils.userSetup(admin.accessToken, createUserDto.user1);
asset = await utils.createAsset(admin.accessToken);
album = await createAlbum(
{
createAlbumDto: {
@@ -45,7 +41,7 @@ describe('/activity', () => {
});
beforeEach(async () => {
await dbUtils.reset(['activity']);
await utils.resetDatabase(['activity']);
});
describe('GET /activity', () => {
@@ -56,13 +52,9 @@ describe('/activity', () => {
});
it('should require an albumId', async () => {
const { status, body } = await request(app)
.get('/activity')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/activity').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid albumId', async () => {
@@ -71,9 +63,7 @@ describe('/activity', () => {
.query({ albumId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should reject an invalid assetId', async () => {
@@ -82,9 +72,7 @@ describe('/activity', () => {
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])),
);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
});
it('should start off empty', async () => {
@@ -160,9 +148,7 @@ describe('/activity', () => {
});
it('should filter by userId', async () => {
const [reaction] = await Promise.all([
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
const response1 = await request(app)
.get('/activity')
@@ -215,9 +201,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.invalid });
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])),
);
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
});
it('should require a comment when type is comment', async () => {
@@ -226,12 +210,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest([
'comment must be a string',
'comment should not be empty',
]),
);
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
});
it('should add a comment to an album', async () => {
@@ -271,9 +250,7 @@ describe('/activity', () => {
});
it('should return a 200 for a duplicate like on the album', async () => {
const [reaction] = await Promise.all([
createActivity({ albumId: album.id, type: ReactionType.Like }),
]);
const [reaction] = await Promise.all([createActivity({ albumId: album.id, type: ReactionType.Like })]);
const { status, body } = await request(app)
.post('/activity')
@@ -356,9 +333,7 @@ describe('/activity', () => {
describe('DELETE /activity/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/activity/${uuidDto.notFound}`,
);
const { status, body } = await request(app).delete(`/activity/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -420,9 +395,7 @@ describe('/activity', () => {
.set('Authorization', `Bearer ${nonOwner.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest('Not found or no activity.delete access'),
);
expect(body).toEqual(errorDto.badRequest('Not found or no activity.delete access'));
});
it('should let a non-owner remove their own comment', async () => {

View File

@@ -7,7 +7,7 @@ import {
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
@@ -29,49 +29,48 @@ describe('/album', () => {
let user3: LoginResponseDto; // deleted
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await utils.resetDatabase();
admin = await apiUtils.adminSetup();
admin = await utils.adminSetup();
[user1, user2, user3] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
]);
[user1Asset1, user1Asset2] = await Promise.all([
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken, { isFavorite: true }),
utils.createAsset(user1.accessToken),
]);
const albums = await Promise.all([
// user 1
apiUtils.createAlbum(user1.accessToken, {
utils.createAlbum(user1.accessToken, {
albumName: user1SharedUser,
sharedWithUserIds: [user2.userId],
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
utils.createAlbum(user1.accessToken, {
albumName: user1SharedLink,
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user1.accessToken, {
utils.createAlbum(user1.accessToken, {
albumName: user1NotShared,
assetIds: [user1Asset1.id, user1Asset2.id],
}),
// user 2
apiUtils.createAlbum(user2.accessToken, {
utils.createAlbum(user2.accessToken, {
albumName: user2SharedUser,
sharedWithUserIds: [user1.userId],
assetIds: [user1Asset1.id],
}),
apiUtils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
apiUtils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
// user 3
apiUtils.createAlbum(user3.accessToken, {
utils.createAlbum(user3.accessToken, {
albumName: 'Deleted',
sharedWithUserIds: [user1.userId],
}),
@@ -82,21 +81,18 @@ describe('/album', () => {
await Promise.all([
// add shared link to user1SharedLink album
apiUtils.createSharedLink(user1.accessToken, {
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: user1Albums[1].id,
}),
// add shared link to user2SharedLink album
apiUtils.createSharedLink(user2.accessToken, {
utils.createSharedLink(user2.accessToken, {
type: SharedLinkType.Album,
albumId: user2Albums[1].id,
}),
]);
await deleteUser(
{ id: user3.userId },
{ headers: asBearerAuth(admin.accessToken) },
);
await deleteUser({ id: user3.userId }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /album', () => {
@@ -111,9 +107,7 @@ describe('/album', () => {
.get('/album?shared=invalid')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(400);
expect(body).toEqual(
errorDto.badRequest(['shared must be a boolean value']),
);
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
});
it('should reject an invalid assetId param', async () => {
@@ -124,6 +118,17 @@ describe('/album', () => {
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
});
it("should not show other users' favorites", async () => {
const { status, body } = await request(app)
.get(`/album/${user1Albums[0].id}?withoutAssets=false`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toEqual(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining({ isFavorite: false })],
});
});
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
.get('/album?shared=true')
@@ -153,9 +158,7 @@ describe('/album', () => {
});
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app)
.get('/album')
.set('Authorization', `Bearer ${user1.accessToken}`);
const { status, body } = await request(app).get('/album').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(3);
expect(body).toEqual(
@@ -250,9 +253,7 @@ describe('/album', () => {
describe('GET /album/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/album/${user1Albums[0].id}`,
);
const { status, body } = await request(app).get(`/album/${user1Albums[0].id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -265,7 +266,7 @@ describe('/album', () => {
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
});
});
@@ -277,7 +278,7 @@ describe('/album', () => {
expect(status).toBe(200);
expect(body).toEqual({
...user2Albums[0],
assets: [expect.objectContaining(user2Albums[0].assets[0])],
assets: [expect.objectContaining({ id: user2Albums[0].assets[0].id })],
});
});
@@ -289,7 +290,7 @@ describe('/album', () => {
expect(status).toBe(200);
expect(body).toEqual({
...user1Albums[0],
assets: [expect.objectContaining(user1Albums[0].assets[0])],
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
});
});
@@ -326,9 +327,7 @@ describe('/album', () => {
describe('POST /album', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post('/album')
.send({ albumName: 'New album' });
const { status, body } = await request(app).post('/album').send({ albumName: 'New album' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -360,37 +359,31 @@ describe('/album', () => {
describe('PUT /album/:id/assets', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/album/${user1Albums[0].id}/assets`,
);
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/assets`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should be able to add own asset to own album', async () => {
const asset = await apiUtils.createAsset(user1.accessToken);
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
});
it('should be able to add own asset to shared album', async () => {
const asset = await apiUtils.createAsset(user1.accessToken);
const asset = await utils.createAsset(user1.accessToken);
const { status, body } = await request(app)
.put(`/album/${user2Albums[0].id}/assets`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ ids: [asset.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: asset.id, success: true }),
]);
expect(body).toEqual([expect.objectContaining({ id: asset.id, success: true })]);
});
});
@@ -404,7 +397,7 @@ describe('/album', () => {
});
it('should update an album', async () => {
const album = await apiUtils.createAlbum(user1.accessToken, {
const album = await utils.createAlbum(user1.accessToken, {
albumName: 'New album',
});
const { status, body } = await request(app)
@@ -473,9 +466,7 @@ describe('/album', () => {
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
});
it('should be able to remove own asset from shared album', async () => {
@@ -485,9 +476,7 @@ describe('/album', () => {
.send({ ids: [user1Asset1.id] });
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user1Asset1.id, success: true }),
]);
expect(body).toEqual([expect.objectContaining({ id: user1Asset1.id, success: true })]);
});
});
@@ -495,15 +484,13 @@ describe('/album', () => {
let album: AlbumResponseDto;
beforeEach(async () => {
album = await apiUtils.createAlbum(user1.accessToken, {
album = await utils.createAlbum(user1.accessToken, {
albumName: 'testAlbum',
});
});
it('should require authentication', async () => {
const { status, body } = await request(app)
.put(`/album/${user1Albums[0].id}/users`)
.send({ sharedUserIds: [] });
const { status, body } = await request(app).put(`/album/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);

View File

@@ -7,27 +7,18 @@ import {
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
import { createHash } from 'node:crypto';
import { readFile, writeFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import {
apiUtils,
app,
dbUtils,
tempDir,
testAssetDir,
wsUtils,
} from 'src/utils';
import { app, tempDir, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const sha1 = (bytes: Buffer) =>
createHash('sha1').update(bytes).digest('base64');
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -53,82 +44,74 @@ describe('/asset', () => {
let ws: Socket;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[ws, user1, user2, userStats] = await Promise.all([
wsUtils.connect(admin.accessToken),
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
utils.connectWebsocket(admin.accessToken),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
]);
// asset location
assetLocation = await apiUtils.createAsset(
admin.accessToken,
{},
{
assetLocation = await utils.createAsset(admin.accessToken, {
assetData: {
filename: 'thompson-springs.jpg',
bytes: await readFile(locationAssetFilepath),
},
);
});
await wsUtils.waitForEvent({ event: 'upload', assetId: assetLocation.id });
await utils.waitForWebsocketEvent({ event: 'upload', assetId: assetLocation.id });
user1Assets = await Promise.all([
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(
user1.accessToken,
{
isFavorite: true,
isExternal: true,
isReadOnly: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
},
{ filename: 'example.mp4' },
),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken, {
isFavorite: true,
isReadOnly: true,
fileCreatedAt: yesterday.toISO(),
fileModifiedAt: yesterday.toISO(),
assetData: { filename: 'example.mp4' },
}),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
user2Assets = await Promise.all([apiUtils.createAsset(user2.accessToken)]);
user2Assets = await Promise.all([utils.createAsset(user2.accessToken)]);
for (const asset of [...user1Assets, ...user2Assets]) {
expect(asset.duplicate).toBe(false);
}
await Promise.all([
// stats
apiUtils.createAsset(userStats.accessToken),
apiUtils.createAsset(userStats.accessToken, { isFavorite: true }),
apiUtils.createAsset(userStats.accessToken, { isArchived: true }),
apiUtils.createAsset(
userStats.accessToken,
{
isArchived: true,
isFavorite: true,
},
{ filename: 'example.mp4' },
),
utils.createAsset(userStats.accessToken),
utils.createAsset(userStats.accessToken, { isFavorite: true }),
utils.createAsset(userStats.accessToken, { isArchived: true }),
utils.createAsset(userStats.accessToken, {
isArchived: true,
isFavorite: true,
assetData: { filename: 'example.mp4' },
}),
]);
const person1 = await apiUtils.createPerson(user1.accessToken, {
const person1 = await utils.createPerson(user1.accessToken, {
name: 'Test Person',
});
await dbUtils.createFace({
await utils.createFace({
assetId: user1Assets[0].id,
personId: person1.id,
});
}, 30_000);
afterAll(() => {
wsUtils.disconnect(ws);
utils.disconnectWebsocket(ws);
});
describe('GET /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/asset/${uuidDto.notFound}`,
);
const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
@@ -158,14 +141,12 @@ describe('/asset', () => {
});
it('should work with a shared link', async () => {
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [user1Assets[0].id],
});
const { status, body } = await request(app).get(
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
);
const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
@@ -190,14 +171,12 @@ describe('/asset', () => {
],
});
const sharedLink = await apiUtils.createSharedLink(user1.accessToken, {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [user1Assets[0].id],
});
const data = await request(app).get(
`/asset/${user1Assets[0].id}?key=${sharedLink.key}`,
);
const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] });
});
@@ -264,12 +243,12 @@ describe('/asset', () => {
describe('GET /asset/random', () => {
beforeAll(async () => {
await Promise.all([
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
utils.createAsset(user1.accessToken),
]);
});
@@ -280,7 +259,7 @@ describe('/asset', () => {
expect(body).toEqual(errorDto.unauthorized);
});
it.each(Array(10))('should return 1 random assets', async () => {
it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app)
.get('/asset/random')
.set('Authorization', `Bearer ${user1.accessToken}`);
@@ -290,14 +269,9 @@ describe('/asset', () => {
const assets: AssetResponseDto[] = body;
expect(assets.length).toBe(1);
expect(assets[0].ownerId).toBe(user1.userId);
// assets owned by user1
expect([user1Assets.map(({ id }) => id)]).toContain(assets[0].id);
// assets owned by user2
expect([user1Assets.map(({ id }) => id)]).not.toContain(assets[0].id);
});
it.each(Array(10))('should return 2 random assets', async () => {
it.each(TEN_TIMES)('should return 2 random assets', async () => {
const { status, body } = await request(app)
.get('/asset/random?count=2')
.set('Authorization', `Bearer ${user1.accessToken}`);
@@ -309,24 +283,18 @@ describe('/asset', () => {
for (const asset of assets) {
expect(asset.ownerId).toBe(user1.userId);
// assets owned by user1
expect([user1Assets.map(({ id }) => id)]).toContain(asset.id);
// assets owned by user2
expect([user2Assets.map(({ id }) => id)]).not.toContain(asset.id);
}
});
it.each(Array(10))(
it.each(TEN_TIMES)(
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
async () => {
const { status, body } = await request(app)
.get('/[]asset/random')
.get('/asset/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
expect.objectContaining({ id: user2Assets[0].id }),
]);
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
},
);
@@ -341,9 +309,7 @@ describe('/asset', () => {
describe('PUT /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/asset/:${uuidDto.notFound}`,
);
const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -365,10 +331,7 @@ describe('/asset', () => {
});
it('should favorite an asset', async () => {
const before = await apiUtils.getAssetInfo(
user1.accessToken,
user1Assets[0].id,
);
const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id);
expect(before.isFavorite).toBe(false);
const { status, body } = await request(app)
@@ -380,10 +343,7 @@ describe('/asset', () => {
});
it('should archive an asset', async () => {
const before = await apiUtils.getAssetInfo(
user1.accessToken,
user1Assets[0].id,
);
const before = await utils.getAssetInfo(user1.accessToken, user1Assets[0].id);
expect(before.isArchived).toBe(false);
const { status, body } = await request(app)
@@ -497,9 +457,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest(['each value in ids must be a UUID']),
);
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
});
it('should throw an error when the id is not found', async () => {
@@ -509,15 +467,13 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest('Not found or no asset.delete access'),
);
expect(body).toEqual(errorDto.badRequest('Not found or no asset.delete access'));
});
it('should move an asset to the trash', async () => {
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
const { id: assetId } = await utils.createAsset(admin.accessToken);
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(false);
const { status } = await request(app)
@@ -526,7 +482,7 @@ describe('/asset', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(true);
});
});
@@ -537,7 +493,7 @@ describe('/asset', () => {
input: 'formats/jpg/el_torcal_rocks.jpg',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'el_torcal_rocks',
originalFileName: 'el_torcal_rocks.jpg',
resized: true,
exifInfo: {
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
@@ -561,7 +517,7 @@ describe('/asset', () => {
input: 'formats/heic/IMG_2682.heic',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'IMG_2682',
originalFileName: 'IMG_2682.heic',
resized: true,
fileCreatedAt: '2019-03-21T16:04:22.348Z',
exifInfo: {
@@ -586,7 +542,7 @@ describe('/asset', () => {
input: 'formats/png/density_plot.png',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'density_plot',
originalFileName: 'density_plot.png',
resized: true,
exifInfo: {
exifImageWidth: 800,
@@ -601,7 +557,7 @@ describe('/asset', () => {
input: 'formats/raw/Nikon/D80/glarus.nef',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'glarus',
originalFileName: 'glarus.nef',
resized: true,
fileCreatedAt: '2010-07-20T17:27:12.000Z',
exifInfo: {
@@ -623,7 +579,7 @@ describe('/asset', () => {
input: 'formats/raw/Nikon/D700/philadelphia.nef',
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'philadelphia',
originalFileName: 'philadelphia.nef',
resized: true,
fileCreatedAt: '2016-09-22T22:10:29.060Z',
exifInfo: {
@@ -647,17 +603,15 @@ describe('/asset', () => {
for (const { input, expected } of tests) {
it(`should generate a thumbnail for ${input}`, async () => {
const filepath = join(testAssetDir, input);
const { id, duplicate } = await apiUtils.createAsset(
admin.accessToken,
{},
{ bytes: await readFile(filepath), filename: basename(filepath) },
);
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
expect(duplicate).toBe(false);
await wsUtils.waitForEvent({ event: 'upload', assetId: id });
await utils.waitForWebsocketEvent({ event: 'upload', assetId: id });
const asset = await apiUtils.getAssetInfo(admin.accessToken, id);
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(asset.exifInfo).toBeDefined();
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
@@ -667,14 +621,12 @@ describe('/asset', () => {
it('should handle a duplicate', async () => {
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
const { duplicate } = await apiUtils.createAsset(
admin.accessToken,
{},
{
const { duplicate } = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
);
});
expect(duplicate).toBe(true);
});
@@ -701,29 +653,21 @@ describe('/asset', () => {
for (const { filepath, checksum } of motionTests) {
it(`should extract motionphoto video from ${filepath}`, async () => {
const response = await apiUtils.createAsset(
admin.accessToken,
{},
{
const response = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
);
});
await wsUtils.waitForEvent({ event: 'upload', assetId: response.id });
await utils.waitForWebsocketEvent({ event: 'upload', assetId: response.id });
expect(response.duplicate).toBe(false);
const asset = await apiUtils.getAssetInfo(
admin.accessToken,
response.id,
);
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(asset.livePhotoVideoId).toBeDefined();
const video = await apiUtils.getAssetInfo(
admin.accessToken,
asset.livePhotoVideoId as string,
);
const video = await utils.getAssetInfo(admin.accessToken, asset.livePhotoVideoId as string);
expect(video.checksum).toStrictEqual(checksum);
});
}
@@ -731,9 +675,7 @@ describe('/asset', () => {
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/asset/thumbnail/${assetLocation.id}`,
);
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -744,7 +686,7 @@ describe('/asset', () => {
.get(`/asset/thumbnail/${assetLocation.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
await wsUtils.waitForEvent({
await utils.waitForWebsocketEvent({
event: 'upload',
assetId: assetLocation.id,
});
@@ -775,9 +717,7 @@ describe('/asset', () => {
describe('GET /asset/file/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/asset/thumbnail/${assetLocation.id}`,
);
const { status, body } = await request(app).get(`/asset/thumbnail/${assetLocation.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -792,14 +732,11 @@ describe('/asset', () => {
expect(body).toBeDefined();
expect(type).toBe('image/jpeg');
const asset = await apiUtils.getAssetInfo(
admin.accessToken,
assetLocation.id,
);
const asset = await utils.getAssetInfo(admin.accessToken, assetLocation.id);
const original = await readFile(locationAssetFilepath);
const originalChecksum = sha1(original);
const downloadChecksum = sha1(body);
const originalChecksum = utils.sha1(original);
const downloadChecksum = utils.sha1(body);
expect(originalChecksum).toBe(downloadChecksum);
expect(downloadChecksum).toBe(asset.checksum);

View File

@@ -1,36 +1,27 @@
import {
deleteAssets,
getAuditFiles,
updateAsset,
type LoginResponseDto,
} from '@immich/sdk';
import { apiUtils, asBearerAuth, dbUtils, fileUtils } from 'src/utils';
import { deleteAssets, getAuditFiles, updateAsset, type LoginResponseDto } from '@immich/sdk';
import { asBearerAuth, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/audit', () => {
let admin: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await fileUtils.reset();
await utils.resetDatabase();
await utils.resetFilesystem();
admin = await apiUtils.adminSetup();
admin = await utils.adminSetup();
});
describe('GET :/file-report', () => {
it('excludes assets without issues from report', async () => {
const [trashedAsset, archivedAsset, _] = await Promise.all([
apiUtils.createAsset(admin.accessToken),
apiUtils.createAsset(admin.accessToken),
apiUtils.createAsset(admin.accessToken),
const [trashedAsset, archivedAsset] = await Promise.all([
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
utils.createAsset(admin.accessToken),
]);
await Promise.all([
deleteAssets(
{ assetBulkDeleteDto: { ids: [trashedAsset.id] } },
{ headers: asBearerAuth(admin.accessToken) },
),
deleteAssets({ assetBulkDeleteDto: { ids: [trashedAsset.id] } }, { headers: asBearerAuth(admin.accessToken) }),
updateAsset(
{
id: archivedAsset.id,

View File

@@ -1,29 +1,15 @@
import {
LoginResponseDto,
getAuthDevices,
login,
signUpAdmin,
} from '@immich/sdk';
import { LoginResponseDto, getAuthDevices, login, signUpAdmin } from '@immich/sdk';
import { loginDto, signupDto, uuidDto } from 'src/fixtures';
import {
deviceDto,
errorDto,
loginResponseDto,
signupResponseDto,
} from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { deviceDto, errorDto, loginResponseDto, signupResponseDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { beforeEach, describe, expect, it } from 'vitest';
const { name, email, password } = signupDto.admin;
describe(`/auth/admin-sign-up`, () => {
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
await utils.resetDatabase();
});
describe('POST /auth/admin-sign-up', () => {
@@ -48,18 +34,14 @@ describe(`/auth/admin-sign-up`, () => {
for (const { should, data } of invalid) {
it(`should ${should}`, async () => {
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send(data);
const { status, body } = await request(app).post('/auth/admin-sign-up').send(data);
expect(status).toEqual(400);
expect(body).toEqual(errorDto.badRequest());
});
}
it(`should sign up the admin`, async () => {
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send(signupDto.admin);
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(201);
expect(body).toEqual(signupResponseDto.admin);
});
@@ -86,9 +68,7 @@ describe(`/auth/admin-sign-up`, () => {
it('should not allow a second admin to sign up', async () => {
await signUpAdmin({ signUpDto: signupDto.admin });
const { status, body } = await request(app)
.post('/auth/admin-sign-up')
.send(signupDto.admin);
const { status, body } = await request(app).post('/auth/admin-sign-up').send(signupDto.admin);
expect(status).toBe(400);
expect(body).toEqual(errorDto.alreadyHasAdmin);
@@ -100,16 +80,14 @@ describe('/auth/*', () => {
let admin: LoginResponseDto;
beforeEach(async () => {
await dbUtils.reset();
await utils.resetDatabase();
await signUpAdmin({ signUpDto: signupDto.admin });
admin = await login({ loginCredentialDto: loginDto.admin });
});
describe(`POST /auth/login`, () => {
it('should reject an incorrect password', async () => {
const { status, body } = await request(app)
.post('/auth/login')
.send({ email, password: 'incorrect' });
const { status, body } = await request(app).post('/auth/login').send({ email, password: 'incorrect' });
expect(status).toBe(401);
expect(body).toEqual(errorDto.incorrectLogin);
});
@@ -125,9 +103,7 @@ describe('/auth/*', () => {
}
it('should accept a correct password', async () => {
const { status, body, headers } = await request(app)
.post('/auth/login')
.send({ email, password });
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
expect(status).toBe(201);
expect(body).toEqual(loginResponseDto.admin);
@@ -136,15 +112,9 @@ describe('/auth/*', () => {
const cookies = headers['set-cookie'];
expect(cookies).toHaveLength(3);
expect(cookies[0]).toEqual(
`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`
);
expect(cookies[1]).toEqual(
'immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;'
);
expect(cookies[2]).toEqual(
'immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;'
);
expect(cookies[0]).toEqual(`immich_access_token=${token}; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;`);
expect(cookies[1]).toEqual('immich_auth_type=password; HttpOnly; Path=/; Max-Age=34560000; SameSite=Lax;');
expect(cookies[2]).toEqual('immich_is_authenticated=true; Path=/; Max-Age=34560000; SameSite=Lax;');
});
});
@@ -176,18 +146,12 @@ describe('/auth/*', () => {
await login({ loginCredentialDto: loginDto.admin });
}
await expect(
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(6);
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(6);
const { status } = await request(app)
.delete(`/auth/devices`)
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status } = await request(app).delete(`/auth/devices`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await expect(
getAuthDevices({ headers: asBearerAuth(admin.accessToken) })
).resolves.toHaveLength(1);
await expect(getAuthDevices({ headers: asBearerAuth(admin.accessToken) })).resolves.toHaveLength(1);
});
it('should throw an error for a non-existent device id', async () => {
@@ -195,9 +159,7 @@ describe('/auth/*', () => {
.delete(`/auth/devices/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest('Not found or no authDevice.delete access')
);
expect(body).toEqual(errorDto.badRequest('Not found or no authDevice.delete access'));
});
it('should logout a device', async () => {
@@ -219,9 +181,7 @@ describe('/auth/*', () => {
describe('POST /auth/validateToken', () => {
it('should reject an invalid token', async () => {
const { status, body } = await request(app)
.post(`/auth/validateToken`)
.set('Authorization', 'Bearer 123');
const { status, body } = await request(app).post(`/auth/validateToken`).set('Authorization', 'Bearer 123');
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidToken);
});

View File

@@ -1,18 +1,19 @@
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile, writeFile } from 'node:fs/promises';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import { app, tempDir, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/download', () => {
let admin: LoginResponseDto;
let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup();
asset1 = await apiUtils.createAsset(admin.accessToken);
await utils.resetDatabase();
admin = await utils.adminSetup();
[asset1, asset2] = await Promise.all([utils.createAsset(admin.accessToken), utils.createAsset(admin.accessToken)]);
});
describe('POST /download/info', () => {
@@ -40,11 +41,42 @@ describe('/download', () => {
});
});
describe('POST /download/archive', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/download/archive`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download an archive', async () => {
const { status, body } = await request(app)
.post('/download/archive')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ assetIds: [asset1.id, asset2.id] });
expect(status).toBe(200);
expect(body instanceof Buffer).toBe(true);
await writeFile(`${tempDir}/archive.zip`, body);
await utils.unzip(`${tempDir}/archive.zip`, `${tempDir}/archive`);
const files = [
{ filename: 'example.png', id: asset1.id },
{ filename: 'example+1.png', id: asset2.id },
];
for (const { id, filename } of files) {
const bytes = await readFile(`${tempDir}/archive/${filename}`);
const asset = await utils.getAssetInfo(admin.accessToken, id);
expect(utils.sha1(bytes)).toBe(asset.checksum);
}
});
});
describe('POST /download/asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(
`/download/asset/${asset1.id}`,
);
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -56,7 +88,7 @@ describe('/download', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/jpeg');
expect(response.headers['content-type']).toEqual('image/png');
});
});
});

View File

@@ -0,0 +1,455 @@
import { LibraryResponseDto, LibraryType, LoginResponseDto, getAllLibraries } from '@immich/sdk';
import { userDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, testAssetDirInternal, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
describe('/library', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let library: LibraryResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
user = await utils.userSetup(admin.accessToken, userDto.user1);
library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
});
describe('GET /library', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/library');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should start with a default upload library', async () => {
const { status, body } = await request(app).get('/library').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.Upload,
name: 'Default Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
]),
);
});
});
describe('POST /library', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/library').send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require admin authentication', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${user.accessToken}`)
.send({ type: LibraryType.External });
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should create an external library with defaults', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ type: LibraryType.External });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.External,
name: 'New External Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
);
});
it('should create an external library with options', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**'],
});
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
}),
);
});
it('should not create an external library with duplicate import paths', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path', '/path'],
exclusionPatterns: ['**/Raw/**'],
});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
});
it('should not create an external library with duplicate exclusion patterns', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
type: LibraryType.External,
name: 'My Awesome Library',
importPaths: ['/path/to/import'],
exclusionPatterns: ['**/Raw/**', '**/Raw/**'],
});
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
});
it('should create an upload library with defaults', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ type: LibraryType.Upload });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.Upload,
name: 'New Upload Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
);
});
it('should create an upload library with options', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ type: LibraryType.Upload, name: 'My Awesome Library' });
expect(status).toBe(201);
expect(body).toEqual(
expect.objectContaining({
name: 'My Awesome Library',
}),
);
});
it('should not allow upload libraries to have import paths', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ type: LibraryType.Upload, importPaths: ['/path/to/import'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have import paths'));
});
it('should not allow upload libraries to have exclusion patterns', async () => {
const { status, body } = await request(app)
.post('/library')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ type: LibraryType.Upload, exclusionPatterns: ['**/Raw/**'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest('Upload libraries cannot have exclusion patterns'));
});
});
describe('PUT /library/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/library/${uuidDto.notFound}`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should change the library name', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: 'New Library Name' });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
name: 'New Library Name',
}),
);
});
it('should not set an empty name', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ name: '' });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['name should not be empty']));
});
it('should change the import paths', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [testAssetDirInternal] });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
importPaths: [testAssetDirInternal],
}),
);
});
it('should reject an empty import path', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: [''] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty']));
});
it('should reject duplicate import paths', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ importPaths: ['/path', '/path'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
});
it('should change the exclusion pattern', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/Raw/**'] });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
exclusionPatterns: ['**/Raw/**'],
}),
);
});
it('should reject duplicate exclusion patterns', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
});
it('should reject an empty exclusion pattern', async () => {
const { status, body } = await request(app)
.put(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ exclusionPatterns: [''] });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty']));
});
});
describe('GET /library/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require admin access', async () => {
const { status, body } = await request(app)
.get(`/library/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${user.accessToken}`);
expect(status).toBe(403);
expect(body).toEqual(errorDto.forbidden);
});
it('should get library by id', async () => {
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
const { status, body } = await request(app)
.get(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({
ownerId: admin.userId,
type: LibraryType.External,
name: 'New External Library',
refreshedAt: null,
assetCount: 0,
importPaths: [],
exclusionPatterns: [],
}),
);
});
});
describe('DELETE /library/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/library/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should not delete the last upload library', async () => {
const libraries = await getAllLibraries(
{ $type: LibraryType.Upload },
{ headers: asBearerAuth(admin.accessToken) },
);
const adminLibraries = libraries.filter((library) => library.ownerId === admin.userId);
expect(adminLibraries.length).toBeGreaterThanOrEqual(1);
const lastLibrary = adminLibraries.pop() as LibraryResponseDto;
// delete all but the last upload library
for (const library of adminLibraries) {
const { status } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
}
const { status, body } = await request(app)
.delete(`/library/${lastLibrary.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(errorDto.noDeleteUploadLibrary);
expect(status).toBe(400);
});
it('should delete an external library', async () => {
const library = await utils.createLibrary(admin.accessToken, { type: LibraryType.External });
const { status, body } = await request(app)
.delete(`/library/${library.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
expect(body).toEqual({});
const libraries = await getAllLibraries({}, { headers: asBearerAuth(admin.accessToken) });
expect(libraries).not.toEqual(
expect.arrayContaining([
expect.objectContaining({
id: library.id,
}),
]),
);
});
});
describe('GET /library/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/library/${uuidDto.notFound}/statistics`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /library/:id/scan', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/scan`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /library/:id/removeOffline', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/removeOffline`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('POST /library/:id/validate', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/library/${uuidDto.notFound}/validate`).send({});
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should pass with no import paths', async () => {
const response = await utils.validateLibrary(admin.accessToken, library.id, { importPaths: [] });
expect(response.importPaths).toEqual([]);
});
it('should fail if path does not exist', async () => {
const pathToTest = `${testAssetDirInternal}/does/not/exist`;
const response = await utils.validateLibrary(admin.accessToken, library.id, {
importPaths: [pathToTest],
});
expect(response.importPaths?.length).toEqual(1);
const pathResponse = response?.importPaths?.at(0);
expect(pathResponse).toEqual({
importPath: pathToTest,
isValid: false,
message: `Path does not exist (ENOENT)`,
});
});
it('should fail if path is a file', async () => {
const pathToTest = `${testAssetDirInternal}/albums/nature/el_torcal_rocks.jpg`;
const response = await utils.validateLibrary(admin.accessToken, library.id, {
importPaths: [pathToTest],
});
expect(response.importPaths?.length).toEqual(1);
const pathResponse = response?.importPaths?.at(0);
expect(pathResponse).toEqual({
importPath: pathToTest,
isValid: false,
message: `Not a directory`,
});
});
});
});

View File

@@ -1,30 +1,19 @@
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`/oauth`, () => {
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
await apiUtils.adminSetup();
beforeAll(async () => {
await utils.resetDatabase();
await utils.adminSetup();
});
describe('POST /oauth/authorize', () => {
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
.send({});
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.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
});
});
});

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, createPartner } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@@ -12,26 +12,19 @@ describe('/partner', () => {
let user3: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await utils.resetDatabase();
admin = await apiUtils.adminSetup();
admin = await utils.adminSetup();
[user1, user2, user3] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
]);
await Promise.all([
createPartner(
{ id: user2.userId },
{ headers: asBearerAuth(user1.accessToken) }
),
createPartner(
{ id: user1.userId },
{ headers: asBearerAuth(user2.accessToken) }
),
createPartner({ id: user2.userId }, { headers: asBearerAuth(user1.accessToken) }),
createPartner({ id: user1.userId }, { headers: asBearerAuth(user2.accessToken) }),
]);
});
@@ -66,9 +59,7 @@ describe('/partner', () => {
describe('POST /partner/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(
`/partner/${user3.userId}`
);
const { status, body } = await request(app).post(`/partner/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -89,17 +80,13 @@ describe('/partner', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Partner already exists' })
);
expect(body).toEqual(expect.objectContaining({ message: 'Partner already exists' }));
});
});
describe('PUT /partner/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/partner/${user2.userId}`
);
const { status, body } = await request(app).put(`/partner/${user2.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -112,17 +99,13 @@ describe('/partner', () => {
.send({ inTimeline: false });
expect(status).toBe(200);
expect(body).toEqual(
expect.objectContaining({ id: user2.userId, inTimeline: false })
);
expect(body).toEqual(expect.objectContaining({ id: user2.userId, inTimeline: false }));
});
});
describe('DELETE /partner/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/partner/${user3.userId}`
);
const { status, body } = await request(app).delete(`/partner/${user3.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -142,9 +125,7 @@ describe('/partner', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Partner not found' })
);
expect(body).toEqual(expect.objectContaining({ message: 'Partner not found' }));
});
});
});

View File

@@ -1,39 +1,49 @@
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
import { uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activity', () => {
const invalidBirthday = [
{ birthDate: 'false', response: 'birthDate must be a date string' },
{ birthDate: '123567', response: 'birthDate must be a date string' },
{ birthDate: 123_567, response: 'birthDate must be a date string' },
{ birthDate: new Date(9999, 0, 0).toISOString(), response: ['Birth date cannot be in the future'] },
];
describe('/person', () => {
let admin: LoginResponseDto;
let visiblePerson: PersonResponseDto;
let hiddenPerson: PersonResponseDto;
let multipleAssetsPerson: PersonResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup();
});
await utils.resetDatabase();
admin = await utils.adminSetup();
beforeEach(async () => {
await dbUtils.reset(['person']);
[visiblePerson, hiddenPerson] = await Promise.all([
apiUtils.createPerson(admin.accessToken, {
[visiblePerson, hiddenPerson, multipleAssetsPerson] = await Promise.all([
utils.createPerson(admin.accessToken, {
name: 'visible_person',
}),
apiUtils.createPerson(admin.accessToken, {
utils.createPerson(admin.accessToken, {
name: 'hidden_person',
isHidden: true,
}),
utils.createPerson(admin.accessToken, {
name: 'multiple_assets_person',
}),
]);
const asset = await apiUtils.createAsset(admin.accessToken);
const asset1 = await utils.createAsset(admin.accessToken);
const asset2 = await utils.createAsset(admin.accessToken);
await Promise.all([
dbUtils.createFace({ assetId: asset.id, personId: visiblePerson.id }),
dbUtils.createFace({ assetId: asset.id, personId: hiddenPerson.id }),
utils.createFace({ assetId: asset1.id, personId: visiblePerson.id }),
utils.createFace({ assetId: asset1.id, personId: hiddenPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset1.id, personId: multipleAssetsPerson.id }),
utils.createFace({ assetId: asset2.id, personId: multipleAssetsPerson.id }),
]);
});
@@ -55,9 +65,10 @@ describe('/activity', () => {
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
total: 3,
hidden: 1,
people: [
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'visible_person' }),
expect.objectContaining({ name: 'hidden_person' }),
],
@@ -65,24 +76,23 @@ describe('/activity', () => {
});
it('should return only visible people', async () => {
const { status, body } = await request(app)
.get('/person')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/person').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
total: 2,
total: 3,
hidden: 1,
people: [expect.objectContaining({ name: 'visible_person' })],
people: [
expect.objectContaining({ name: 'multiple_assets_person' }),
expect.objectContaining({ name: 'visible_person' }),
],
});
});
});
describe('GET /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/person/${uuidDto.notFound}`
);
const { status, body } = await request(app).get(`/person/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -107,11 +117,71 @@ describe('/activity', () => {
});
});
describe('GET /person/:id/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/person/${multipleAssetsPerson.id}/statistics`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should throw error if person with id does not exist', async () => {
const { status, body } = await request(app)
.get(`/person/${uuidDto.notFound}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest());
});
it('should return the correct number of assets', async () => {
const { status, body } = await request(app)
.get(`/person/${multipleAssetsPerson.id}/statistics`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ assets: 2 }));
});
});
describe('POST /person', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/person`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.post(`/person`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response));
});
}
it('should create a person', async () => {
const { status, body } = await request(app)
.post(`/person`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({
name: 'New Person',
birthDate: '1990-01-01T05:00:00.000Z',
});
expect(status).toBe(201);
expect(body).toMatchObject({
id: expect.any(String),
name: 'New Person',
birthDate: '1990-01-01T05:00:00.000Z',
});
});
});
describe('PUT /person/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(
`/person/${uuidDto.notFound}`
);
const { status, body } = await request(app).put(`/person/${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -131,24 +201,16 @@ describe('/activity', () => {
});
}
it('should not accept invalid birth dates', async () => {
for (const { birthDate, response } of [
{ birthDate: false, response: 'Not found or no person.write access' },
{ birthDate: 'false', response: ['birthDate must be a Date instance'] },
{
birthDate: '123567',
response: 'Not found or no person.write access',
},
{ birthDate: 123567, response: 'Not found or no person.write access' },
]) {
for (const { birthDate, response } of invalidBirthday) {
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
const { status, body } = await request(app)
.put(`/person/${uuidDto.notFound}`)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate });
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(response));
}
});
});
}
it('should update a date of birth', async () => {
const { status, body } = await request(app)
@@ -160,15 +222,8 @@ describe('/activity', () => {
});
it('should clear a date of birth', async () => {
// TODO ironically this uses the update endpoint to create the person
const person = await apiUtils.createPerson(admin.accessToken, {
birthDate: new Date('1990-01-01').toISOString(),
});
expect(person.birthDate).toBeDefined();
const { status, body } = await request(app)
.put(`/person/${person.id}`)
.put(`/person/${visiblePerson.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ birthDate: null });
expect(status).toBe(200);

View File

@@ -0,0 +1,224 @@
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
const albums = { total: 0, count: 0, items: [], facets: [] };
describe('/search', () => {
let admin: LoginResponseDto;
let assetFalcon: AssetFileUploadResponseDto;
let assetDenali: AssetFileUploadResponseDto;
let websocket: Socket;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
websocket = await utils.connectWebsocket(admin.accessToken);
const files: string[] = [
'/albums/nature/prairie_falcon.jpg',
'/formats/webp/denali.webp',
'/formats/raw/Nikon/D700/philadelphia.nef',
'/albums/nature/orychophragmus_violaceus.jpg',
'/albums/nature/notocactus_minimus.jpg',
'/albums/nature/silver_fir.jpg',
'/albums/nature/tanners_ridge.jpg',
'/albums/nature/cyclamen_persicum.jpg',
'/albums/nature/polemonium_reptans.jpg',
'/albums/nature/wood_anemones.jpg',
'/formats/heic/IMG_2682.heic',
'/formats/jpg/el_torcal_rocks.jpg',
'/formats/png/density_plot.png',
'/formats/motionphoto/Samsung One UI 6.jpg',
'/formats/motionphoto/Samsung One UI 6.heic',
'/formats/motionphoto/Samsung One UI 5.jpg',
'/formats/raw/Nikon/D80/glarus.nef',
'/metadata/gps-position/thompson-springs.jpg',
];
const assets: AssetFileUploadResponseDto[] = [];
for (const filename of files) {
const bytes = await readFile(join(testAssetDir, filename));
assets.push(
await utils.createAsset(admin.accessToken, {
deviceAssetId: `test-${filename}`,
assetData: { bytes, filename },
}),
);
}
for (const asset of assets) {
await utils.waitForWebsocketEvent({ event: 'upload', assetId: asset.id });
}
[assetFalcon, assetDenali] = assets;
});
afterAll(async () => {
await utils.disconnectWebsocket(websocket);
});
describe('POST /search/metadata', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/metadata');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should search by camera make', async () => {
const { status, body } = await request(app)
.post('/search/metadata')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ make: 'Canon' });
expect(status).toBe(200);
expect(body).toEqual({
albums,
assets: {
count: 2,
items: expect.arrayContaining([
expect.objectContaining({ id: assetDenali.id }),
expect.objectContaining({ id: assetFalcon.id }),
]),
facets: [],
nextPage: null,
total: 2,
},
});
});
it('should search by camera model', async () => {
const { status, body } = await request(app)
.post('/search/metadata')
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ model: 'Canon EOS 7D' });
expect(status).toBe(200);
expect(body).toEqual({
albums,
assets: {
count: 1,
items: [expect.objectContaining({ id: assetDenali.id })],
facets: [],
nextPage: null,
total: 1,
},
});
});
});
describe('POST /search/smart', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post('/search/smart');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
});
describe('GET /search/explore', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/explore');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get explore data', async () => {
const { status, body } = await request(app)
.get('/search/explore')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{ fieldName: 'exifInfo.city', items: [] },
{ fieldName: 'smartInfo.tags', items: [] },
]);
});
});
describe('GET /search/places', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/places');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get places', async () => {
const { status, body } = await request(app)
.get('/search/places?name=Paris')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(Array.isArray(body)).toBe(true);
expect(body.length).toBeGreaterThan(10);
});
});
describe('GET /search/suggestions', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/search/suggestions');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should get suggestions for country', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=country')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(['United States of America']);
expect(status).toBe(200);
});
it('should get suggestions for state', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=state')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(['Douglas County, Nebraska', 'Mesa County, Colorado']);
expect(status).toBe(200);
});
it('should get suggestions for city', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=city')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual(['Palisade', 'Ralston']);
expect(status).toBe(200);
});
it('should get suggestions for camera make', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=camera-make')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Apple',
'Canon',
'FUJIFILM',
'NIKON CORPORATION',
'PENTAX Corporation',
'samsung',
'SONY',
]);
expect(status).toBe(200);
});
it('should get suggestions for camera model', async () => {
const { status, body } = await request(app)
.get('/search/suggestions?type=camera-model')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toEqual([
'Canon EOS 7D',
'Canon EOS R5',
'DSLR-A550',
'FinePix S3Pro',
'iPhone 7',
'NIKON D700',
'NIKON D750',
'NIKON D80',
'PENTAX K10D',
'SM-F711N',
'SM-S906U',
'SM-T970',
]);
expect(status).toBe(200);
});
});
});

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, getServerConfig } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@@ -10,10 +10,9 @@ describe('/server-info', () => {
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('GET /server-info', () => {
@@ -88,6 +87,7 @@ describe('/server-info', () => {
loginPageMessage: '',
oauthButtonText: 'Login with OAuth',
trashDays: 30,
userDeleteDelay: 7,
isInitialized: true,
externalDomain: '',
isOnboarded: false,
@@ -97,9 +97,7 @@ describe('/server-info', () => {
describe('GET /server-info/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
'/server-info/statistics'
);
const { status, body } = await request(app).get('/server-info/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -145,9 +143,7 @@ describe('/server-info', () => {
describe('GET /server-info/media-types', () => {
it('should return accepted media types', async () => {
const { status, body } = await request(app).get(
'/server-info/media-types'
);
const { status, body } = await request(app).get('/server-info/media-types');
expect(status).toBe(200);
expect(body).toEqual({
sidecar: ['.xmp'],

View File

@@ -9,7 +9,7 @@ import {
} from '@immich/sdk';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@@ -30,30 +30,20 @@ describe('/shared-link', () => {
let linkWithoutMetadata: SharedLinkResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await utils.resetDatabase();
admin = await apiUtils.adminSetup();
admin = await utils.adminSetup();
[user1, user2] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
]);
[asset1, asset2] = await Promise.all([
apiUtils.createAsset(user1.accessToken),
apiUtils.createAsset(user1.accessToken),
]);
[asset1, asset2] = await Promise.all([utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken)]);
[album, deletedAlbum, metadataAlbum] = await Promise.all([
createAlbum(
{ createAlbumDto: { albumName: 'album' } },
{ headers: asBearerAuth(user1.accessToken) },
),
createAlbum(
{ createAlbumDto: { albumName: 'deleted album' } },
{ headers: asBearerAuth(user2.accessToken) },
),
createAlbum({ createAlbumDto: { albumName: 'album' } }, { headers: asBearerAuth(user1.accessToken) }),
createAlbum({ createAlbumDto: { albumName: 'deleted album' } }, { headers: asBearerAuth(user2.accessToken) }),
createAlbum(
{
createAlbumDto: {
@@ -65,47 +55,38 @@ describe('/shared-link', () => {
),
]);
[
linkWithDeletedAlbum,
linkWithAlbum,
linkWithAssets,
linkWithPassword,
linkWithMetadata,
linkWithoutMetadata,
] = await Promise.all([
apiUtils.createSharedLink(user2.accessToken, {
type: SharedLinkType.Album,
albumId: deletedAlbum.id,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset1.id],
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
password: 'foo',
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: true,
}),
apiUtils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: false,
}),
]);
[linkWithDeletedAlbum, linkWithAlbum, linkWithAssets, linkWithPassword, linkWithMetadata, linkWithoutMetadata] =
await Promise.all([
utils.createSharedLink(user2.accessToken, {
type: SharedLinkType.Album,
albumId: deletedAlbum.id,
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset1.id],
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
password: 'foo',
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: true,
}),
utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Album,
albumId: metadataAlbum.id,
showMetadata: false,
}),
]);
await deleteUser(
{ id: user2.userId },
{ headers: asBearerAuth(admin.accessToken) },
);
await deleteUser({ id: user2.userId }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /shared-link', () => {
@@ -146,17 +127,13 @@ describe('/shared-link', () => {
describe('GET /shared-link/me', () => {
it('should not require admin authentication', async () => {
const { status } = await request(app)
.get('/shared-link/me')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status } = await request(app).get('/shared-link/me').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(403);
});
it('should get data for correct shared link', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithAlbum.key });
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithAlbum.key });
expect(status).toBe(200);
expect(body).toEqual(
@@ -178,18 +155,14 @@ describe('/shared-link', () => {
});
it('should return unauthorized if target has been soft deleted', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithDeletedAlbum.key });
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithDeletedAlbum.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidShareKey);
});
it('should return unauthorized for password protected link', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithPassword.key });
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithPassword.key });
expect(status).toBe(401);
expect(body).toEqual(errorDto.invalidSharePassword);
@@ -211,15 +184,13 @@ describe('/shared-link', () => {
});
it('should return metadata for album shared link', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithMetadata.key });
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
expect(body.assets[0]).toEqual(
expect.objectContaining({
originalFileName: 'example',
originalFileName: 'example.png',
localDateTime: expect.any(String),
fileCreatedAt: expect.any(String),
exifInfo: expect.any(Object),
@@ -229,9 +200,7 @@ describe('/shared-link', () => {
});
it('should not return metadata for album shared link without metadata', async () => {
const { status, body } = await request(app)
.get('/shared-link/me')
.query({ key: linkWithoutMetadata.key });
const { status, body } = await request(app).get('/shared-link/me').query({ key: linkWithoutMetadata.key });
expect(status).toBe(200);
expect(body.assets).toHaveLength(1);
@@ -247,9 +216,7 @@ describe('/shared-link', () => {
describe('GET /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
`/shared-link/${linkWithAlbum.id}`,
);
const { status, body } = await request(app).get(`/shared-link/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -276,9 +243,7 @@ describe('/shared-link', () => {
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Shared link not found' }),
);
expect(body).toEqual(expect.objectContaining({ message: 'Shared link not found' }));
});
});
@@ -308,9 +273,7 @@ describe('/shared-link', () => {
.send({ type: SharedLinkType.Album });
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid albumId' }),
);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid albumId' }));
});
it('should require a valid asset id', async () => {
@@ -320,9 +283,7 @@ describe('/shared-link', () => {
.send({ type: SharedLinkType.Individual, assetId: uuidDto.notFound });
expect(status).toBe(400);
expect(body).toEqual(
expect.objectContaining({ message: 'Invalid assetIds' }),
);
expect(body).toEqual(expect.objectContaining({ message: 'Invalid assetIds' }));
});
it('should create a shared link', async () => {
@@ -424,9 +385,7 @@ describe('/shared-link', () => {
describe('DELETE /shared-link/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/shared-link/${linkWithAlbum.id}`,
);
const { status, body } = await request(app).delete(`/shared-link/${linkWithAlbum.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, dbUtils } from 'src/utils';
import { app, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@@ -10,17 +10,14 @@ describe('/system-config', () => {
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup();
nonAdmin = await apiUtils.userSetup(admin.accessToken, createUserDto.user1);
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
});
describe('GET /system-config/map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(
'/system-config/map/style.json'
);
const { status, body } = await request(app).get('/system-config/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -32,11 +29,7 @@ describe('/system-config', () => {
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(
errorDto.badRequest([
'theme must be one of the following values: light, dark',
])
);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, getAllAssets } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils, wsUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
@@ -10,14 +10,13 @@ describe('/trash', () => {
let ws: Socket;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
ws = await wsUtils.connect(admin.accessToken);
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
ws = await utils.connectWebsocket(admin.accessToken);
});
afterAll(() => {
wsUtils.disconnect(ws);
utils.disconnectWebsocket(ws);
});
describe('POST /trash/empty', () => {
@@ -29,27 +28,19 @@ describe('/trash', () => {
});
it('should empty the trash', async () => {
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await getAllAssets(
{},
{ headers: asBearerAuth(admin.accessToken) },
);
const before = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(before.length).toBeGreaterThanOrEqual(1);
const { status } = await request(app)
.post('/trash/empty')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
await wsUtils.waitForEvent({ event: 'delete', assetId });
await utils.waitForWebsocketEvent({ event: 'delete', assetId });
const after = await getAllAssets(
{},
{ headers: asBearerAuth(admin.accessToken) },
);
const after = await getAllAssets({}, { headers: asBearerAuth(admin.accessToken) });
expect(after.length).toBe(0);
});
});
@@ -63,18 +54,16 @@ describe('/trash', () => {
});
it('should restore all trashed assets', async () => {
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
const { status } = await request(app)
.post('/trash/restore')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status } = await request(app).post('/trash/restore').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false);
});
});
@@ -88,10 +77,10 @@ describe('/trash', () => {
});
it('should restore a trashed asset by id', async () => {
const { id: assetId } = await apiUtils.createAsset(admin.accessToken);
await apiUtils.deleteAssets(admin.accessToken, [assetId]);
const { id: assetId } = await utils.createAsset(admin.accessToken);
await utils.deleteAssets(admin.accessToken, [assetId]);
const before = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const before = await utils.getAssetInfo(admin.accessToken, assetId);
expect(before.isTrashed).toBe(true);
const { status } = await request(app)
@@ -100,7 +89,7 @@ describe('/trash', () => {
.send({ ids: [assetId] });
expect(status).toBe(204);
const after = await apiUtils.getAssetInfo(admin.accessToken, assetId);
const after = await utils.getAssetInfo(admin.accessToken, assetId);
expect(after.isTrashed).toBe(false);
});
});

View File

@@ -1,7 +1,7 @@
import { LoginResponseDto, deleteUser, getUserById } from '@immich/sdk';
import { createUserDto, userDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { apiUtils, app, asBearerAuth, dbUtils } from 'src/utils';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
import { beforeAll, describe, expect, it } from 'vitest';
@@ -12,20 +12,16 @@ describe('/server-info', () => {
let nonAdmin: LoginResponseDto;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup({ onboarding: false });
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
[deletedUser, nonAdmin, userToDelete] = await Promise.all([
apiUtils.userSetup(admin.accessToken, createUserDto.user1),
apiUtils.userSetup(admin.accessToken, createUserDto.user2),
apiUtils.userSetup(admin.accessToken, createUserDto.user3),
utils.userSetup(admin.accessToken, createUserDto.user1),
utils.userSetup(admin.accessToken, createUserDto.user2),
utils.userSetup(admin.accessToken, createUserDto.user3),
]);
await deleteUser(
{ id: deletedUser.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
await deleteUser({ id: deletedUser.userId }, { headers: asBearerAuth(admin.accessToken) });
});
describe('GET /user', () => {
@@ -36,9 +32,7 @@ describe('/server-info', () => {
});
it('should get users', async () => {
const { status, body } = await request(app)
.get('/user')
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get('/user').set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toEqual(200);
expect(body).toHaveLength(4);
expect(body).toEqual(
@@ -47,7 +41,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
]),
);
});
@@ -63,7 +57,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'admin@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
]),
);
});
@@ -81,7 +75,7 @@ describe('/server-info', () => {
expect.objectContaining({ email: 'user1@immich.cloud' }),
expect.objectContaining({ email: 'user2@immich.cloud' }),
expect.objectContaining({ email: 'user3@immich.cloud' }),
])
]),
);
});
});
@@ -112,9 +106,7 @@ describe('/server-info', () => {
});
it('should get my info', async () => {
const { status, body } = await request(app)
.get(`/user/me`)
.set('Authorization', `Bearer ${admin.accessToken}`);
const { status, body } = await request(app).get(`/user/me`).set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
id: admin.userId,
@@ -125,9 +117,7 @@ describe('/server-info', () => {
describe('POST /user', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.post(`/user`)
.send(createUserDto.user1);
const { status, body } = await request(app).post(`/user`).send(createUserDto.user1);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -181,9 +171,7 @@ describe('/server-info', () => {
describe('DELETE /user/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(
`/user/${userToDelete.userId}`
);
const { status, body } = await request(app).delete(`/user/${userToDelete.userId}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
@@ -241,10 +229,7 @@ describe('/server-info', () => {
});
it('should ignore updates to createdAt, updatedAt and deletedAt', async () => {
const before = await getUserById(
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
@@ -261,10 +246,7 @@ describe('/server-info', () => {
});
it('should update first and last name', async () => {
const before = await getUserById(
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
@@ -284,10 +266,7 @@ describe('/server-info', () => {
});
it('should update memories enabled', async () => {
const before = await getUserById(
{ id: admin.userId },
{ headers: asBearerAuth(admin.accessToken) }
);
const before = await getUserById({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/user`)
.send({

View File

@@ -1,14 +1,10 @@
import { stat } from 'node:fs/promises';
import { apiUtils, app, dbUtils, immichCli } from 'src/utils';
import { beforeEach, beforeAll, describe, expect, it } from 'vitest';
import { app, immichCli, utils } from 'src/utils';
import { beforeEach, describe, expect, it } from 'vitest';
describe(`immich login-key`, () => {
beforeAll(() => {
apiUtils.setup();
});
beforeEach(async () => {
await dbUtils.reset();
await utils.resetDatabase();
});
it('should require a url', async () => {
@@ -24,27 +20,17 @@ describe(`immich login-key`, () => {
});
it('should require a valid key', async () => {
const { stderr, exitCode } = await immichCli([
'login-key',
app,
'immich-is-so-cool',
]);
expect(stderr).toContain(
'Failed to connect to server http://127.0.0.1:2283/api: Error: 401'
);
const { stderr, exitCode } = await immichCli(['login-key', app, 'immich-is-so-cool']);
expect(stderr).toContain('Failed to connect to server http://127.0.0.1:2283/api: Error: 401');
expect(exitCode).toBe(1);
});
it('should login', async () => {
const admin = await apiUtils.adminSetup();
const key = await apiUtils.createApiKey(admin.accessToken);
const { stdout, stderr, exitCode } = await immichCli([
'login-key',
app,
`${key.secret}`,
]);
it('should login and save auth.yml with 600', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken);
const { stdout, stderr, exitCode } = await immichCli(['login-key', app, `${key.secret}`]);
expect(stdout.split('\n')).toEqual([
'Logging in...',
'Logging in to http://127.0.0.1:2283/api',
'Logged in as admin@immich.cloud',
'Wrote auth info to /tmp/immich/auth.yml',
]);
@@ -55,4 +41,18 @@ describe(`immich login-key`, () => {
const mode = (stats.mode & 0o777).toString(8);
expect(mode).toEqual('600');
});
it('should login without /api in the url', async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken);
const { stdout, stderr, exitCode } = await immichCli(['login-key', app.replaceAll('/api', ''), `${key.secret}`]);
expect(stdout.split('\n')).toEqual([
'Logging in to http://127.0.0.1:2283',
'Discovered API at http://127.0.0.1:2283/api',
'Logged in as admin@immich.cloud',
'Wrote auth info to /tmp/immich/auth.yml',
]);
expect(stderr).toBe('');
expect(exitCode).toBe(0);
});
});

View File

@@ -1,11 +1,10 @@
import { apiUtils, cliUtils, dbUtils, immichCli } from 'src/utils';
import { immichCli, utils } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
describe(`immich server-info`, () => {
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
await cliUtils.login();
await utils.resetDatabase();
await utils.cliLogin();
});
it('should return the server info', async () => {

View File

@@ -1,40 +1,26 @@
import { getAllAlbums, getAllAssets } from '@immich/sdk';
import { mkdir, readdir, rm, symlink } from 'fs/promises';
import {
apiUtils,
asKeyAuth,
cliUtils,
dbUtils,
immichCli,
testAssetDir,
} from 'src/utils';
import { mkdir, readdir, rm, symlink } from 'node:fs/promises';
import { asKeyAuth, immichCli, testAssetDir, utils } from 'src/utils';
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe(`immich upload`, () => {
let key: string;
beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
key = await cliUtils.login();
await utils.resetDatabase();
key = await utils.cliLogin();
});
beforeEach(async () => {
await dbUtils.reset(['assets', 'albums']);
await utils.resetDatabase(['assets', 'albums']);
});
describe('immich upload --recursive', () => {
it('should upload a folder recursively', async () => {
const { stderr, stdout, exitCode } = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
const { stderr, stdout, exitCode } = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
expect(stderr).toBe('');
expect(stdout.split('\n')).toEqual(
expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'),
]),
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
);
expect(exitCode).toBe(0);
@@ -70,15 +56,9 @@ describe(`immich upload`, () => {
});
it('should add existing assets to albums', async () => {
const response1 = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
]);
const response1 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive']);
expect(response1.stdout.split('\n')).toEqual(
expect.arrayContaining([
expect.stringContaining('Successfully uploaded 9 assets'),
]),
expect.arrayContaining([expect.stringContaining('Successfully uploaded 9 assets')]),
);
expect(response1.stderr).toBe('');
expect(response1.exitCode).toBe(0);
@@ -89,17 +69,10 @@ describe(`immich upload`, () => {
const albums1 = await getAllAlbums({}, { headers: asKeyAuth(key) });
expect(albums1.length).toBe(0);
const response2 = await immichCli([
'upload',
`${testAssetDir}/albums/nature/`,
'--recursive',
'--album',
]);
const response2 = await immichCli(['upload', `${testAssetDir}/albums/nature/`, '--recursive', '--album']);
expect(response2.stdout.split('\n')).toEqual(
expect.arrayContaining([
expect.stringContaining(
'All assets were already uploaded, nothing to do.',
),
expect.stringContaining('All assets were already uploaded, nothing to do.'),
expect.stringContaining('Successfully updated 9 assets'),
]),
);
@@ -147,17 +120,10 @@ describe(`immich upload`, () => {
await mkdir(`/tmp/albums/nature`, { recursive: true });
const filesToLink = await readdir(`${testAssetDir}/albums/nature`);
for (const file of filesToLink) {
await symlink(
`${testAssetDir}/albums/nature/${file}`,
`/tmp/albums/nature/${file}`,
);
await symlink(`${testAssetDir}/albums/nature/${file}`, `/tmp/albums/nature/${file}`);
}
const { stderr, stdout, exitCode } = await immichCli([
'upload',
`/tmp/albums/nature`,
'--delete',
]);
const { stderr, stdout, exitCode } = await immichCli(['upload', `/tmp/albums/nature`, '--delete']);
const files = await readdir(`/tmp/albums/nature`);
await rm(`/tmp/albums/nature`, { recursive: true });

View File

@@ -1,14 +1,10 @@
import { readFileSync } from 'node:fs';
import { apiUtils, immichCli } from 'src/utils';
import { beforeAll, describe, expect, it } from 'vitest';
import { immichCli } from 'src/utils';
import { describe, expect, it } from 'vitest';
const pkg = JSON.parse(readFileSync('../cli/package.json', 'utf8'));
describe(`immich --version`, () => {
beforeAll(() => {
apiUtils.setup();
});
describe('immich --version', () => {
it('should print the cli version', async () => {
const { stdout, stderr, exitCode } = await immichCli(['--version']);

View File

@@ -44,7 +44,6 @@ export const userDto = {
email: signupDto.admin.email,
password: signupDto.admin.password,
storageLabel: 'admin',
externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',
@@ -63,7 +62,6 @@ export const userDto = {
email: createUserDto.user1.email,
password: createUserDto.user1.password,
storageLabel: null,
externalPath: null,
oauthId: '',
shouldChangePassword: false,
profileImagePath: '',

31
e2e/src/generators.ts Normal file
View File

@@ -0,0 +1,31 @@
import { PNG } from 'pngjs';
const createPNG = (r: number, g: number, b: number) => {
const image = new PNG({ width: 1, height: 1 });
image.data[0] = r;
image.data[1] = g;
image.data[2] = b;
image.data[3] = 255;
return PNG.sync.write(image);
};
function* newPngFactory() {
for (let r = 0; r < 255; r++) {
for (let g = 0; g < 255; g++) {
for (let b = 0; b < 255; b++) {
yield createPNG(r, g, b);
}
}
}
}
const pngFactory = newPngFactory();
export const makeRandomImage = () => {
const { value } = pngFactory.next();
if (!value) {
throw new Error('Ran out of random asset data');
}
return value;
};

View File

@@ -65,7 +65,6 @@ export const signupResponseDto = {
name: 'Immich Admin',
email: 'admin@immich.cloud',
storageLabel: 'admin',
externalPath: null,
profileImagePath: '',
// why? lol
shouldChangePassword: true,
@@ -77,6 +76,7 @@ export const signupResponseDto = {
memoriesEnabled: true,
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',
},
};

View File

@@ -1,8 +1,16 @@
import { spawn, exec } from 'child_process';
import { exec, spawn } from 'node:child_process';
import { setTimeout } from 'node:timers';
export default async () => {
let _resolve: () => unknown;
const ready = new Promise<void>((resolve) => (_resolve = resolve));
let _reject: (error: Error) => unknown;
const ready = new Promise<void>((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
const timeout = setTimeout(() => _reject(new Error('Timeout starting e2e environment')), 60_000);
const child = spawn('docker', ['compose', 'up'], { stdio: 'pipe' });
@@ -17,10 +25,9 @@ export default async () => {
child.stderr.on('data', (data) => console.log(data.toString()));
await ready;
clearTimeout(timeout);
return async () => {
await new Promise<void>((resolve) =>
exec('docker compose down', () => resolve()),
);
await new Promise<void>((resolve) => exec('docker compose down', () => resolve()));
};
};

View File

@@ -3,11 +3,14 @@ import {
AssetResponseDto,
CreateAlbumDto,
CreateAssetDto,
CreateLibraryDto,
CreateUserDto,
PersonUpdateDto,
PersonCreateDto,
SharedLinkCreateDto,
ValidateLibraryDto,
createAlbum,
createApiKey,
createLibrary,
createPerson,
createSharedLink,
createUser,
@@ -17,147 +20,41 @@ import {
login,
setAdminOnboarding,
signUpAdmin,
updatePerson,
validate,
} from '@immich/sdk';
import { BrowserContext } from '@playwright/test';
import { exec, spawn } from 'node:child_process';
import { randomBytes } from 'node:crypto';
import { access } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { existsSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { EventEmitter } from 'node:stream';
import { promisify } from 'node:util';
import pg from 'pg';
import { io, type Socket } from 'socket.io-client';
import { loginDto, signupDto } from 'src/fixtures';
import { makeRandomImage } from 'src/generators';
import request from 'supertest';
const execPromise = promisify(exec);
type CliResponse = { stdout: string; stderr: string; exitCode: number | null };
type EventType = 'upload' | 'delete';
type WaitOptions = { event: EventType; assetId: string; timeout?: number };
type AdminSetupOptions = { onboarding?: boolean };
type AssetData = { bytes?: Buffer; filename: string };
export const app = 'http://127.0.0.1:2283/api';
const directoryExists = (directory: string) =>
access(directory)
.then(() => true)
.catch(() => false);
const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich';
const baseUrl = 'http://127.0.0.1:2283';
export const app = `${baseUrl}/api`;
// TODO move test assets into e2e/assets
export const testAssetDir = path.resolve(`./../server/test/assets/`);
export const testAssetDirInternal = '/data/assets';
export const tempDir = tmpdir();
const serverContainerName = 'immich-e2e-server';
const mediaDir = '/usr/src/app/upload';
const dirs = [
`"${mediaDir}/thumbs"`,
`"${mediaDir}/upload"`,
`"${mediaDir}/library"`,
`"${mediaDir}/encoded-video"`,
].join(' ');
if (!(await directoryExists(`${testAssetDir}/albums`))) {
throw new Error(
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
);
}
export const asBearerAuth = (accessToken: string) => ({
Authorization: `Bearer ${accessToken}`,
});
export const asBearerAuth = (accessToken: string) => ({ Authorization: `Bearer ${accessToken}` });
export const asKeyAuth = (key: string) => ({ 'x-api-key': key });
let client: pg.Client | null = null;
export const fileUtils = {
reset: async () => {
await execPromise(
`docker exec -i "${serverContainerName}" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`,
);
},
};
export const dbUtils = {
createFace: async ({
assetId,
personId,
}: {
assetId: string;
personId: string;
}) => {
if (!client) {
return;
}
const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`;
await client.query(
'INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)',
[assetId, personId, embedding],
);
},
setPersonThumbnail: async (personId: string) => {
if (!client) {
return;
}
await client.query(
`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`,
[personId],
);
},
reset: async (tables?: string[]) => {
try {
if (!client) {
client = new pg.Client(
'postgres://postgres:postgres@127.0.0.1:5433/immich',
);
await client.connect();
}
tables = tables || [
'shared_links',
'person',
'albums',
'assets',
'asset_faces',
'activity',
'api_keys',
'user_token',
'users',
'system_metadata',
];
for (const table of tables) {
await client.query(`DELETE FROM ${table} CASCADE;`);
}
} catch (error) {
console.error('Failed to reset database', error);
throw error;
}
},
teardown: async () => {
try {
if (client) {
await client.end();
client = null;
}
} catch (error) {
console.error('Failed to teardown database', error);
throw error;
}
},
};
export interface CliResponse {
stdout: string;
stderr: string;
exitCode: number | null;
}
export const immichCli = async (args: string[]) => {
let _resolve: (value: CliResponse) => void;
const deferred = new Promise<CliResponse>((resolve) => (_resolve = resolve));
const _args = ['node_modules/.bin/immich', '-d', '/tmp/immich/', ...args];
const _args = ['node_modules/.bin/immich', '-d', `/${tempDir}/immich/`, ...args];
const child = spawn('node', _args, {
stdio: 'pipe',
});
@@ -178,21 +75,7 @@ export const immichCli = async (args: string[]) => {
return deferred;
};
export interface AdminSetupOptions {
onboarding?: boolean;
}
export enum SocketEvent {
UPLOAD = 'upload',
DELETE = 'delete',
}
export type EventType = 'upload' | 'delete';
export interface WaitOptions {
event: EventType;
assetId: string;
timeout?: number;
}
let client: pg.Client | null = null;
const events: Record<EventType, Set<string>> = {
upload: new Set<string>(),
@@ -201,6 +84,8 @@ const events: Record<EventType, Set<string>> = {
const callbacks: Record<string, () => void> = {};
const execPromise = promisify(exec);
const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
events[event].add(assetId);
const callback = callbacks[assetId];
@@ -210,9 +95,57 @@ const onEvent = ({ event, assetId }: { event: EventType; assetId: string }) => {
}
};
export const wsUtils = {
connect: async (accessToken: string) => {
const websocket = io('http://127.0.0.1:2283', {
export const utils = {
resetDatabase: async (tables?: string[]) => {
try {
if (!client) {
client = new pg.Client(dbUrl);
await client.connect();
}
tables = tables || [
'libraries',
'shared_links',
'person',
'albums',
'assets',
'asset_faces',
'activity',
'api_keys',
'user_token',
'users',
'system_metadata',
];
for (const table of tables) {
await client.query(`DELETE FROM ${table} CASCADE;`);
}
} catch (error) {
console.error('Failed to reset database', error);
throw error;
}
},
resetFilesystem: async () => {
const mediaInternal = '/usr/src/app/upload';
const dirs = [
`"${mediaInternal}/thumbs"`,
`"${mediaInternal}/upload"`,
`"${mediaInternal}/library"`,
`"${mediaInternal}/encoded-video"`,
].join(' ');
await execPromise(`docker exec -i "immich-e2e-server" /bin/bash -c "rm -rf ${dirs} && mkdir ${dirs}"`);
},
unzip: async (input: string, output: string) => {
await execPromise(`unzip -o -d "${output}" "${input}"`);
},
sha1: (bytes: Buffer) => createHash('sha1').update(bytes).digest('base64'),
connectWebsocket: async (accessToken: string) => {
const websocket = io(baseUrl, {
path: '/api/socket.io',
transports: ['websocket'],
extraHeaders: { Authorization: `Bearer ${accessToken}` },
@@ -223,16 +156,13 @@ export const wsUtils = {
return new Promise<Socket>((resolve) => {
websocket
.on('connect', () => resolve(websocket))
.on('on_upload_success', (data: AssetResponseDto) =>
onEvent({ event: 'upload', assetId: data.id }),
)
.on('on_asset_delete', (assetId: string) =>
onEvent({ event: 'delete', assetId }),
)
.on('on_upload_success', (data: AssetResponseDto) => onEvent({ event: 'upload', assetId: data.id }))
.on('on_asset_delete', (assetId: string) => onEvent({ event: 'delete', assetId }))
.connect();
});
},
disconnect: (ws: Socket) => {
disconnectWebsocket: (ws: Socket) => {
if (ws?.connected) {
ws.disconnect();
}
@@ -241,21 +171,16 @@ export const wsUtils = {
set.clear();
}
},
waitForEvent: async ({
event,
assetId,
timeout: ms,
}: WaitOptions): Promise<void> => {
waitForWebsocketEvent: async ({ event, assetId, timeout: ms }: WaitOptions): Promise<void> => {
console.log(`Waiting for ${event} [${assetId}]`);
const set = events[event];
if (set.has(assetId)) {
return;
}
return new Promise<void>((resolve, reject) => {
const timeout = setTimeout(
() => reject(new Error(`Timed out waiting for ${event} event`)),
ms || 5000,
);
const timeout = setTimeout(() => reject(new Error(`Timed out waiting for ${event} event`)), ms || 10_000);
callbacks[assetId] = () => {
clearTimeout(timeout);
@@ -263,10 +188,8 @@ export const wsUtils = {
};
});
},
};
export const apiUtils = {
setup: () => {
setApiEndpoint: () => {
defaults.baseUrl = app;
},
@@ -280,99 +203,98 @@ export const apiUtils = {
}
return response;
},
userSetup: async (accessToken: string, dto: CreateUserDto) => {
await createUser(
{ createUserDto: dto },
{ headers: asBearerAuth(accessToken) },
);
await createUser({ createUserDto: dto }, { headers: asBearerAuth(accessToken) });
return login({
loginCredentialDto: { email: dto.email, password: dto.password },
});
},
createApiKey: (accessToken: string) => {
return createApiKey(
{ apiKeyCreateDto: { name: 'e2e' } },
{ headers: asBearerAuth(accessToken) },
);
return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) });
},
createAlbum: (accessToken: string, dto: CreateAlbumDto) =>
createAlbum(
{ createAlbumDto: dto },
{ headers: asBearerAuth(accessToken) },
),
createAlbum({ createAlbumDto: dto }, { headers: asBearerAuth(accessToken) }),
createAsset: async (
accessToken: string,
dto?: Partial<Omit<CreateAssetDto, 'assetData'>>,
data?: {
bytes?: Buffer;
filename?: string;
},
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
) => {
const _dto = {
deviceAssetId: 'test-1',
deviceId: 'test',
fileCreatedAt: new Date().toISOString(),
fileModifiedAt: new Date().toISOString(),
...(dto || {}),
...dto,
};
const _assetData = {
bytes: randomBytes(32),
filename: 'example.jpg',
...(data || {}),
};
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)
.post(`/asset/upload`)
.attach('assetData', _assetData.bytes, _assetData.filename)
.attach('assetData', assetData, filename)
.set('Authorization', `Bearer ${accessToken}`);
for (const [key, value] of Object.entries(_dto)) {
builder.field(key, String(value));
void builder.field(key, String(value));
}
const { body } = await builder;
return body as AssetFileUploadResponseDto;
},
getAssetInfo: (accessToken: string, id: string) =>
getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets(
{ assetBulkDeleteDto: { ids } },
{ headers: asBearerAuth(accessToken) },
),
createPerson: async (accessToken: string, dto?: PersonUpdateDto) => {
// TODO fix createPerson to accept a body
let person = await createPerson({ headers: asBearerAuth(accessToken) });
await dbUtils.setPersonThumbnail(person.id);
if (!dto) {
return person;
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
createPerson: async (accessToken: string, dto?: PersonCreateDto) => {
const person = await createPerson({ personCreateDto: dto || {} }, { headers: asBearerAuth(accessToken) });
await utils.setPersonThumbnail(person.id);
return person;
},
createFace: async ({ assetId, personId }: { assetId: string; personId: string }) => {
if (!client) {
return;
}
return updatePerson(
{ id: person.id, personUpdateDto: dto },
{ headers: asBearerAuth(accessToken) },
);
const vector = Array.from({ length: 512 }, Math.random);
const embedding = `[${vector.join(',')}]`;
await client.query('INSERT INTO asset_faces ("assetId", "personId", "embedding") VALUES ($1, $2, $3)', [
assetId,
personId,
embedding,
]);
},
setPersonThumbnail: async (personId: string) => {
if (!client) {
return;
}
await client.query(`UPDATE "person" set "thumbnailPath" = '/my/awesome/thumbnail.jpg' where "id" = $1`, [personId]);
},
createSharedLink: (accessToken: string, dto: SharedLinkCreateDto) =>
createSharedLink(
{ sharedLinkCreateDto: dto },
{ headers: asBearerAuth(accessToken) },
),
};
createSharedLink({ sharedLinkCreateDto: dto }, { headers: asBearerAuth(accessToken) }),
export const cliUtils = {
login: async () => {
const admin = await apiUtils.adminSetup();
const key = await apiUtils.createApiKey(admin.accessToken);
await immichCli(['login-key', app, `${key.secret}`]);
return key.secret;
},
};
createLibrary: (accessToken: string, dto: CreateLibraryDto) =>
createLibrary({ createLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
validateLibrary: (accessToken: string, id: string, dto: ValidateLibraryDto) =>
validate({ id, validateLibraryDto: dto }, { headers: asBearerAuth(accessToken) }),
export const webUtils = {
setAuthCookies: async (context: BrowserContext, accessToken: string) =>
await context.addCookies([
{
@@ -380,7 +302,7 @@ export const webUtils = {
value: accessToken,
domain: '127.0.0.1',
path: '/',
expires: 1742402728,
expires: 1_742_402_728,
httpOnly: true,
secure: false,
sameSite: 'Lax',
@@ -390,7 +312,7 @@ export const webUtils = {
value: 'password',
domain: '127.0.0.1',
path: '/',
expires: 1742402728,
expires: 1_742_402_728,
httpOnly: true,
secure: false,
sameSite: 'Lax',
@@ -400,10 +322,25 @@ export const webUtils = {
value: 'true',
domain: '127.0.0.1',
path: '/',
expires: 1742402728,
expires: 1_742_402_728,
httpOnly: false,
secure: false,
sameSite: 'Lax',
},
]),
cliLogin: async () => {
const admin = await utils.adminSetup();
const key = await utils.createApiKey(admin.accessToken);
await immichCli(['login-key', app, `${key.secret}`]);
return key.secret;
},
};
utils.setApiEndpoint();
if (!existsSync(`${testAssetDir}/albums`)) {
throw new Error(
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${testAssetDir} before testing`,
);
}

View File

@@ -1,17 +1,13 @@
import { test, expect } from '@playwright/test';
import { apiUtils, dbUtils, webUtils } from 'src/utils';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Registration', () => {
test.beforeAll(() => {
apiUtils.setup();
utils.setApiEndpoint();
});
test.beforeEach(async () => {
await dbUtils.reset();
});
test.afterAll(async () => {
await dbUtils.teardown();
await utils.resetDatabase();
});
test('admin registration', async ({ page }) => {
@@ -45,8 +41,8 @@ test.describe('Registration', () => {
});
test('user registration', async ({ context, page }) => {
const admin = await apiUtils.adminSetup();
await webUtils.setAuthCookies(context, admin.accessToken);
const admin = await utils.adminSetup();
await utils.setAuthCookies(context, admin.accessToken);
// create user
await page.goto('/admin/user-management');
@@ -68,7 +64,7 @@ test.describe('Registration', () => {
await page.getByRole('button', { name: 'Login' }).click();
// change password
expect(page.getByRole('heading')).toHaveText('Change Password');
await expect(page.getByRole('heading')).toHaveText('Change Password');
await expect(page).toHaveURL('/auth/change-password');
await page.getByLabel('New Password').fill('new-password');
await page.getByLabel('Confirm Password').fill('new-password');

View File

@@ -1,27 +1,26 @@
import {
AlbumResponseDto,
AssetResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
SharedLinkResponseDto,
SharedLinkType,
createAlbum,
createSharedLink,
} from '@immich/sdk';
import { test } from '@playwright/test';
import { apiUtils, asBearerAuth, dbUtils } from 'src/utils';
import { asBearerAuth, utils } from 'src/utils';
test.describe('Shared Links', () => {
let admin: LoginResponseDto;
let asset: AssetResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
let sharedLink: SharedLinkResponseDto;
let sharedLinkPassword: SharedLinkResponseDto;
test.beforeAll(async () => {
apiUtils.setup();
await dbUtils.reset();
admin = await apiUtils.adminSetup();
asset = await apiUtils.createAsset(admin.accessToken);
utils.setApiEndpoint();
await utils.resetDatabase();
admin = await utils.adminSetup();
asset = await utils.createAsset(admin.accessToken);
album = await createAlbum(
{
createAlbumDto: {
@@ -29,23 +28,19 @@ test.describe('Shared Links', () => {
assetIds: [asset.id],
},
},
{ headers: asBearerAuth(admin.accessToken) }
{ headers: asBearerAuth(admin.accessToken) },
);
sharedLink = await apiUtils.createSharedLink(admin.accessToken, {
sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
});
sharedLinkPassword = await apiUtils.createSharedLink(admin.accessToken, {
sharedLinkPassword = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Album,
albumId: album.id,
password: 'test-password',
});
});
test.afterAll(async () => {
await dbUtils.teardown();
});
test('download from a shared link', async ({ page }) => {
await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
@@ -53,7 +48,7 @@ test.describe('Shared Links', () => {
await page.waitForSelector('#asset-group-by-date svg');
await page.getByRole('checkbox').click();
await page.getByRole('button', { name: 'Download' }).click();
await page.getByText('DOWNLOADING').waitFor();
await page.getByText('DOWNLOADING', { exact: true }).waitFor();
});
test('enter password for a shared link', async ({ page }) => {

View File

@@ -18,5 +18,6 @@
"rootDirs": ["src"],
"baseUrl": "./"
},
"include": ["src/**/*.ts"],
"exclude": ["dist", "node_modules"]
}

View File

@@ -1,9 +1,18 @@
import { defineConfig } from 'vitest/config';
// skip `docker compose up` if `make e2e` was already run
const globalSetup: string[] = [];
try {
await fetch('http://127.0.0.1:2283/api/server-info/ping');
} catch {
globalSetup.push('src/setup.ts');
}
export default defineConfig({
test: {
include: ['src/{api,cli}/specs/*.e2e-spec.ts'],
globalSetup: ['src/setup.ts'],
globalSetup,
testTimeout: 10_000,
poolOptions: {
threads: {
singleThread: true,

View File

@@ -167,6 +167,10 @@ cython_debug/
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
.idea/
# VS Code
.vscode
*.onnx
*.zip
*.zip
core

View File

@@ -39,7 +39,7 @@ FROM python:3.11-slim-bookworm@sha256:ce81dc539f0aedc9114cae640f8352fad83d37461c
FROM openvino/ubuntu22_runtime:2023.1.0@sha256:002842a9005ba01543b7169ff6f14ecbec82287f09c4d1dd37717f0a8e8754a7 as prod-openvino
USER root
FROM nvidia/cuda:11.8.0-cudnn8-runtime-ubuntu22.04@sha256:85fb7ac694079fff1061a0140fd5b5a641997880e12112d92589c3bbb1e8b7ca as prod-cuda
FROM nvidia/cuda:12.2.2-cudnn8-runtime-ubuntu22.04@sha256:2d913b09e6be8387e1a10976933642c73c840c0b735f0bf3c28d97fc9bc422e0 as prod-cuda
COPY --from=builder-cuda /usr/local/bin/python3 /usr/local/bin/python3
COPY --from=builder-cuda /usr/local/lib/python3.11 /usr/local/lib/python3.11

View File

@@ -6,7 +6,7 @@ from pathlib import Path
from socket import socket
from gunicorn.arbiter import Arbiter
from pydantic import BaseSettings
from pydantic import BaseModel, BaseSettings
from rich.console import Console
from rich.logging import RichHandler
from uvicorn import Server
@@ -15,6 +15,11 @@ from uvicorn.workers import UvicornWorker
from .schemas import ModelType
class PreloadModelData(BaseModel):
clip: str | None
facial_recognition: str | None
class Settings(BaseSettings):
cache_folder: str = "/cache"
model_ttl: int = 300
@@ -27,10 +32,12 @@ class Settings(BaseSettings):
model_inter_op_threads: int = 0
model_intra_op_threads: int = 0
ann: bool = True
preload: PreloadModelData | None = None
class Config:
env_prefix = "MACHINE_LEARNING_"
case_sensitive = False
env_nested_delimiter = "__"
class LogSettings(BaseSettings):

View File

@@ -17,7 +17,7 @@ from starlette.formparsers import MultiPartParser
from app.models.base import InferenceModel
from .config import log, settings
from .config import PreloadModelData, log, settings
from .models.cache import ModelCache
from .schemas import (
MessageResponse,
@@ -27,7 +27,7 @@ from .schemas import (
MultiPartParser.max_file_size = 2**26 # spools to disk if payload is 64 MiB or larger
model_cache = ModelCache(ttl=settings.model_ttl, revalidate=settings.model_ttl > 0)
model_cache = ModelCache(revalidate=settings.model_ttl > 0)
thread_pool: ThreadPoolExecutor | None = None
lock = threading.Lock()
active_requests = 0
@@ -51,6 +51,8 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
log.info(f"Initialized request thread pool with {settings.request_threads} threads.")
if settings.model_ttl > 0 and settings.model_ttl_poll_s > 0:
asyncio.ensure_future(idle_shutdown_task())
if settings.preload is not None:
await preload_models(settings.preload)
yield
finally:
log.handlers.clear()
@@ -61,6 +63,14 @@ async def lifespan(_: FastAPI) -> AsyncGenerator[None, None]:
gc.collect()
async def preload_models(preload_models: PreloadModelData) -> None:
log.info(f"Preloading models: {preload_models}")
if preload_models.clip is not None:
await load(await model_cache.get(preload_models.clip, ModelType.CLIP))
if preload_models.facial_recognition is not None:
await load(await model_cache.get(preload_models.facial_recognition, ModelType.FACIAL_RECOGNITION))
def update_state() -> Iterator[None]:
global active_requests, last_called
active_requests += 1
@@ -103,7 +113,7 @@ async def predict(
except orjson.JSONDecodeError:
raise HTTPException(400, f"Invalid options JSON: {options}")
model = await load(await model_cache.get(model_name, model_type, **kwargs))
model = await load(await model_cache.get(model_name, model_type, ttl=settings.model_ttl, **kwargs))
model.configure(**kwargs)
outputs = await run(model.predict, inputs)
return ORJSONResponse(outputs)

View File

@@ -2,7 +2,7 @@ from typing import Any
from aiocache.backends.memory import SimpleMemoryCache
from aiocache.lock import OptimisticLock
from aiocache.plugins import BasePlugin, TimingPlugin
from aiocache.plugins import TimingPlugin
from app.models import from_model_type
@@ -15,28 +15,25 @@ class ModelCache:
def __init__(
self,
ttl: float | None = None,
revalidate: bool = False,
timeout: int | None = None,
profiling: bool = False,
) -> None:
"""
Args:
ttl: Unloads model after this duration. Disabled if None. Defaults to None.
revalidate: Resets TTL on cache hit. Useful to keep models in memory while active. Defaults to False.
timeout: Maximum allowed time for model to load. Disabled if None. Defaults to None.
profiling: Collects metrics for cache operations, adding slight overhead. Defaults to False.
"""
self.ttl = ttl
plugins = []
if revalidate:
plugins.append(RevalidationPlugin())
if profiling:
plugins.append(TimingPlugin())
self.cache = SimpleMemoryCache(ttl=ttl, timeout=timeout, plugins=plugins, namespace=None)
self.revalidate_enable = revalidate
self.cache = SimpleMemoryCache(timeout=timeout, plugins=plugins, namespace=None)
async def get(self, model_name: str, model_type: ModelType, **model_kwargs: Any) -> InferenceModel:
"""
@@ -49,11 +46,14 @@ class ModelCache:
"""
key = f"{model_name}{model_type.value}{model_kwargs.get('mode', '')}"
async with OptimisticLock(self.cache, key) as lock:
model: InferenceModel | None = await self.cache.get(key)
if model is None:
model = from_model_type(model_type, model_name, **model_kwargs)
await lock.cas(model, ttl=self.ttl)
await lock.cas(model, ttl=model_kwargs.get("ttl", None))
elif self.revalidate_enable:
await self.revalidate(key, model_kwargs.get("ttl", None))
return model
async def get_profiling(self) -> dict[str, float] | None:
@@ -62,21 +62,6 @@ class ModelCache:
return self.cache.profiling
class RevalidationPlugin(BasePlugin): # type: ignore[misc]
"""Revalidates cache item's TTL after cache hit."""
async def post_get(
self,
client: SimpleMemoryCache,
key: str,
ret: Any | None = None,
namespace: str | None = None,
**kwargs: Any,
) -> None:
if ret is None:
return
if namespace is not None:
key = client.build_key(key, namespace)
if key in client._handlers:
await client.expire(key, client.ttl)
async def revalidate(self, key: str, ttl: int | None) -> None:
if ttl is not None and key in self.cache._handlers:
await self.cache.expire(key, ttl)

View File

@@ -13,11 +13,12 @@ import onnxruntime as ort
import pytest
from fastapi.testclient import TestClient
from PIL import Image
from pytest import MonkeyPatch
from pytest_mock import MockerFixture
from app.main import load
from app.main import load, preload_models
from .config import log, settings
from .config import Settings, log, settings
from .models.base import InferenceModel
from .models.cache import ModelCache
from .models.clip import MCLIPEncoder, OpenCLIPEncoder
@@ -509,20 +510,20 @@ class TestCache:
@mock.patch("app.models.cache.OptimisticLock", autospec=True)
async def test_model_ttl(self, mock_lock_cls: mock.Mock, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache(ttl=100)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
model_cache = ModelCache()
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
mock_lock_cls.return_value.__aenter__.return_value.cas.assert_called_with(mock.ANY, ttl=100)
@mock.patch("app.models.cache.SimpleMemoryCache.expire")
async def test_revalidate_get(self, mock_cache_expire: mock.Mock, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache(ttl=100, revalidate=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
model_cache = ModelCache(revalidate=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
mock_cache_expire.assert_called_once_with(mock.ANY, 100)
async def test_profiling(self, mock_get_model: mock.Mock) -> None:
model_cache = ModelCache(ttl=100, profiling=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION)
model_cache = ModelCache(profiling=True)
await model_cache.get("test_model_name", ModelType.FACIAL_RECOGNITION, ttl=100)
profiling = await model_cache.get_profiling()
assert isinstance(profiling, dict)
assert profiling == model_cache.cache.profiling
@@ -548,6 +549,25 @@ class TestCache:
with pytest.raises(ValueError):
await model_cache.get("test_model_name", ModelType.CLIP, mode="text")
async def test_preloads_models(self, monkeypatch: MonkeyPatch, mock_get_model: mock.Mock) -> None:
os.environ["MACHINE_LEARNING_PRELOAD__CLIP"] = "ViT-B-32__openai"
os.environ["MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION"] = "buffalo_s"
settings = Settings()
assert settings.preload is not None
assert settings.preload.clip == "ViT-B-32__openai"
assert settings.preload.facial_recognition == "buffalo_s"
model_cache = ModelCache()
monkeypatch.setattr("app.main.model_cache", model_cache)
await preload_models(settings.preload)
assert len(model_cache.cache._cache) == 2
assert mock_get_model.call_count == 2
await model_cache.get("ViT-B-32__openai", ModelType.CLIP, ttl=100)
await model_cache.get("buffalo_s", ModelType.FACIAL_RECOGNITION, ttl=100)
assert mock_get_model.call_count == 2
@pytest.mark.asyncio
class TestLoad:

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:6038b89363c9181215f3d9e8ce2720c880e224537f4028a854482e43a9b4998a as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:96586e238e2fed914b839e50cf91943b5655262348d141466b34ced2e0b5b155 as builder
ENV NODE_ENV=production \
TRANSFORMERS_CACHE=/cache \

View File

@@ -1,4 +1,4 @@
# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
# This file is automatically @generated by Poetry 1.8.1 and should not be changed by hand.
[[package]]
name = "aiocache"
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi"
version = "0.109.2"
version = "0.110.0"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
{file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"},
{file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"},
{file = "fastapi-0.110.0-py3-none-any.whl", hash = "sha256:87a1f6fb632a218222c5984be540055346a8f5d8a68e8f6fb647b1dc9934de4b"},
{file = "fastapi-0.110.0.tar.gz", hash = "sha256:266775f0dcc95af9d3ef39bad55cff525329a931d5fd51930aadd4f428bf7ff3"},
]
[package.dependencies]
@@ -1274,13 +1274,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.20.3"
version = "0.21.3"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.20.3-py3-none-any.whl", hash = "sha256:d988ae4f00d3e307b0c80c6a05ca6dbb7edba8bba3079f74cda7d9c2e562a7b6"},
{file = "huggingface_hub-0.20.3.tar.gz", hash = "sha256:94e7f8e074475fbc67d6a71957b678e1b4a74ff1b64a644fd6cbb83da962d05d"},
{file = "huggingface_hub-0.21.3-py3-none-any.whl", hash = "sha256:b183144336fdf2810a8c109822e0bb6ef1fd61c65da6fb60e8c3f658b7144016"},
{file = "huggingface_hub-0.21.3.tar.gz", hash = "sha256:26a15b604e4fc7bad37c467b76456543ec849386cbca9cd7e1e135f53e500423"},
]
[package.dependencies]
@@ -1297,11 +1297,12 @@ all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi",
cli = ["InquirerPy (==0.3.4)"]
dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "mypy (==1.5.1)", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.1.3)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"]
fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"]
hf-transfer = ["hf-transfer (>=0.1.4)"]
inference = ["aiohttp", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)"]
quality = ["mypy (==1.5.1)", "ruff (>=0.1.3)"]
tensorflow = ["graphviz", "pydot", "tensorflow"]
testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "gradio", "jedi", "numpy", "pydantic (>1.1,<2.0)", "pydantic (>1.1,<3.0)", "pytest", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"]
torch = ["torch"]
torch = ["safetensors", "torch"]
typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
[[package]]
@@ -1566,13 +1567,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.23.1"
version = "2.24.0"
description = "Developer friendly load testing framework"
optional = false
python-versions = ">=3.8"
files = [
{file = "locust-2.23.1-py3-none-any.whl", hash = "sha256:96013a460a4b4d6d4fd46c70e6ff1fd2b6e03b48ddb1b48d1513d3134ba2cecf"},
{file = "locust-2.23.1.tar.gz", hash = "sha256:6cc729729e5ebf5852fc9d845302cfcf0ab0132f198e68b3eb0c88b438b6a863"},
{file = "locust-2.24.0-py3-none-any.whl", hash = "sha256:1b6b878b4fd0108fec956120815e69775d2616c8f4d1e9f365c222a7a5c17d9a"},
{file = "locust-2.24.0.tar.gz", hash = "sha256:6cffa378d995244a7472af6be1d6139331f19aee44e907deee73e0281252804d"},
]
[package.dependencies]
@@ -1588,6 +1589,7 @@ pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
pyzmq = ">=25.0.0"
requests = ">=2.26.0"
roundrobin = ">=0.0.2"
tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
Werkzeug = ">=2.0.0"
[[package]]
@@ -1988,36 +1990,36 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
version = "1.17.0"
version = "1.17.1"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
{file = "onnxruntime-1.17.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d2b22a25a94109cc983443116da8d9805ced0256eb215c5e6bc6dcbabefeab96"},
{file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4c87d83c6f58d1af2675fc99e3dc810f2dbdb844bcefd0c1b7573632661f6fc"},
{file = "onnxruntime-1.17.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dba55723bf9b835e358f48c98a814b41692c393eb11f51e02ece0625c756b797"},
{file = "onnxruntime-1.17.0-cp310-cp310-win32.whl", hash = "sha256:ee48422349cc500273beea7607e33c2237909f58468ae1d6cccfc4aecd158565"},
{file = "onnxruntime-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:f34cc46553359293854e38bdae2ab1be59543aad78a6317e7746d30e311110c3"},
{file = "onnxruntime-1.17.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:16d26badd092c8c257fa57c458bb600d96dc15282c647ccad0ed7b2732e6c03b"},
{file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6f1273bebcdb47ed932d076c85eb9488bc4768fcea16d5f2747ca692fad4f9d3"},
{file = "onnxruntime-1.17.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cb60fd3c2c1acd684752eb9680e89ae223e9801a9b0e0dc7b28adabe45a2e380"},
{file = "onnxruntime-1.17.0-cp311-cp311-win32.whl", hash = "sha256:4b038324586bc905299e435f7c00007e6242389c856b82fe9357fdc3b1ef2bdc"},
{file = "onnxruntime-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:93d39b3fa1ee01f034f098e1c7769a811a21365b4883f05f96c14a2b60c6028b"},
{file = "onnxruntime-1.17.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:90c0890e36f880281c6c698d9bc3de2afbeee2f76512725ec043665c25c67d21"},
{file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7466724e809a40e986b1637cba156ad9fc0d1952468bc00f79ef340bc0199552"},
{file = "onnxruntime-1.17.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d47bee7557a8b99c8681b6882657a515a4199778d6d5e24e924d2aafcef55b0a"},
{file = "onnxruntime-1.17.0-cp312-cp312-win32.whl", hash = "sha256:bb1bf1ee575c665b8bbc3813ab906e091a645a24ccc210be7932154b8260eca1"},
{file = "onnxruntime-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac2f286da3494b29b4186ca193c7d4e6a2c1f770c4184c7192c5da142c3dec28"},
{file = "onnxruntime-1.17.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:1ec485643b93e0a3896c655eb2426decd63e18a278bb7ccebc133b340723624f"},
{file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:83c35809cda898c5a11911c69ceac8a2ac3925911854c526f73bad884582f911"},
{file = "onnxruntime-1.17.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa464aa4d81df818375239e481887b656e261377d5b6b9a4692466f5f3261edc"},
{file = "onnxruntime-1.17.0-cp38-cp38-win32.whl", hash = "sha256:b7b337cd0586f7836601623cbd30a443df9528ef23965860d11c753ceeb009f2"},
{file = "onnxruntime-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:fbb9faaf51d01aa2c147ef52524d9326744c852116d8005b9041809a71838878"},
{file = "onnxruntime-1.17.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:5a06ab84eaa350bf64b1d747b33ccf10da64221ed1f38f7287f15eccbec81603"},
{file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d3d11db2c8242766212a68d0b139745157da7ce53bd96ba349a5c65e5a02357"},
{file = "onnxruntime-1.17.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5632077c3ab8b0cd4f74b0af9c4e924be012b1a7bcd7daa845763c6c6bf14b7d"},
{file = "onnxruntime-1.17.0-cp39-cp39-win32.whl", hash = "sha256:61a12732cba869b3ad2d4e29ab6cb62c7a96f61b8c213f7fcb961ba412b70b37"},
{file = "onnxruntime-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:461fa0fc7d9c392c352b6cccdedf44d818430f3d6eacd924bb804fdea2dcfd02"},
{file = "onnxruntime-1.17.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d43ac17ac4fa3c9096ad3c0e5255bb41fd134560212dc124e7f52c3159af5d21"},
{file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55b5e92a4c76a23981c998078b9bf6145e4fb0b016321a8274b1607bd3c6bd35"},
{file = "onnxruntime-1.17.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ebbcd2bc3a066cf54e6f18c75708eb4d309ef42be54606d22e5bdd78afc5b0d7"},
{file = "onnxruntime-1.17.1-cp310-cp310-win32.whl", hash = "sha256:5e3716b5eec9092e29a8d17aab55e737480487deabfca7eac3cd3ed952b6ada9"},
{file = "onnxruntime-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:fbb98cced6782ae1bb799cc74ddcbbeeae8819f3ad1d942a74d88e72b6511337"},
{file = "onnxruntime-1.17.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:36fd6f87a1ecad87e9c652e42407a50fb305374f9a31d71293eb231caae18784"},
{file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99a8bddeb538edabc524d468edb60ad4722cff8a49d66f4e280c39eace70500b"},
{file = "onnxruntime-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd7fddb4311deb5a7d3390cd8e9b3912d4d963efbe4dfe075edbaf18d01c024e"},
{file = "onnxruntime-1.17.1-cp311-cp311-win32.whl", hash = "sha256:606a7cbfb6680202b0e4f1890881041ffc3ac6e41760a25763bd9fe146f0b335"},
{file = "onnxruntime-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:53e4e06c0a541696ebdf96085fd9390304b7b04b748a19e02cf3b35c869a1e76"},
{file = "onnxruntime-1.17.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:40f08e378e0f85929712a2b2c9b9a9cc400a90c8a8ca741d1d92c00abec60843"},
{file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac79da6d3e1bb4590f1dad4bb3c2979d7228555f92bb39820889af8b8e6bd472"},
{file = "onnxruntime-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ae9ba47dc099004e3781f2d0814ad710a13c868c739ab086fc697524061695ea"},
{file = "onnxruntime-1.17.1-cp312-cp312-win32.whl", hash = "sha256:2dff1a24354220ac30e4a4ce2fb1df38cb1ea59f7dac2c116238d63fe7f4c5ff"},
{file = "onnxruntime-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:6226a5201ab8cafb15e12e72ff2a4fc8f50654e8fa5737c6f0bd57c5ff66827e"},
{file = "onnxruntime-1.17.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:cd0c07c0d1dfb8629e820b05fda5739e4835b3b82faf43753d2998edf2cf00aa"},
{file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:617ebdf49184efa1ba6e4467e602fbfa029ed52c92f13ce3c9f417d303006381"},
{file = "onnxruntime-1.17.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9dae9071e3facdf2920769dceee03b71c684b6439021defa45b830d05e148924"},
{file = "onnxruntime-1.17.1-cp38-cp38-win32.whl", hash = "sha256:835d38fa1064841679433b1aa8138b5e1218ddf0cfa7a3ae0d056d8fd9cec713"},
{file = "onnxruntime-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:96621e0c555c2453bf607606d08af3f70fbf6f315230c28ddea91754e17ad4e6"},
{file = "onnxruntime-1.17.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:7a9539935fb2d78ebf2cf2693cad02d9930b0fb23cdd5cf37a7df813e977674d"},
{file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45c6a384e9d9a29c78afff62032a46a993c477b280247a7e335df09372aedbe9"},
{file = "onnxruntime-1.17.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4e19f966450f16863a1d6182a685ca33ae04d7772a76132303852d05b95411ea"},
{file = "onnxruntime-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e2ae712d64a42aac29ed7a40a426cb1e624a08cfe9273dcfe681614aa65b07dc"},
{file = "onnxruntime-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:f7e9f7fb049825cdddf4a923cfc7c649d84d63c0134315f8e0aa9e0c3004672c"},
]
[package.dependencies]
@@ -2030,21 +2032,21 @@ sympy = "*"
[[package]]
name = "onnxruntime-gpu"
version = "1.17.0"
version = "1.17.1"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1f2a4e0468ac0bd8246996c3d5dbba92cbbaca874bcd7f9cee4e99ce6eb27f5b"},
{file = "onnxruntime_gpu-1.17.0-cp310-cp310-win_amd64.whl", hash = "sha256:0721b7930d7abed3730b2335e639e60d94ec411bb4d35a0347cc9c8b52c34540"},
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:be0314afe399943904de7c1ca797cbcc63e6fad60eb85d3df6422f81dd94e79e"},
{file = "onnxruntime_gpu-1.17.0-cp311-cp311-win_amd64.whl", hash = "sha256:52125c24b21406d1431e43de1c98cea29c21e0cceba80db530b7e4c9216d86ea"},
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:bb802d8033885c412269f8bc8877d8779b0dc874df6fb9df8b796cba7276ad66"},
{file = "onnxruntime_gpu-1.17.0-cp312-cp312-win_amd64.whl", hash = "sha256:8c43533e3e5335eaa78059fb86b849a4faded513a00c1feaaa205ca5af51c40f"},
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:1d461455bba160836d6c11c648c8fd4e4500d5c17096a13e6c2c9d22a4abd436"},
{file = "onnxruntime_gpu-1.17.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4398f2175a92f4b35d95279a6294a89c462f24de058a2736ee1d498bab5a16"},
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1d0e3805cd1c024aba7f4ae576fd08545fc27530a2aaad2b3c8ac0ee889fbd05"},
{file = "onnxruntime_gpu-1.17.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc1da5b93363ee600b5b220b04eeec51ad2c2b3e96f0b7615b16b8a173c88001"},
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"},
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"},
]
[package.dependencies]
@@ -2055,6 +2057,11 @@ packaging = "*"
protobuf = "*"
sympy = "*"
[package.source]
type = "legacy"
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple"
reference = "cuda12"
[[package]]
name = "onnxruntime-openvino"
version = "1.15.0"
@@ -2808,13 +2815,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rich"
version = "13.7.0"
version = "13.7.1"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
{file = "rich-13.7.0-py3-none-any.whl", hash = "sha256:6da14c108c4866ee9520bbffa71f6fe3962e193b7da68720583850cd4548e235"},
{file = "rich-13.7.0.tar.gz", hash = "sha256:5cb5123b5cf9ee70584244246816e9114227e0b98ad9176eede6ad54bf5403fa"},
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
]
[package.dependencies]
@@ -2836,28 +2843,28 @@ files = [
[[package]]
name = "ruff"
version = "0.2.2"
version = "0.3.0"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:0a9efb032855ffb3c21f6405751d5e147b0c6b631e3ca3f6b20f917572b97eb6"},
{file = "ruff-0.2.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d450b7fbff85913f866a5384d8912710936e2b96da74541c82c1b458472ddb39"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ecd46e3106850a5c26aee114e562c329f9a1fbe9e4821b008c4404f64ff9ce73"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e22676a5b875bd72acd3d11d5fa9075d3a5f53b877fe7b4793e4673499318ba"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1695700d1e25a99d28f7a1636d85bafcc5030bba9d0578c0781ba1790dbcf51c"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b0c232af3d0bd8f521806223723456ffebf8e323bd1e4e82b0befb20ba18388e"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f63d96494eeec2fc70d909393bcd76c69f35334cdbd9e20d089fb3f0640216ca"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6a61ea0ff048e06de273b2e45bd72629f470f5da8f71daf09fe481278b175001"},
{file = "ruff-0.2.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e1439c8f407e4f356470e54cdecdca1bd5439a0673792dbe34a2b0a551a2fe3"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:940de32dc8853eba0f67f7198b3e79bc6ba95c2edbfdfac2144c8235114d6726"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:0c126da55c38dd917621552ab430213bdb3273bb10ddb67bc4b761989210eb6e"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:3b65494f7e4bed2e74110dac1f0d17dc8e1f42faaa784e7c58a98e335ec83d7e"},
{file = "ruff-0.2.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1ec49be4fe6ddac0503833f3ed8930528e26d1e60ad35c2446da372d16651ce9"},
{file = "ruff-0.2.2-py3-none-win32.whl", hash = "sha256:d920499b576f6c68295bc04e7b17b6544d9d05f196bb3aac4358792ef6f34325"},
{file = "ruff-0.2.2-py3-none-win_amd64.whl", hash = "sha256:cc9a91ae137d687f43a44c900e5d95e9617cb37d4c989e462980ba27039d239d"},
{file = "ruff-0.2.2-py3-none-win_arm64.whl", hash = "sha256:c9d15fc41e6054bfc7200478720570078f0b41c9ae4f010bcc16bd6f4d1aacdd"},
{file = "ruff-0.2.2.tar.gz", hash = "sha256:e62ed7f36b3068a30ba39193a14274cd706bc486fad521276458022f7bccb31d"},
{file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7deb528029bacf845bdbb3dbb2927d8ef9b4356a5e731b10eef171e3f0a85944"},
{file = "ruff-0.3.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e1e0d4381ca88fb2b73ea0766008e703f33f460295de658f5467f6f229658c19"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f7dbba46e2827dfcb0f0cc55fba8e96ba7c8700e0a866eb8cef7d1d66c25dcb"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:23dbb808e2f1d68eeadd5f655485e235c102ac6f12ad31505804edced2a5ae77"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ef655c51f41d5fa879f98e40c90072b567c666a7114fa2d9fe004dffba00932"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d0d3d7ef3d4f06433d592e5f7d813314a34601e6c5be8481cccb7fa760aa243e"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b08b356d06a792e49a12074b62222f9d4ea2a11dca9da9f68163b28c71bf1dd4"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9343690f95710f8cf251bee1013bf43030072b9f8d012fbed6ad702ef70d360a"},
{file = "ruff-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a1f3ed501a42f60f4dedb7805fa8d4534e78b4e196f536bac926f805f0743d49"},
{file = "ruff-0.3.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:cc30a9053ff2f1ffb505a585797c23434d5f6c838bacfe206c0e6cf38c921a1e"},
{file = "ruff-0.3.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5da894a29ec018a8293d3d17c797e73b374773943e8369cfc50495573d396933"},
{file = "ruff-0.3.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:755c22536d7f1889be25f2baf6fedd019d0c51d079e8417d4441159f3bcd30c2"},
{file = "ruff-0.3.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:dd73fe7f4c28d317855da6a7bc4aa29a1500320818dd8f27df95f70a01b8171f"},
{file = "ruff-0.3.0-py3-none-win32.whl", hash = "sha256:19eacceb4c9406f6c41af806418a26fdb23120dfe53583df76d1401c92b7c14b"},
{file = "ruff-0.3.0-py3-none-win_amd64.whl", hash = "sha256:128265876c1d703e5f5e5a4543bd8be47c73a9ba223fd3989d4aa87dd06f312f"},
{file = "ruff-0.3.0-py3-none-win_arm64.whl", hash = "sha256:e3a4a6d46aef0a84b74fcd201a4401ea9a6cd85614f6a9435f2d33dd8cefbf83"},
{file = "ruff-0.3.0.tar.gz", hash = "sha256:0886184ba2618d815067cf43e005388967b67ab9c80df52b32ec1152ab49f53a"},
]
[[package]]
@@ -3619,4 +3626,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"]
[metadata]
lock-version = "2.0"
python-versions = ">=3.10,<3.12"
content-hash = "c982d5c5fee76ca102d823010a538f287ac98583f330ebee3c0775c5f42f117d"
content-hash = "c947090d326e81179054b7ce4dded311df8b7ca5a56680d5e9459cf8ca18df1a"

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
version = "1.97.0"
version = "1.98.1"
description = ""
authors = ["Hau Tran <alex.tran1502@gmail.com>"]
readme = "README.md"
@@ -45,7 +45,7 @@ onnxruntime = "^1.15.0"
optional = true
[tool.poetry.group.cuda.dependencies]
onnxruntime-gpu = "^1.15.0"
onnxruntime-gpu = {version = "^1.17.0", source = "cuda12"}
[tool.poetry.group.openvino]
optional = true
@@ -59,6 +59,11 @@ optional = true
[tool.poetry.group.armnn.dependencies]
onnxruntime = "^1.15.0"
[[tool.poetry.source]]
name = "cuda12"
url = "https://aiinfra.pkgs.visualstudio.com/PublicPackages/_packaging/onnxruntime-cuda-12/pypi/simple/"
priority = "explicit"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"

View File

@@ -62,8 +62,12 @@ fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
npm --prefix server version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP"
make open-api
npm --prefix open-api/typescript-sdk version "$SERVER_PUMP"
npm --prefix web version "$SERVER_PUMP"
npm --prefix web i --package-lock-only
npm --prefix cli i --package-lock-only
npm --prefix e2e i --package-lock-only
poetry --directory machine-learning version "$SERVER_PUMP"
fi

View File

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

View File

@@ -5,17 +5,17 @@
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000271">
<testcase classname="fastlane.lanes" name="0: default_platform" time="0.000266">
</testcase>
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="74.334294">
<testcase classname="fastlane.lanes" name="1: bundleRelease" time="81.342186">
</testcase>
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="29.507669">
<testcase classname="fastlane.lanes" name="2: upload_to_play_store" time="48.746195">
</testcase>

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETALLS",
"exif_bottom_sheet_location": "UBICACIÓ",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "LOKALITA",
"exif_bottom_sheet_location_add": "Přidat polohu",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Zpracovávám",
"experimental_settings_new_asset_list_title": "Povolení experimentální mřížky fotografií",
"experimental_settings_subtitle": "Používejte na vlastní riziko!",

View File

@@ -35,8 +35,8 @@
"app_bar_signout_dialog_title": "Log ud",
"archive_page_no_archived_assets": "Ingen arkiverede elementer blev fundet",
"archive_page_title": "Arkivér ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_action_delete_err_read_only": "Kan ikke slette kun læselige elementer. Springer over",
"asset_action_share_err_offline": "Kan ikke hente offline element(er). Springer over",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout",
"asset_list_layout_settings_group_automatically": "Automatisk",
"asset_list_layout_settings_group_by": "Gruppér elementer pr. ",
@@ -150,7 +150,7 @@
"control_bottom_app_bar_share": "Del",
"control_bottom_app_bar_share_to": "Del til",
"control_bottom_app_bar_stack": "Stak",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Flyt til papirkurv",
"control_bottom_app_bar_unarchive": "Afakivér",
"control_bottom_app_bar_unfavorite": "Fjern favorit",
"control_bottom_app_bar_upload": "Upload",
@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "LOKATION",
"exif_bottom_sheet_location_add": "Tilføj en placering",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Under udarbejdelse",
"experimental_settings_new_asset_list_title": "Aktiver eksperimentelt fotogitter",
"experimental_settings_subtitle": "Brug på eget ansvar!",
@@ -199,7 +200,7 @@
"home_page_archive_err_partner": "Kan endnu ikke arkivere partners elementer. Springer over",
"home_page_building_timeline": "Bygger tidslinjen",
"home_page_delete_err_partner": "Kan endnu ikke slette partners elementer. Springer over",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_remote_err_local": "Lokale elementer i fjernsletningssektion. Springer over",
"home_page_favorite_err_local": "Kan endnu ikke gøre lokale elementer til favoritter. Springer over..",
"home_page_favorite_err_partner": "Kan endnu ikke tilføje partners elementer som favoritter. Springer over",
"home_page_first_time_notice": "Hvis det er din første gang i appen, bedes du vælge en sikkerhedskopi af albummer så tidlinjen kan blive fyldt med billeder og videoer fra albummerne.",
@@ -279,8 +280,8 @@
"map_zoom_to_see_photos": "Zoom ud for at vise billeder",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bevægelsesbilleder",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke redigere datoen på kun læselige elementer. Springer over",
"multiselect_grid_edit_gps_err_read_only": "Kan ikke redigere lokation af kun læselige elementer. Springer over",
"notification_permission_dialog_cancel": "Annuller",
"notification_permission_dialog_content": "Gå til indstillinger for at slå notifikationer til.",
"notification_permission_dialog_settings": "Indstillinger",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "STANDORT",
"exif_bottom_sheet_location_add": "Aufnahmeort hinzufügen",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "In Arbeit",
"experimental_settings_new_asset_list_title": "Experimentelles Fotogitter aktivieren",
"experimental_settings_subtitle": "Benutzung auf eigene Gefahr!",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",

View File

@@ -1,6 +1,6 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"action_common_cancel": "Cancelar",
"action_common_update": "Actualizar",
"add_to_album_bottom_sheet_added": "Agregado a {album}",
"add_to_album_bottom_sheet_already_exists": "Ya se encuentra en {album}",
"advanced_settings_log_level_title": "Nivel de log: {}",
@@ -35,7 +35,7 @@
"app_bar_signout_dialog_title": "Cerrar sesión",
"archive_page_no_archived_assets": "No se encontraron recursos archivados",
"archive_page_title": "Archivo ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_delete_err_read_only": "No se pueden borrar los archivos de solo lectura. Saltando.",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Diseño dinámico",
"asset_list_layout_settings_group_automatically": "Automatico",
@@ -142,15 +142,15 @@
"control_bottom_app_bar_archive": "Archivar",
"control_bottom_app_bar_create_new_album": "Crear nuevo álbum",
"control_bottom_app_bar_delete": "Eliminar",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_delete_from_immich": "Borrar de Immich",
"control_bottom_app_bar_delete_from_local": "Borrar del dispositivo",
"control_bottom_app_bar_edit_location": "Editar ubicación",
"control_bottom_app_bar_edit_time": "Editar fecha y hora",
"control_bottom_app_bar_favorite": "Favorito",
"control_bottom_app_bar_share": "Compartir",
"control_bottom_app_bar_share_to": "Enviar",
"control_bottom_app_bar_stack": "Apilar",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Mover a la papelera",
"control_bottom_app_bar_unarchive": "Desarchivar",
"control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Subir",
@@ -165,9 +165,9 @@
"daily_title_text_date_year": "E dd de MMM, yyyy",
"date_format": "E d, LLL y • h:mm a",
"delete_dialog_alert": "Estos elementos serán eliminados permanentemente de Immich y de tu dispositivo",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Estas imágenes van a ser borradas de tu dispositivo, pero seguirán disponibles en el servidor Immich",
"delete_dialog_alert_local_non_backed_up": "Algunas de las imágenes no tienen copia de seguridad y serán borradas de forma permanente de tu dispositivo",
"delete_dialog_alert_remote": "Estas imágenes van a ser borradas de forma permanente del servidor Immich",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Eliminar",
"delete_dialog_ok_force": "Delete Anyway",
@@ -178,13 +178,14 @@
"delete_shared_link_dialog_title": "Eliminar enlace compartido",
"description_input_hint_text": "Agregar descripción...",
"description_input_submit_error": "Error al actualizar la descripción, verifica el registro para obtener más detalles",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Location",
"edit_date_time_dialog_date_time": "Fecha y Hora",
"edit_date_time_dialog_timezone": "Zona horaria",
"edit_location_dialog_title": "Ubicación",
"exif_bottom_sheet_description": "Agregar Descripción...",
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "UBICACIÓN",
"exif_bottom_sheet_location_add": "Añadir ubicación",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",
@@ -220,13 +221,13 @@
"library_page_sort_most_oldest_photo": "Foto más antigua",
"library_page_sort_most_recent_photo": "Foto más reciente",
"library_page_sort_title": "Título del álbum",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Enter a valid latitude",
"location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude": "Longitude",
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"location_picker_choose_on_map": "Elegir en el mapa",
"location_picker_latitude": "Latitud",
"location_picker_latitude_error": "Introduce una latitud válida",
"location_picker_latitude_hint": "Introduce tu latitud aquí",
"location_picker_longitude": "Longitud",
"location_picker_longitude_error": "Introduce una longitud válida",
"location_picker_longitude_hint": "Introduce tu longitud aquí",
"login_disabled": "El inicio de sesión ha sido desactivado",
"login_form_api_exception": "Excepción producida por API. Por favor, verifica el URL del servidor e inténtalo de nuevo.",
"login_form_back_button_text": "Atrás",
@@ -252,12 +253,12 @@
"login_form_server_error": "No se pudo conectar al servidor.",
"login_password_changed_error": "Hubo un error actualizando la contraseña",
"login_password_changed_success": "Contraseña cambiado con éxito",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_assets_in_bound": "{} foto",
"map_assets_in_bounds": "{} fotos",
"map_cannot_get_user_location": "No se pudo obtener la posición del usuario",
"map_location_dialog_cancel": "Cancelar",
"map_location_dialog_yes": "Sí",
"map_location_picker_page_use_location": "Use this location",
"map_location_picker_page_use_location": "Usar esta ubicación",
"map_location_service_disabled_content": "Los servicios de ubicación deben estar activados para mostrar elementos de tu ubicación actual. Deseas activarlos ahora?",
"map_location_service_disabled_title": "Servicios de ubicación desactivados",
"map_no_assets_in_bounds": "No hay fotos en esta zona",
@@ -265,22 +266,22 @@
"map_no_location_permission_title": "Permisos de ubicación denegados",
"map_settings_dark_mode": "Modo oscuro",
"map_settings_date_range_option_all": "Todo",
"map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_date_range_option_day": "Últimas 24 horas",
"map_settings_date_range_option_days": "Últimos {} días",
"map_settings_date_range_option_year": "Último año",
"map_settings_date_range_option_years": "Últimos {} años",
"map_settings_dialog_cancel": "Cancelar",
"map_settings_dialog_save": "Guardar",
"map_settings_dialog_title": "Ajustes mapa",
"map_settings_include_show_archived": "Incluir archivados",
"map_settings_only_relative_range": "Rango de fechas",
"map_settings_only_show_favorites": "Mostrar solo favoritas",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Apariencia del Mapa",
"map_zoom_to_see_photos": "Alejar para ver fotos",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Foto en Movimiento",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha de archivos de solo lectura. Saltando.",
"multiselect_grid_edit_gps_err_read_only": "No se puede cambiar la localización de archivos de solo lectura. Saltando.",
"notification_permission_dialog_cancel": "Cancelar",
"notification_permission_dialog_content": "Para activar las notificaciones, ve a Configuración y selecciona permitir.",
"notification_permission_dialog_settings": "Ajustes",
@@ -318,7 +319,7 @@
"profile_drawer_sign_out": "Cerrar Sesión",
"profile_drawer_trash": "Papelera",
"recently_added_page_title": "Recién Agregadas",
"scaffold_body_error_occurred": "Error occurred",
"scaffold_body_error_occurred": "Ha ocurrido un error",
"search_bar_hint": "Busca tus fotos",
"search_page_categories": "Categorías",
"search_page_favorites": "Favoritos",
@@ -330,9 +331,9 @@
"search_page_person_add_name_dialog_hint": "Nombre",
"search_page_person_add_name_dialog_save": "Guardar",
"search_page_person_add_name_dialog_title": "Añadir nombre",
"search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name",
"search_page_person_add_name_subtitle": "Encuéntralos rápido buscando por nombre",
"search_page_person_add_name_title": "Añadir nombre",
"search_page_person_edit_name": "Cambiar nombre",
"search_page_places": "Lugares",
"search_page_recently_added": "Recién agregadas",
"search_page_screenshots": "Capturas de pantalla",
@@ -341,7 +342,7 @@
"search_page_videos": "Videos",
"search_page_view_all_button": "Ver todo",
"search_page_your_activity": "Tu actividad",
"search_page_your_map": "Your Map",
"search_page_your_map": "Tu Mapa",
"search_result_page_new_search_hint": "Nueva Busqueda",
"search_suggestion_list_smart_search_hint_1": "La búsqueda inteligente está habilitada por defecto, para buscar metadatos utiliza esta sintaxis ",
"search_suggestion_list_smart_search_hint_2": "m:tu-término-de-búsqueda",
@@ -381,17 +382,17 @@
"shared_album_activity_remove_title": "Eliminar Actividad",
"shared_album_activity_setting_subtitle": "Permitir que otros respondan",
"shared_album_activity_setting_title": "Comentarios y me gusta",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"shared_album_section_people_action_error": "Error dejando/eliminando del album",
"shared_album_section_people_action_leave": "Eliminar usuario del album",
"shared_album_section_people_action_remove_user": "Eliminar usuario del album",
"shared_album_section_people_owner_label": "Propietario",
"shared_album_section_people_title": "GENTE",
"share_dialog_preparing": "Preparando...",
"shared_link_app_bar_title": "Enlaces compartidos",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_clipboard_copied_massage": "Copiado al portapapeles",
"shared_link_clipboard_text": "Enlace: {}\nContraseña: {}",
"shared_link_create_app_bar_title": "Crear enlace compartido",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_error": "Error creando el enlace compartido",
"shared_link_create_info": "Cualquier persona con el enlace puede ver las fotos seleccionadas",
"shared_link_create_submit_button": "Crear enlace",
"shared_link_edit_allow_download": "Permitir descargar a usuarios públicos",
@@ -401,32 +402,32 @@
"shared_link_edit_description": "Descripción",
"shared_link_edit_description_hint": "Introduce la descripción del enlace",
"shared_link_edit_expire_after": "Expirar después de",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_expire_after_option_day": "1 día",
"shared_link_edit_expire_after_option_days": "{} días",
"shared_link_edit_expire_after_option_hour": "1 hora",
"shared_link_edit_expire_after_option_hours": "{} horas",
"shared_link_edit_expire_after_option_minute": "1 minuto",
"shared_link_edit_expire_after_option_minutes": "{} minutos",
"shared_link_edit_expire_after_option_never": "Nunca",
"shared_link_edit_password": "Contraseña",
"shared_link_edit_password_hint": "Introduce la contraseña del enlace",
"shared_link_edit_show_meta": "Mostrar metadatos",
"shared_link_edit_submit_button": "Actualizar enlace",
"shared_link_empty": "No tienes enlaces compartidos",
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Expired",
"shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute",
"shared_link_error_server_url_fetch": "No se puede adquirir la URL del servidor",
"shared_link_expired": "Caducado",
"shared_link_expires_day": "Caduca en {} día",
"shared_link_expires_days": "Caduca en {} días",
"shared_link_expires_hour": "Caduca en {} hora",
"shared_link_expires_hours": "Caduca en {} horas",
"shared_link_expires_minute": "Caduca en {} minuto",
"shared_link_expires_minutes": "Caduca en {} minutos",
"shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_never": "Caduca ∞",
"shared_link_expires_second": "Caduca en {} segundo",
"shared_link_expires_seconds": "Caduca en {} segundos",
"shared_link_info_chip_download": "Download",
"shared_link_info_chip_download": "Descargar",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload",
"shared_link_info_chip_upload": "Subir",
"shared_link_manage_links": "Administrar enlaces compartidos",
"share_done": "Hecho",
"share_invite": "Invitar al álbum",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "UBICACIÓN",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETALLES",
"exif_bottom_sheet_location": "UBICACIÓN",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Trabajo en progreso",
"experimental_settings_new_asset_list_title": "Habilitar cuadrícula fotográfica experimental",
"experimental_settings_subtitle": "Úsalo bajo tu responsabilidad",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "TIEDOT",
"exif_bottom_sheet_location": "SIJAINTI",
"exif_bottom_sheet_location_add": "Lisää sijainti",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Työn alla",
"experimental_settings_new_asset_list_title": "Ota käyttöön kokeellinen kuvaruudukko",
"experimental_settings_subtitle": "Käyttö omalla vastuulla!",
@@ -199,7 +200,7 @@
"home_page_archive_err_partner": "Kumppanin kohteita ei voi arkistoida. Hypätään yli",
"home_page_building_timeline": "Rakennetaan aikajanaa",
"home_page_delete_err_partner": "Kumppanin kohteita ei voi poistaa.Hypätään yli",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_remote_err_local": "Paikallisia kohteita etäkohdevalintojen joukossa, ohitetaan",
"home_page_favorite_err_local": "Paikallisten kohteiden lisääminen suosikkeihin ei ole mahdollista, ohitetaan",
"home_page_favorite_err_partner": "Kumppanin kohteita ei voi vielä merkitä suosikiksi. Hypätään yli",
"home_page_first_time_notice": "Jos käytät sovellusta ensimmäistä kertaa, muista valita varmuuskopioitavat albumi(t), jotta aikajanalla voi olla kuvia ja videoita.",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DÉTAILS",
"exif_bottom_sheet_location": "LOCALISATION",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "En cours de développement",
"experimental_settings_new_asset_list_title": "Activer la grille de photos expérimentale",
"experimental_settings_subtitle": "Utilisez à vos dépends !",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",

View File

@@ -1,74 +1,74 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"action_common_cancel": "Mégsem",
"action_common_update": "Frissít",
"add_to_album_bottom_sheet_added": "Hozzáadva a(z) {album} nevű albumhoz",
"add_to_album_bottom_sheet_already_exists": "Már eleme a(z) {album} nevű albumnak",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
"advanced_settings_log_level_title": "Naplózás szintje: {}",
"advanced_settings_prefer_remote_subtitle": "Néhány eszköz fájdalmasan lassan tölti be az eszközön lévő elemeket. Ezzel a beállítással inkább a távoli képeket töltjük be helyette.",
"advanced_settings_prefer_remote_title": "Távoli képek preferálása",
"advanced_settings_self_signed_ssl_subtitle": "SSL tanúsítvány ellenőrzésének kihagyása a szerver végponthoz. Ehhez saját aláírt tanúsítványok szükségesek.",
"advanced_settings_self_signed_ssl_title": "Saját aláírt SSL tanúsítványok engedélyezése",
"advanced_settings_tile_subtitle": "Haladó felhasználói beállítások",
"advanced_settings_tile_title": "Haladó",
"advanced_settings_troubleshooting_subtitle": "További funkciók engedélyezése hibaelhárítás céljából",
"advanced_settings_troubleshooting_title": "Hibaelhárítás",
"album_info_card_backup_album_excluded": "EXCLUDED",
"album_info_card_backup_album_included": "INCLUDED",
"album_info_card_backup_album_excluded": "KIZÁRVA",
"album_info_card_backup_album_included": "BELEÉRTVE",
"album_thumbnail_card_item": "1 elem",
"album_thumbnail_card_items": "{} elem",
"album_thumbnail_card_shared": "· Megosztott",
"album_thumbnail_owned": "Tulajdonos",
"album_thumbnail_shared_by": "Megosztotta: {}",
"album_viewer_appbar_share_delete": "Album törlése",
"album_viewer_appbar_share_err_delete": "Hiba az album törlése közben",
"album_viewer_appbar_share_err_leave": "Hiba az albumból való kilépés közben",
"album_viewer_appbar_share_err_remove": "Hiba az elemek törlése közben",
"album_viewer_appbar_share_err_title": "Hiba az album átnevezése közben",
"album_viewer_appbar_share_err_delete": "Nem sikerült törölni az albumot",
"album_viewer_appbar_share_err_leave": "Nem sikerült kilépni az albumból",
"album_viewer_appbar_share_err_remove": "Néhány elemet nem sikerült törölni az albumból",
"album_viewer_appbar_share_err_title": "Nem sikerült átnevezni az albumot",
"album_viewer_appbar_share_leave": "Kilépés az albumból",
"album_viewer_appbar_share_remove": "Törlés az albumból",
"album_viewer_appbar_share_to": "Share To",
"album_viewer_appbar_share_remove": "Eltávolítás az albumból",
"album_viewer_appbar_share_to": "Megosztás Ide",
"album_viewer_page_share_add_users": "Felhasználók hozzáadása",
"all_people_page_title": "Emberek",
"all_videos_page_title": "Videók",
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"archive_page_no_archived_assets": "Nem található archivált média",
"app_bar_signout_dialog_content": "Biztos, hogy ki szeretnél jelentkezni?",
"app_bar_signout_dialog_ok": "Igen",
"app_bar_signout_dialog_title": "Kijelentkezés",
"archive_page_no_archived_assets": "Nem található archivált elem",
"archive_page_title": "Archívum ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_action_delete_err_read_only": "Nem sikerült törölni a csak-olvasható elem(ek)et, így ezeket átugorjuk",
"asset_action_share_err_offline": "Nem sikerült betölteni az offline elem(ek)et, így ezeket kihagyjuk",
"asset_list_layout_settings_dynamic_layout_title": "Dinamikus elrendezés",
"asset_list_layout_settings_group_automatically": "Automatikus",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Hónap",
"asset_list_layout_settings_group_by_month_day": "Hónap + nap",
"asset_list_settings_subtitle": "Photo grid layout settings",
"asset_list_settings_title": "Photo Grid",
"backup_album_selection_page_albums_device": "Az eszközön lévő albumok ({})",
"backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude",
"backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.",
"backup_album_selection_page_select_albums": "Albumok kiválasztása",
"backup_album_selection_page_selection_info": "Selection Info",
"asset_list_layout_settings_group_by": "Elemek csoportosítása",
"asset_list_layout_settings_group_by_month": "hónapok szerint",
"asset_list_layout_settings_group_by_month_day": "hónap és nap szerint",
"asset_list_settings_subtitle": "Fotórács elrendezése",
"asset_list_settings_title": "Fotórács",
"backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})",
"backup_album_selection_page_albums_tap": "Koppincs a hozzáadáshoz, duplán koppincs az eltávolításhoz",
"backup_album_selection_page_assets_scatter": "Egy elem több albumban is lehet. Ezért a mentéshez albumokat lehet hozzáadni vagy azokat a mentésből kihagyni.",
"backup_album_selection_page_select_albums": "Válassz albumokat",
"backup_album_selection_page_selection_info": "Összegzés",
"backup_album_selection_page_total_assets": "Összes egyedi elem",
"backup_all": "Összes",
"backup_background_service_backup_failed_message": "HIba a mentés közben. Újrapróbálkozás...",
"backup_background_service_connection_failed_message": "HIba a szerverhez való csatlakozás közben. Újrapróbálkozás...",
"backup_background_service_current_upload_notification": "Feltöltés {}",
"backup_background_service_default_notification": "Keresés új elemek után...",
"backup_background_service_default_notification": "Új elemek keresése...",
"backup_background_service_error_title": "Hiba mentés közben",
"backup_background_service_in_progress_notification": "Elemek mentés alatt..",
"backup_background_service_upload_failure_notification": "Hiba feltöltés közben {}",
"backup_controller_page_albums": "Albumok mentése",
"backup_controller_page_background_app_refresh_disabled_content": "Enable background app refresh in Settings > General > Background App Refresh in order to use background backup.",
"backup_controller_page_background_app_refresh_disabled_title": "Background app refresh disabled",
"backup_controller_page_albums": "Albumok Mentése",
"backup_controller_page_background_app_refresh_disabled_content": "Engedélyezd a háttérben történő frissítést a Beállítások > Általános > Háttérben Frissítés menüpontban.",
"backup_controller_page_background_app_refresh_disabled_title": "Háttérben frissítés kikapcsolva",
"backup_controller_page_background_app_refresh_enable_button_text": "Beállítások megnyitása",
"backup_controller_page_background_battery_info_link": "Mutasd meg hogyan",
"backup_controller_page_background_battery_info_message": "For the best background backup experience, please disable any battery optimizations restricting background activity for Immich.\n\nSince this is device-specific, please lookup the required information for your device manufacturer.",
"backup_controller_page_background_battery_info_message": "A sikeres háttérben történő mentéshez kérjük, tiltsd le az Immich akkumulátor optimalizálását.\n\nMivel ezt a különféle eszközökön máshogy kell, ezért kérjük, az eszközöd gyártójától tudd meg, hogyan kell.",
"backup_controller_page_background_battery_info_ok": "OK",
"backup_controller_page_background_battery_info_title": "Akkumulátoroptimalizálás",
"backup_controller_page_background_battery_info_title": "Akkumulátor optimalizálás",
"backup_controller_page_background_charging": "Csak töltés közben",
"backup_controller_page_background_configure_error": "Failed to configure the background service",
"backup_controller_page_background_delay": "Delay new assets backup: {}",
"backup_controller_page_background_configure_error": "Nem sikerült beállítani a háttér szolgáltatást",
"backup_controller_page_background_delay": "Új elemek mentésének késleltetése: {}",
"backup_controller_page_background_description": "Kapcsold be a háttérfolyamatot, hogy automatikusan mentsen elemeket az applikáció megnyitása nélkül",
"backup_controller_page_background_is_off": "Automatikus mentés a háttérben ki van kapcsolva",
"backup_controller_page_background_is_on": "Automatikus mentés a háttérben bekapcsolva",
@@ -78,63 +78,63 @@
"backup_controller_page_backup": "Mentés",
"backup_controller_page_backup_selected": "Kiválasztva:",
"backup_controller_page_backup_sub": "Mentett fotók és videók",
"backup_controller_page_cancel": "Megszakít",
"backup_controller_page_cancel": "Mégsem",
"backup_controller_page_created": "Létrehozva: {}",
"backup_controller_page_desc_backup": "Turn on foreground backup to automatically upload new assets to the server when opening the app.",
"backup_controller_page_desc_backup": "Ha engedélyezed az előtérben mentést, akkor az új elemek automatikusan feltöltődnek a szerverre, amikor megyitod az alkalmazást.",
"backup_controller_page_excluded": "Kivéve:",
"backup_controller_page_failed": "Sikertelen ({})",
"backup_controller_page_filename": "Fájlnév: {}[{}]",
"backup_controller_page_id": "Azonosító: {}",
"backup_controller_page_info": "Mentésinformációk",
"backup_controller_page_none_selected": "Egy sincs kiválasztva",
"backup_controller_page_remainder": "Maradék",
"backup_controller_page_remainder": "Hátralévő",
"backup_controller_page_remainder_sub": "Hátralévő fotók és videók a kijelöltek közül",
"backup_controller_page_select": "Kiválaszt",
"backup_controller_page_server_storage": "Szerver tárhely",
"backup_controller_page_start_backup": "Mentés elindítása",
"backup_controller_page_status_off": "Autoatikus mentés az előtérben kikapcsolva",
"backup_controller_page_status_on": "Autoatikus mentés az előtérben bekapcsolva",
"backup_controller_page_server_storage": "Szerver Tárhely",
"backup_controller_page_start_backup": "Mentés Elindítása",
"backup_controller_page_status_off": "Automatikus mentés az előtérben kikapcsolva",
"backup_controller_page_status_on": "Automatikus mentés az előtérben bekapcsolva",
"backup_controller_page_storage_format": "{} / {} felhasználva",
"backup_controller_page_to_backup": "Albumok amiket mentesz",
"backup_controller_page_total": "Összes",
"backup_controller_page_to_backup": "Mentésre kijelölt albumok",
"backup_controller_page_total": "Összesen",
"backup_controller_page_total_sub": "Minden egyedi fotó és videó a kijelölt albumokból",
"backup_controller_page_turn_off": "Turn off foreground backup",
"backup_controller_page_turn_on": "Turn on foreground backup",
"backup_controller_page_uploading_file_info": "Uploading file info",
"backup_controller_page_turn_off": "Előtérben mentés kikapcsolása",
"backup_controller_page_turn_on": "Előtérben mentés bekapcsolása",
"backup_controller_page_uploading_file_info": "Fájl információk feltöltése",
"backup_err_only_album": "Az utolsó albumot nem tudod törölni",
"backup_info_card_assets": "elemek",
"backup_manual_cancelled": "Megszakítva",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_failed": "Sikertelen",
"backup_manual_in_progress": "Feltöltés már folyamatban. Próbáld meg később",
"backup_manual_success": "Sikeres",
"backup_manual_title": "Upload status",
"backup_manual_title": "Feltöltés állapota",
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
"cache_settings_clear_cache_button": "Gyorsítótár törlése",
"cache_settings_clear_cache_button": "Gyorsítótár kiürítése",
"cache_settings_clear_cache_button_title": "Clears the app's cache. This will significantly impact the app's performance until the cache has rebuilt.",
"cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_image_cache_size": "Image cache size ({} assets)",
"cache_settings_statistics_album": "Library thumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
"cache_settings_statistics_full": "Teljes képek",
"cache_settings_statistics_shared": "Shared album thumbnails",
"cache_settings_statistics_thumbnail": "Előnézeti képek",
"cache_settings_statistics_title": "Gyorsítótár által használt terület",
"cache_settings_subtitle": "Control the caching behaviour of the Immich mobile application",
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
"cache_settings_tile_subtitle": "Control the local storage behaviour",
"cache_settings_tile_title": "Local Storage",
"cache_settings_title": "Gyorsítótár beállítások",
"cache_settings_duplicated_assets_clear_button": "KIÜRÍT",
"cache_settings_duplicated_assets_subtitle": "Fotók és videók, amiket az alkalmazás fekete listára tett",
"cache_settings_duplicated_assets_title": "Duplikált Elemek ({})",
"cache_settings_image_cache_size": "Kép gyorsítótár mérete ({} elem)",
"cache_settings_statistics_album": "Mappa bélyegképei",
"cache_settings_statistics_assets": "{} elem ({})",
"cache_settings_statistics_full": "Teljes méretű képek",
"cache_settings_statistics_shared": "Megosztott album bélyegképei",
"cache_settings_statistics_thumbnail": "Bélyegképek",
"cache_settings_statistics_title": "Gyorsítótár használata",
"cache_settings_subtitle": "Az Immich mobilalkalmazás gyorsítótár viselkedésének beállítása",
"cache_settings_thumbnail_size": "Bélyegkép gyorsítótár mérete ({} elem)",
"cache_settings_tile_subtitle": "Helyi tárhely viselkedésének beállítása",
"cache_settings_tile_title": "Helyi Tárhely",
"cache_settings_title": "Gyorsítótár Beállítások",
"change_password_form_confirm_password": "Jelszó Megerősítése",
"change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséfes a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
"change_password_form_description": "Kedves {name}!\n\nMost jelentkezel be először a rendszerbe vagy más okból szükséges a jelszavad meváltoztatása. Kérjük, add meg új jelszavad.",
"change_password_form_new_password": "Új Jelszó",
"change_password_form_password_mismatch": "A két beírt jelszó nem egyezik",
"change_password_form_reenter_new_password": "Jelszó (még egyszer)",
"common_add_to_album": "Albumhoz ad",
"common_change_password": "Jelszócsere",
"common_create_new_album": "Új album létrehozása",
"common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az app és a szerver kompatibilis verziójú legyen.",
"common_server_error": "Kérjük, ellenőrizd a hálózati kapcsolatot, gondoskodj róla, hogy a szerver elérhető legyen, valamint az alkalmazás és a szerver kompatibilis verziójú legyen.",
"common_shared": "Megosztva",
"control_bottom_app_bar_add_to_album": "Hozzáadás az albumhoz",
"control_bottom_app_bar_album_info": "{} elem",
@@ -142,23 +142,23 @@
"control_bottom_app_bar_archive": "Archivál",
"control_bottom_app_bar_create_new_album": "Album létrehozása",
"control_bottom_app_bar_delete": "Törlés",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_delete_from_immich": "Törlés az Immich-ből",
"control_bottom_app_bar_delete_from_local": "Törlés az eszközről",
"control_bottom_app_bar_edit_location": "Hely Módosítása",
"control_bottom_app_bar_edit_time": "Dátum és Idő Módosítása",
"control_bottom_app_bar_favorite": "Kedvenc",
"control_bottom_app_bar_share": "Megosztás",
"control_bottom_app_bar_share_to": "Share To",
"control_bottom_app_bar_share_to": "Megosztás Ide",
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_unarchive": "Archiválás megszüntetése",
"control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Upload",
"control_bottom_app_bar_trash_from_immich": "Lomtárba Helyez",
"control_bottom_app_bar_unarchive": "Nem Archivált",
"control_bottom_app_bar_unfavorite": "Nem Kedvenc",
"control_bottom_app_bar_upload": "Feltöltés",
"create_album_page_untitled": "Névtelen",
"create_shared_album_page_create": "Létrehoz",
"create_shared_album_page_share": "Megosztás",
"create_shared_album_page_share_add_assets": "ELEMEK HOZZÁADÁSA",
"create_shared_album_page_share_select_photos": "Fotók kiválasztása",
"create_shared_album_page_share_select_photos": "Fotók választása",
"curated_location_page_title": "Helyek",
"curated_object_page_title": "Dolgok",
"daily_title_text_date": "E, MMM dd",
@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "RÉSZLETEK",
"exif_bottom_sheet_location": "HELYSZÍN",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Fejlesztés alatt",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Csak saját felelősségre használd",
@@ -230,10 +231,10 @@
"login_disabled": "A bejelentkezés letiltva",
"login_form_api_exception": "API hiba. Kérljük, ellenőrid a szerver címét, majd próbáld újra.",
"login_form_back_button_text": "Back",
"login_form_button_text": "Belépés",
"login_form_email_hint": "teemailed@email.com",
"login_form_button_text": "Bejelentkezés",
"login_form_email_hint": "email@cimed.hu",
"login_form_endpoint_hint": "http://szerver-címe:port/api",
"login_form_endpoint_url": "Kiszolgáló végpont címe",
"login_form_endpoint_url": "Szerver címe",
"login_form_err_http": "Kérem, adjon meg egy http:// vagy https:// címet",
"login_form_err_invalid_email": "Érvénytelen email cím",
"login_form_err_invalid_url": "Érvénytelen cím",
@@ -346,7 +347,7 @@
"search_suggestion_list_smart_search_hint_1": "Az intelligens keresés alapértelmezetten be van kapcsolva, metaadatokat így kereshetsz",
"search_suggestion_list_smart_search_hint_2": "m:keresési-kifejezés",
"select_additional_user_for_sharing_page_suggestions": "Javaslatok",
"select_user_for_sharing_page_err_album": "Hiba az album létrehozása közben",
"select_user_for_sharing_page_err_album": "Nem sikerült létrehozni az albumot",
"select_user_for_sharing_page_share_suggestions": "Javaslatok",
"server_info_box_app_version": "Alkalmazás Verzió",
"server_info_box_latest_release": "Latest Version",
@@ -373,7 +374,7 @@
"settings_require_restart": "Kérlek indítsd újra az Immich-et hogy alkalmazd ezt a beállítást",
"share_add": "Hozzáadás",
"share_add_photos": "Fotók hozzáadása",
"share_add_title": "Cím hozzáadása",
"share_add_title": "Album neve",
"share_create_album": "Album létrehozása",
"shared_album_activities_input_disable": "Comment is disabled",
"shared_album_activities_input_hint": "Say something",
@@ -431,11 +432,11 @@
"share_done": "Done",
"share_invite": "Meghívás az albumba",
"sharing_page_album": "Megosztott albumok",
"sharing_page_description": "Hozzon létre megosztott albumokat, hogy megoszthasson fényképeket és videókat a hálózatában lévő emberekkel.",
"sharing_page_description": "Megosztott albumok létrehozásával fényképeket és videókatoszthatsz meg a hálózatodban lévő emberekkel.",
"sharing_page_empty_list": "ÜRES LISTA",
"sharing_silver_appbar_create_shared_album": "Megosztott album létrehozása",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Megosztás másokkal",
"sharing_silver_appbar_share_partner": "Megosztás partnerrel",
"tab_controller_nav_library": "Könyvtár",
"tab_controller_nav_photos": "Képek",
"tab_controller_nav_search": "Keresés",

View File

@@ -1,13 +1,13 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"action_common_cancel": "Annulla",
"action_common_update": "Aggiorna",
"add_to_album_bottom_sheet_added": "Aggiunto in {album}",
"add_to_album_bottom_sheet_already_exists": "Già presente in {album}",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_log_level_title": "Livello log: {}",
"advanced_settings_prefer_remote_subtitle": "Alcuni dispositivi sono molto lenti a caricare le anteprime delle immagini dal dispositivo. Attivare questa impostazione per caricare invece le immagini remote.",
"advanced_settings_prefer_remote_title": "Preferisci immagini remote.",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
"advanced_settings_self_signed_ssl_subtitle": "Salta la verifica dei certificati SSL del server. Richiesto con l'uso di certificati self-signed.",
"advanced_settings_self_signed_ssl_title": "Consenti certificati SSL self-signed",
"advanced_settings_tile_subtitle": "Impostazioni aggiuntive utenti",
"advanced_settings_tile_title": "Avanzato",
"advanced_settings_troubleshooting_subtitle": "Attiva funzioni addizionali per la risoluzione dei problemi",
@@ -35,7 +35,7 @@
"app_bar_signout_dialog_title": "Disconnetti",
"archive_page_no_archived_assets": "Nessuna oggetto archiviato",
"archive_page_title": "Archivia ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_delete_err_read_only": "Non posso eliminare degli elementi in sola lettura, ignorato",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Layout dinamico",
"asset_list_layout_settings_group_automatically": "Automatico",
@@ -111,9 +111,9 @@
"cache_settings_album_thumbnails": "Anteprime pagine librerie ({} assets)",
"cache_settings_clear_cache_button": "Cancella cache",
"cache_settings_clear_cache_button_title": "Cancella la cache dell'app. Questo impatterà significativamente le prestazioni dell''app fino a quando la cache non sarà rigenerata.",
"cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_duplicated_assets_clear_button": "ELIMINA",
"cache_settings_duplicated_assets_subtitle": "Foto e video che sono nella black list dell'applicazione",
"cache_settings_duplicated_assets_title": "Elementi duplicati ({})",
"cache_settings_image_cache_size": "Dimensione cache delle foto ({} assets)",
"cache_settings_statistics_album": "Anteprime librerie",
"cache_settings_statistics_assets": "{} contenuti ({})",
@@ -142,15 +142,15 @@
"control_bottom_app_bar_archive": "Archivia",
"control_bottom_app_bar_create_new_album": "Crea nuovo album",
"control_bottom_app_bar_delete": "Elimina",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_delete_from_immich": "Elimina da Immich",
"control_bottom_app_bar_delete_from_local": "Elimina dal dispositivo",
"control_bottom_app_bar_edit_location": "Modifica posizione",
"control_bottom_app_bar_edit_time": "Modifica data e ora",
"control_bottom_app_bar_favorite": "Preferiti",
"control_bottom_app_bar_share": "Condividi",
"control_bottom_app_bar_share_to": "Share To",
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Sposta nel cestino",
"control_bottom_app_bar_unarchive": "Rimuovi dagli archivi",
"control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Upload",
@@ -165,26 +165,27 @@
"daily_title_text_date_year": "E, dd MMM, yyyy",
"date_format": "E, d LLL, y • hh:mm",
"delete_dialog_alert": "Questi oggetti saranno cancellati definitivamente da Immich e dal tuo device",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Questi elementi verranno eliminati definitivamente dal dispositivo, ma saranno ancora disponibili sul server Immich",
"delete_dialog_alert_local_non_backed_up": "Alcuni degli elementi non sono stati caricati su Immich e saranno rimossi definitivamente dal tuo dispositivo",
"delete_dialog_alert_remote": "Questi elementi verranno eliminati permanentemente dal server Immich",
"delete_dialog_cancel": "Annulla",
"delete_dialog_ok": "Elimina",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "Elimina comunque",
"delete_dialog_title": "Cancella definitivamente",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Elimina solo quelli con backup",
"delete_local_dialog_ok_force": "Elimina comunque",
"delete_shared_link_dialog_content": "Sei sicuro di voler eliminare questo link condiviso?",
"delete_shared_link_dialog_title": "Elimina link condiviso",
"description_input_hint_text": "Aggiungi descrizione...",
"description_input_submit_error": "Errore modificare descrizione, controlli I log per maggiori dettagli",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Location",
"edit_date_time_dialog_date_time": "Data e ora",
"edit_date_time_dialog_timezone": "Fuso orario",
"edit_location_dialog_title": "Posizione",
"exif_bottom_sheet_description": "Aggiungi una descrizione...",
"exif_bottom_sheet_details": "DETTAGLI",
"exif_bottom_sheet_location": "POSIZIONE",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_location_add": "Aggiungi una posizione",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Attiva griglia di foto sperimentale",
"experimental_settings_subtitle": "Usalo a tuo rischio!",
@@ -199,7 +200,7 @@
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
"home_page_building_timeline": "Costruendo il Timeline",
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_remote_err_local": "Immagini sul disco locale presenti pure nella selezione degli elementi remoti, skippando",
"home_page_favorite_err_local": "Non puoi aggiungere tra i preferiti le foto ancora non caricate",
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
"home_page_first_time_notice": "Se è la prima volta che usi l'app, assicurati di scegliere gli album per avere il Timeline con immagini e video",
@@ -214,22 +215,22 @@
"library_page_favorites": "Preferiti",
"library_page_new_album": "Nuovo Album",
"library_page_sharing": "Condividendo",
"library_page_sort_asset_count": "Number of assets",
"library_page_sort_asset_count": "Numero di elementi",
"library_page_sort_created": "Creato il più recente",
"library_page_sort_last_modified": "Ultima modifica",
"library_page_sort_most_oldest_photo": "Oldest photo",
"library_page_sort_most_oldest_photo": "Foto più vecchia",
"library_page_sort_most_recent_photo": "Più recente",
"library_page_sort_title": "Titolo album",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Enter a valid latitude",
"location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude": "Longitude",
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"location_picker_choose_on_map": "Scegli una mappa",
"location_picker_latitude": "Latitudine",
"location_picker_latitude_error": "Inserisci una latitudine valida",
"location_picker_latitude_hint": "Inserisci la tua latitudine qui",
"location_picker_longitude": "Longitudine",
"location_picker_longitude_error": "Inserisci una longitudine valida",
"location_picker_longitude_hint": "Inserisci la longitudine qui",
"login_disabled": "L'accesso è stato disattivato",
"login_form_api_exception": "API error, per favore ricontrolli URL del server e riprovi",
"login_form_back_button_text": "Back",
"login_form_back_button_text": "Indietro",
"login_form_button_text": "Login",
"login_form_email_hint": "tuaemail@email.com",
"login_form_endpoint_hint": "http://ip-del-tuo-server:port/api",
@@ -252,35 +253,35 @@
"login_form_server_error": "Non è possibile connettersi al server",
"login_password_changed_error": "C'è stato un errore durante l'aggiornamento della password",
"login_password_changed_success": "Password aggiornata con successo",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_assets_in_bound": "{} foto",
"map_assets_in_bounds": "{} foto",
"map_cannot_get_user_location": "Cannot get user's location",
"map_location_dialog_cancel": "Annulla",
"map_location_dialog_yes": "Si",
"map_location_picker_page_use_location": "Use this location",
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
"map_location_picker_page_use_location": "Usa questa posizione",
"map_location_service_disabled_content": "I servizi di geolocalizzazione devono essere attivati per visualizzare gli elementi per la tua posizione attuale. Vuoi attivarli adesso?",
"map_location_service_disabled_title": "Location Service disabled",
"map_no_assets_in_bounds": "Nessuna foto in questa zona",
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_no_location_permission_content": "L'accesso alla posizione è necessario per visualizzare gli elementi per la tua posizione attuale. Vuoi consentirlo adesso?",
"map_no_location_permission_title": "Location Permission denied",
"map_settings_dark_mode": "Modalità scura",
"map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_date_range_option_day": "Ultime 24 ore",
"map_settings_date_range_option_days": "Ultimi {} giorni",
"map_settings_date_range_option_year": "Ultimo anno",
"map_settings_date_range_option_years": "Ultimi {} anni",
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Salva",
"map_settings_dialog_title": "Map Settings",
"map_settings_include_show_archived": "Include Archived",
"map_settings_only_relative_range": "Date range",
"map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Tema della mappa",
"map_zoom_to_see_photos": "Zoom out to see photos",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Foto",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Non posso modificare la data degli elementi in sola lettura, ignorato",
"multiselect_grid_edit_gps_err_read_only": "Non posso modificare la posizione degli elementi in sola lettura, ignorato",
"notification_permission_dialog_cancel": "Annulla",
"notification_permission_dialog_content": "Per attivare le notifiche, vai alle Impostazioni e seleziona concedi",
"notification_permission_dialog_settings": "Impostazioni",
@@ -307,18 +308,18 @@
"permission_onboarding_permission_limited": "Permessi limitati. Perché Immich possa controllare e fare i backup di tutte le foto, concedere i permessi all'intera galleria dalle impostazioni ",
"permission_onboarding_request": "Immich richiede i permessi per vedere le tue foto e video",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
"profile_drawer_client_out_of_date_major": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione principale.",
"profile_drawer_client_out_of_date_minor": "L'applicazione non è aggiornata. Per favore aggiorna all'ultima versione minore.",
"profile_drawer_client_server_up_to_date": "Client e server sono aggiornati",
"profile_drawer_documentation": "Documentazione",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_server_out_of_date_major": "Il server non è aggiornato. Per favore aggiorna all'ultima versione principale.",
"profile_drawer_server_out_of_date_minor": "Il server non è aggiornato. Per favore aggiorna all'ultima versione minore.",
"profile_drawer_settings": "Impostazioni ",
"profile_drawer_sign_out": "Logout",
"profile_drawer_trash": "Trash",
"recently_added_page_title": "Aggiunti di recente",
"scaffold_body_error_occurred": "Error occurred",
"scaffold_body_error_occurred": "Si è verificato un errore.",
"search_bar_hint": "Cerca le tue foto",
"search_page_categories": "Categoria",
"search_page_favorites": "Preferiti",
@@ -326,13 +327,13 @@
"search_page_no_objects": "Nessuna informazione relativa all'oggetto disponibile",
"search_page_no_places": "Nessun informazione sul luogo disponibile",
"search_page_people": "Persone",
"search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_title": "Add a name",
"search_page_person_add_name_dialog_cancel": "Annulla",
"search_page_person_add_name_dialog_hint": "Nome",
"search_page_person_add_name_dialog_save": "Salva",
"search_page_person_add_name_dialog_title": "Aggiungi un nome",
"search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name",
"search_page_person_add_name_title": "Aggiungi un nome",
"search_page_person_edit_name": "Modifica nome",
"search_page_places": "Luoghi",
"search_page_recently_added": "Aggiunte di recente",
"search_page_screenshots": "Screenshot",
@@ -341,7 +342,7 @@
"search_page_videos": "Video",
"search_page_view_all_button": "Guarda tutto",
"search_page_your_activity": "Tua attività ",
"search_page_your_map": "Your Map",
"search_page_your_map": "La tua mappa",
"search_result_page_new_search_hint": "Nuova ricerca ",
"search_suggestion_list_smart_search_hint_1": "\nRicerca Smart è attiva di default, per usare la ricerca con i metadata usare la seguente sintassi",
"search_suggestion_list_smart_search_hint_2": "m:your-search-term",
@@ -381,17 +382,17 @@
"shared_album_activity_remove_title": "Elimina attività",
"shared_album_activity_setting_subtitle": "Let others respond",
"shared_album_activity_setting_title": "Commenti e Mi piace",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"shared_album_section_people_action_error": "Errore durante la rimozione/uscita dell'album",
"shared_album_section_people_action_leave": "Rimuovi utente dall'album",
"shared_album_section_people_action_remove_user": "Rimuovi utente dall'album",
"shared_album_section_people_owner_label": "Proprietario",
"shared_album_section_people_title": "PERSONE",
"share_dialog_preparing": "Preparo…",
"shared_link_app_bar_title": "Link condivisi",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_copied_massage": "Copiato negli appunti",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Crea link di condivisione",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_error": "Si è verificato un errore durante la creazione del link condiviso",
"shared_link_create_info": "Consenti a chiunque abbia il link di vedere le foto selezionate",
"shared_link_create_submit_button": "Crea link di condivisione",
"shared_link_edit_allow_download": "Consenti ad utenti pubblici di scaricare i contenuti",
@@ -401,32 +402,32 @@
"shared_link_edit_description": "Descrizione",
"shared_link_edit_description_hint": "Inserisci la descrizione della condivisione",
"shared_link_edit_expire_after": "Scade dopo",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_expire_after_option_day": "1 giorno",
"shared_link_edit_expire_after_option_days": "{} giorni",
"shared_link_edit_expire_after_option_hour": "1 ora",
"shared_link_edit_expire_after_option_hours": "{} ore",
"shared_link_edit_expire_after_option_minute": "1 minuto",
"shared_link_edit_expire_after_option_minutes": "{} minuti",
"shared_link_edit_expire_after_option_never": "Mai",
"shared_link_edit_password": "Password",
"shared_link_edit_password_hint": "Inserire la password di condivisione",
"shared_link_edit_show_meta": "Visualizza metadati",
"shared_link_edit_submit_button": "Aggiorna link",
"shared_link_empty": "Non hai alcun link condiviso",
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Expired",
"shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_info_chip_download": "Download",
"shared_link_expired": "Scaduto",
"shared_link_expires_day": "Scade tra {} giorno",
"shared_link_expires_days": "Scade tra {} giorni",
"shared_link_expires_hour": "Scade tra {} ora",
"shared_link_expires_hours": "Scade tra {} ore",
"shared_link_expires_minute": "Scade tra {} minuto",
"shared_link_expires_minutes": "Scade tra {} minuti",
"shared_link_expires_never": "Scadenza ∞",
"shared_link_expires_second": "Scade tra {} secondo",
"shared_link_expires_seconds": "Scade tra {} secondi",
"shared_link_info_chip_download": "Scarica",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload",
"shared_link_info_chip_upload": "Carica",
"shared_link_manage_links": "Gestisci link condivisi",
"share_done": "Done",
"share_invite": "Invita nell'album ",
@@ -454,7 +455,7 @@
"trash_page_delete": "Elimina",
"trash_page_delete_all": "Elimina tutti",
"trash_page_empty_trash_btn": "Svuota cestino",
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
"trash_page_empty_trash_dialog_content": "Vuoi eliminare gli elementi nel cestino? Questi elementi saranno eliminati definitivamente da Immich",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Gli elementi cestinati saranno eliminati definitivamente dopo {} giorni",
"trash_page_no_assets": "Nessun elemento cestinato",

View File

@@ -1,13 +1,13 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"action_common_cancel": "キャンセル",
"action_common_update": "更新",
"add_to_album_bottom_sheet_added": "{album}に追加",
"add_to_album_bottom_sheet_already_exists": "{album}に追加済み",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
"advanced_settings_self_signed_ssl_title": "Allow self-signed SSL certificates",
"advanced_settings_log_level_title": "ログレベル: {}",
"advanced_settings_prefer_remote_subtitle": "端末によっては端末上に存在するサムネイルのロードに非常に時間がかかります。このオプションをに有効にする事によってサーバーから直接画像をロードすることが可能です",
"advanced_settings_prefer_remote_title": "リモートを優先する",
"advanced_settings_self_signed_ssl_subtitle": "SSLのチェックをスキップする。Self-signedな署名で必要です",
"advanced_settings_self_signed_ssl_title": "Self-signed署名を許可する",
"advanced_settings_tile_subtitle": "追加ユーザー設定",
"advanced_settings_tile_title": "詳細設定",
"advanced_settings_troubleshooting_subtitle": "トラブルシューティング用の詳細設定をオンにする",
@@ -26,17 +26,17 @@
"album_viewer_appbar_share_err_title": "タイトル変更の失敗",
"album_viewer_appbar_share_leave": "アルバムから脱退",
"album_viewer_appbar_share_remove": "アルバムから削除",
"album_viewer_appbar_share_to": "Share To",
"album_viewer_appbar_share_to": "次の方々と共有します",
"album_viewer_page_share_add_users": "ユーザーを追加",
"all_people_page_title": "People",
"all_people_page_title": "ピープル",
"all_videos_page_title": "ビデオ",
"app_bar_signout_dialog_content": " サインアウトしますか?",
"app_bar_signout_dialog_ok": "はい",
"app_bar_signout_dialog_title": " サインアウト",
"archive_page_no_archived_assets": "アーカイブ済みの写真またはビデオがありません",
"archive_page_title": "アーカイブ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_action_delete_err_read_only": "読み取り専用の項目は削除できません。スキップします",
"asset_action_share_err_offline": "オフラインの項目をゲットできません。スキップします",
"asset_list_layout_settings_dynamic_layout_title": "ダイナミックレイアウト",
"asset_list_layout_settings_group_automatically": "自動",
"asset_list_layout_settings_group_by": "写真のグループ分け",
@@ -103,17 +103,17 @@
"backup_controller_page_uploading_file_info": "アップロード中のファイル",
"backup_err_only_album": "最低1つのアルバムを選択してください",
"backup_info_card_assets": "写真と動画",
"backup_manual_cancelled": "Cancelled",
"backup_manual_failed": "Failed",
"backup_manual_in_progress": "Upload already in progress. Try after sometime",
"backup_manual_success": "Success",
"backup_manual_title": "Upload status",
"backup_manual_cancelled": "キャンセルされました",
"backup_manual_failed": "失敗",
"backup_manual_in_progress": "アップロードが進行中です。後でもう一度試してください",
"backup_manual_success": "成功",
"backup_manual_title": "アップロード状況",
"cache_settings_album_thumbnails": "ライブラリのサムネイル ({}枚)",
"cache_settings_clear_cache_button": "キャッシュをクリア",
"cache_settings_clear_cache_button_title": "キャッシュを削除(キャッシュ再生成までアプリのパフォーマンスが著しく低下)",
"cache_settings_duplicated_assets_clear_button": "CLEAR",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
"cache_settings_duplicated_assets_clear_button": "クリア",
"cache_settings_duplicated_assets_subtitle": "アプリがブラックリストに追加している項目",
"cache_settings_duplicated_assets_title": "{}項目が重複",
"cache_settings_image_cache_size": "キャッシュのサイズ ({}枚) ",
"cache_settings_statistics_album": "ライブラリのサムネイル",
"cache_settings_statistics_assets": "{} 枚 ({}枚中)",
@@ -123,8 +123,8 @@
"cache_settings_statistics_title": "キャッシュ",
"cache_settings_subtitle": "キャッシュの動作を変更する",
"cache_settings_thumbnail_size": "サムネイルのキャッシュのサイズ ({}枚)",
"cache_settings_tile_subtitle": "Control the local storage behaviour",
"cache_settings_tile_title": "Local Storage",
"cache_settings_tile_subtitle": "ローカルストレージの挙動を確認する",
"cache_settings_tile_title": "ローカルストレージ",
"cache_settings_title": "キャッシュの設定",
"change_password_form_confirm_password": "確定",
"change_password_form_description": "{name}さん こんにちは\n\nサーバーにアクセスするのが初めてか、パスワードリセットのリクエストがされました。新しいパスワードを入力してください",
@@ -142,18 +142,18 @@
"control_bottom_app_bar_archive": "アーカイブ",
"control_bottom_app_bar_create_new_album": "アルバムを作成",
"control_bottom_app_bar_delete": "削除",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_delete_from_immich": "Immichから削除",
"control_bottom_app_bar_delete_from_local": "端末から削除",
"control_bottom_app_bar_edit_location": "位置情報を編集",
"control_bottom_app_bar_edit_time": "日時を変更",
"control_bottom_app_bar_favorite": "お気に入り",
"control_bottom_app_bar_share": "共有",
"control_bottom_app_bar_share_to": "Share To",
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_share_to": "次のユーザーに共有: ",
"control_bottom_app_bar_stack": "スタック",
"control_bottom_app_bar_trash_from_immich": "ゴミ箱に捨てる",
"control_bottom_app_bar_unarchive": "アーカイブを解除",
"control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Upload",
"control_bottom_app_bar_unfavorite": "お気に入りから外す",
"control_bottom_app_bar_upload": "アップロード",
"create_album_page_untitled": "タイトルなし",
"create_shared_album_page_create": "作成",
"create_shared_album_page_share": "共有",
@@ -165,26 +165,27 @@
"daily_title_text_date_year": "yyyy年 MM月 DD日, EE",
"date_format": "MM月 DD日, EE • hh時mm分",
"delete_dialog_alert": "サーバーとデバイスの両方から永久的に削除されます!",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "選択された項目は端末から削除されますがImmichには残ります",
"delete_dialog_alert_local_non_backed_up": "Immichにバックアップされていない項目があります。それらの項目はデバイスからも永久に削除されます",
"delete_dialog_alert_remote": "選択された項目はImmichから永久に削除されます",
"delete_dialog_cancel": "キャンセル",
"delete_dialog_ok": "削除",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "削除します",
"delete_dialog_title": "永久的に削除",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "バックアップ済みのみを削除",
"delete_local_dialog_ok_force": "削除します",
"delete_shared_link_dialog_content": "本当にこの共有リンクを消しますか?",
"delete_shared_link_dialog_title": "共有リンクを消す",
"description_input_hint_text": "説明を追加",
"description_input_submit_error": "説明の編集に失敗、詳細の確認はログで行ってください",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Location",
"edit_date_time_dialog_date_time": "日付と時間",
"edit_date_time_dialog_timezone": "タイムゾーン",
"edit_location_dialog_title": "位置情報",
"exif_bottom_sheet_description": "説明を追加",
"exif_bottom_sheet_details": "詳細",
"exif_bottom_sheet_location": "撮影場所",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_location_add": "位置情報を追加",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "製作途中(WIP)",
"experimental_settings_new_asset_list_title": "試験的なグリッドを有効化",
"experimental_settings_subtitle": "試験的機能につき自己責任で!",
@@ -194,42 +195,42 @@
"home_page_add_to_album_conflicts": "{album}に{added}枚写真を追加しました。追加済みの{failed}枚はスキップしました。",
"home_page_add_to_album_err_local": "まだアップロードされてない項目はアルバムに登録できません",
"home_page_add_to_album_success": "{album}に{added}枚写真を追加しました",
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
"home_page_album_err_partner": "まだパートナーの写真はアルバムに追加できません。スキップします(アップデート待ってね)",
"home_page_archive_err_local": "まだアップロードされてない項目はアーカイブできません",
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
"home_page_archive_err_partner": "パートナーの写真はアーカイブできません。スキップします",
"home_page_building_timeline": "タイムライン構築中",
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_err_partner": "パートナーの写真は削除できません。スキップします",
"home_page_delete_remote_err_local": "リモート削除の選択にローカルなアイテムが含まれています。スキップします",
"home_page_favorite_err_local": "まだアップロードされてない項目はお気に入り登録できません",
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
"home_page_favorite_err_partner": "まだパートナーの写真をお気に入り登録できません。スキップします(アップデート待ってね)",
"home_page_first_time_notice": "はじめてアプリを使う場合、タイムラインに写真を表示するためにアルバムを選択してください",
"home_page_share_err_local": "Can not share local assets via link, skipping",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"home_page_share_err_local": "ローカルのみの項目をリンクで共有はできません。スキップします",
"home_page_upload_err_limit": "一回でアップロードできる写真の数は30枚です。スキップします",
"image_viewer_page_state_provider_download_error": "ダウンロード失敗",
"image_viewer_page_state_provider_download_success": "ダウンロード成功",
"image_viewer_page_state_provider_share_error": "Share Error",
"image_viewer_page_state_provider_share_error": "共有エラー",
"library_page_albums": "アルバム",
"library_page_archive": "アーカイブ",
"library_page_device_albums": "デバイス上のアルバム",
"library_page_favorites": "お気に入り",
"library_page_new_album": "新しいアルバム",
"library_page_sharing": "共有中",
"library_page_sort_asset_count": "Number of assets",
"library_page_sort_asset_count": "項目の数",
"library_page_sort_created": "作成日時",
"library_page_sort_last_modified": "Last modified",
"library_page_sort_most_oldest_photo": "Oldest photo",
"library_page_sort_most_recent_photo": "Most recent photo",
"library_page_sort_last_modified": "最終変更",
"library_page_sort_most_oldest_photo": "一番古い項目",
"library_page_sort_most_recent_photo": "最近の項目",
"library_page_sort_title": "アルバム名",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Enter a valid latitude",
"location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude": "Longitude",
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"login_disabled": "Login has been disabled",
"location_picker_choose_on_map": "マップを選択",
"location_picker_latitude": "緯度",
"location_picker_latitude_error": "有効な緯度を入力してください",
"location_picker_latitude_hint": "緯度をここに入力",
"location_picker_longitude": "経度",
"location_picker_longitude_error": "有効な経度を入力してください",
"location_picker_longitude_hint": "経度をここに入力",
"login_disabled": "ログインは無効化されました",
"login_form_api_exception": "APIエラー。URLをチェックしてもう一度試してください",
"login_form_back_button_text": "Back",
"login_form_back_button_text": "戻る",
"login_form_button_text": "ログイン",
"login_form_email_hint": "hoge@email.com",
"login_form_endpoint_hint": "https://example.com:port/api",
@@ -242,7 +243,7 @@
"login_form_failed_get_oauth_server_config": "OAuthログインに失敗しました。サーバーのURLを確認してください。",
"login_form_failed_get_oauth_server_disable": "このサーバーではOAuthが使えません",
"login_form_failed_login": "ログインエラー。サーバーのURL・メールアドレス・パスワードを再確認してください。",
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
"login_form_handshake_exception": "Handshake Exceptionエラー。self-signed署名を設定で有効にしてください",
"login_form_label_email": "メールアドレス",
"login_form_label_password": "パスワード",
"login_form_next_button": "次",
@@ -250,53 +251,53 @@
"login_form_save_login": "ログインを保持",
"login_form_server_empty": "URLを入力",
"login_form_server_error": "サーバーに接続できません",
"login_password_changed_error": "There was an error updating your password",
"login_password_changed_success": "Password updated successfully",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_cannot_get_user_location": "Cannot get user's location",
"map_location_dialog_cancel": "Cancel",
"map_location_dialog_yes": "Yes",
"map_location_picker_page_use_location": "Use this location",
"map_location_service_disabled_content": "Location service needs to be enabled to display assets from your current location. Do you want to enable it now?",
"map_location_service_disabled_title": "Location Service disabled",
"map_no_assets_in_bounds": "No photos in this area",
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_no_location_permission_title": "Location Permission denied",
"map_settings_dark_mode": "Dark mode",
"map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Save",
"map_settings_dialog_title": "Map Settings",
"map_settings_include_show_archived": "Include Archived",
"login_password_changed_error": "パスワードの変更でエラーが発生しました",
"login_password_changed_success": "パスワードの変更に成功",
"map_assets_in_bound": "{}項目",
"map_assets_in_bounds": "{}項目",
"map_cannot_get_user_location": "位置情報がゲットできません",
"map_location_dialog_cancel": "キャンセル",
"map_location_dialog_yes": "はい",
"map_location_picker_page_use_location": "この位置情報を使う",
"map_location_service_disabled_content": "現在地の項目を表示するには位置情報がオンである必要があります。有効化しますか?",
"map_location_service_disabled_title": "位置情報がオフです",
"map_no_assets_in_bounds": "このエリアに写真はありません",
"map_no_location_permission_content": "現在地の項目を表示するには位置情報へのアクセスが必要です。許可しますか?",
"map_no_location_permission_title": "位置情報へのアクセスが拒否されました",
"map_settings_dark_mode": "ダークモード",
"map_settings_date_range_option_all": "全て",
"map_settings_date_range_option_day": "過去24時間",
"map_settings_date_range_option_days": "過去{}日間",
"map_settings_date_range_option_year": "過去1年",
"map_settings_date_range_option_years": "過去{}年間",
"map_settings_dialog_cancel": "キャンセル",
"map_settings_dialog_save": "セーブ",
"map_settings_dialog_title": "マップの設定",
"map_settings_include_show_archived": "アーカイブ済みを含める",
"map_settings_only_relative_range": "Date range",
"map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_theme_settings": "Map Theme",
"map_zoom_to_see_photos": "Zoom out to see photos",
"map_settings_only_show_favorites": "お気に入りのみを表示",
"map_settings_theme_settings": "マップの見た目",
"map_zoom_to_see_photos": "写真を見るにはズームアウト",
"monthly_title_text_date_format": "yyyy年 MM月",
"motion_photos_page_title": "モーションフォト",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "読み取り専用の項目の日付を変更できません",
"multiselect_grid_edit_gps_err_read_only": "読み取り専用の項目の位置情報を変更できません",
"notification_permission_dialog_cancel": "キャンセル",
"notification_permission_dialog_content": "通知を許可するには設定を開いてオンにしてください",
"notification_permission_dialog_settings": "設定",
"notification_permission_list_tile_content": "通知の許可 をオンにしてください",
"notification_permission_list_tile_enable_button": "通知をオンにする",
"notification_permission_list_tile_title": "通知の許可",
"partner_page_add_partner": "Add partner",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_title": "Partner",
"permission_onboarding_back": "Back",
"partner_page_add_partner": "パートナーを追加",
"partner_page_empty_message": "まだどのパートナーとも写真を共有してません",
"partner_page_no_more_users": "追加できるユーザーがもういません",
"partner_page_partner_add_failed": "パートナーの追加に失敗",
"partner_page_select_partner": "パートナーを選択",
"partner_page_shared_to_title": "次のユーザーと共有しす: ",
"partner_page_stop_sharing_content": "{}は写真へのアクセスができなくなります",
"partner_page_stop_sharing_title": "写真の共有を無効化しますか?",
"partner_page_title": "パートナー",
"permission_onboarding_back": "戻る",
"permission_onboarding_continue_anyway": "無視して続行",
"permission_onboarding_get_started": "はじめる",
"permission_onboarding_go_to_settings": "システム設定",
@@ -307,32 +308,32 @@
"permission_onboarding_permission_limited": "写真へのアクセスが制限されています。Immichに写真のバックアップと管理を行わせるにはシステム設定から写真と動画のアクセス権限を変更してください。",
"permission_onboarding_request": "Immichは写真へのアクセス許可が必要です",
"profile_drawer_app_logs": "ログ",
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
"profile_drawer_client_out_of_date_major": "アプリが更新されてません。最新のバージョンに更新してください",
"profile_drawer_client_out_of_date_minor": "アプリが更新されてません。最新のマイナーバージョンに更新してください",
"profile_drawer_client_server_up_to_date": "すべて最新です",
"profile_drawer_documentation": "Documentation",
"profile_drawer_documentation": "Immcihの説明書",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_server_out_of_date_major": "サーバーが更新されてません。最新のバージョンに更新してください",
"profile_drawer_server_out_of_date_minor": "サーバーが更新されてません。最新のマイナーバージョンに更新してください",
"profile_drawer_settings": "設定",
"profile_drawer_sign_out": "サインアウト",
"profile_drawer_trash": "Trash",
"profile_drawer_trash": "ゴミ箱",
"recently_added_page_title": "最近",
"scaffold_body_error_occurred": "Error occurred",
"scaffold_body_error_occurred": "エラーが発生しました",
"search_bar_hint": "写真を検索",
"search_page_categories": "カテゴリ",
"search_page_favorites": "お気に入り",
"search_page_motion_photos": "モーションフォト",
"search_page_no_objects": "被写体に関するデータがなし",
"search_page_no_places": "場所に関するデータなし",
"search_page_people": "People",
"search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_title": "Add a name",
"search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name",
"search_page_people": "ピープル",
"search_page_person_add_name_dialog_cancel": "キャンセル",
"search_page_person_add_name_dialog_hint": "名前",
"search_page_person_add_name_dialog_save": "セーブ",
"search_page_person_add_name_dialog_title": "名前を追加",
"search_page_person_add_name_subtitle": "名前で検索して高速に探す",
"search_page_person_add_name_title": "名前を追加",
"search_page_person_edit_name": "名前を変更",
"search_page_places": "撮影地",
"search_page_recently_added": "最近追加",
"search_page_screenshots": "スクリーンショット",
@@ -341,7 +342,7 @@
"search_page_videos": "ビデオ",
"search_page_view_all_button": "すべて表示",
"search_page_your_activity": "アクティビティ",
"search_page_your_map": "Your Map",
"search_page_your_map": "あなたのマップ",
"search_result_page_new_search_hint": "検索",
"search_suggestion_list_smart_search_hint_1": "スマート検索はデフォルトでオンになっています。メタデータで検索を行う場合:",
"search_suggestion_list_smart_search_hint_2": "m:単語",
@@ -349,7 +350,7 @@
"select_user_for_sharing_page_err_album": "アルバム作成に失敗",
"select_user_for_sharing_page_share_suggestions": "ユーザ一覧",
"server_info_box_app_version": "アプリVer.",
"server_info_box_latest_release": "Latest Version",
"server_info_box_latest_release": "最新バージョン",
"server_info_box_server_url": " サーバのURL",
"server_info_box_server_version": "サーバーVer.",
"setting_image_viewer_help": "写真をタップするとサムネイル・中画質(要設定)・オリジナル(要設定)の順に読み込みます",
@@ -375,66 +376,66 @@
"share_add_photos": "写真を追加",
"share_add_title": "タイトルを追加",
"share_create_album": "アルバムを作成",
"shared_album_activities_input_disable": "Comment is disabled",
"shared_album_activities_input_hint": "Say something",
"shared_album_activity_remove_content": "Do you want to delete this activity?",
"shared_album_activity_remove_title": "Delete Activity",
"shared_album_activity_setting_subtitle": "Let others respond",
"shared_album_activity_setting_title": "Comments & likes",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"shared_album_activities_input_disable": "コメントはオフになってます",
"shared_album_activities_input_hint": "何か書き込みましょう",
"shared_album_activity_remove_content": "このアクティビティを削除しますか",
"shared_album_activity_remove_title": "アクティビティを削除します",
"shared_album_activity_setting_subtitle": "他のユーザーの返信を許可する",
"shared_album_activity_setting_title": "お気に入りとコメント",
"shared_album_section_people_action_error": "アルバムからの退出に失敗",
"shared_album_section_people_action_leave": "ユーザーをアルバムから退出",
"shared_album_section_people_action_remove_user": "ユーザーをアルバムから退出",
"shared_album_section_people_owner_label": "オーナー",
"shared_album_section_people_title": "ピープル",
"share_dialog_preparing": "準備中",
"shared_link_app_bar_title": "共有リンク",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_clipboard_copied_massage": "クリップボードにコピーしました",
"shared_link_clipboard_text": "リンク: {}\nパスワード: {}",
"shared_link_create_app_bar_title": "共有リンクを作る",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
"shared_link_create_error": "共有用のリンク作成時にエラーが発生しました",
"shared_link_create_info": "誰でも写真を見れるようにする",
"shared_link_create_submit_button": "リンクを作る",
"shared_link_edit_allow_download": "Allow public user to download",
"shared_link_edit_allow_upload": "Allow public user to upload",
"shared_link_edit_allow_download": "写真のダウンロードの許可",
"shared_link_edit_allow_upload": "写真のアップロードを許可",
"shared_link_edit_app_bar_title": " リンクを編集する",
"shared_link_edit_change_expiry": "Change expiration time",
"shared_link_edit_description": " デスクリプション ",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_expire_after": "Expire after",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_change_expiry": "有効期限を変更",
"shared_link_edit_description": "概要欄",
"shared_link_edit_description_hint": "概要を追加",
"shared_link_edit_expire_after": "有効期限は",
"shared_link_edit_expire_after_option_day": "1",
"shared_link_edit_expire_after_option_days": "{}",
"shared_link_edit_expire_after_option_hour": "1時間",
"shared_link_edit_expire_after_option_hours": "{}時間",
"shared_link_edit_expire_after_option_minute": "1",
"shared_link_edit_expire_after_option_minutes": "{}",
"shared_link_edit_expire_after_option_never": "有効期限なし",
"shared_link_edit_password": " パスワード",
"shared_link_edit_password_hint": "共有パスワードを入力する",
"shared_link_edit_show_meta": " メタデータを見る",
"shared_link_edit_submit_button": "リンクをアップデートする",
"shared_link_empty": "共有リンクはありません ",
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Expired",
"shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_info_chip_download": "Download",
"shared_link_error_server_url_fetch": "サーバーのURLがゲットできません",
"shared_link_expired": "有効期限が切れました",
"shared_link_expires_day": "{}日間で切れます",
"shared_link_expires_days": "{}日間で有効期限が切れます",
"shared_link_expires_hour": "{}時間で切れます",
"shared_link_expires_hours": "{}時間で有効期限が切れます",
"shared_link_expires_minute": "{}分で切れます",
"shared_link_expires_minutes": "{}分で切れます",
"shared_link_expires_never": "有効期限はありません",
"shared_link_expires_second": "{}秒で切れます",
"shared_link_expires_seconds": "{}秒で切れます",
"shared_link_info_chip_download": "ダウンロード",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Manage Shared links",
"share_done": "Done",
"shared_link_info_chip_upload": "アップロード",
"shared_link_manage_links": "共有済みのリンクを管理",
"share_done": "完了",
"share_invite": "アルバムに招待",
"sharing_page_album": "共有アルバム",
"sharing_page_description": "共有アルバムを作成して同じネットワークにいる人たちに写真を共有",
"sharing_page_empty_list": "共有アルバムなし",
"sharing_silver_appbar_create_shared_album": "共有アルバムを作成",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_shared_links": "共有リンク",
"sharing_silver_appbar_share_partner": "パートナーと共有",
"tab_controller_nav_library": "ライブラリ",
"tab_controller_nav_photos": "写真",
@@ -450,30 +451,30 @@
"theme_setting_theme_title": "テーマ",
"theme_setting_three_stage_loading_subtitle": "三段階読み込みを有効にするとパフォーマンスが改善する可能性がありますが、ネットワーク負荷が著しく増加します",
"theme_setting_three_stage_loading_title": "三段階読み込みをオンにする",
"translated_text_options": "Options",
"trash_page_delete": "Delete",
"trash_page_delete_all": "Delete All",
"trash_page_empty_trash_btn": "Empty trash",
"trash_page_empty_trash_dialog_content": "Do you want to empty your trashed assets? These items will be permanently removed from Immich",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Trashed items will be permanently deleted after {} days",
"trash_page_no_assets": "No trashed assets",
"trash_page_restore": "Restore",
"trash_page_restore_all": "Restore All",
"trash_page_select_assets_btn": "Select assets",
"trash_page_select_btn": "Select",
"trash_page_title": "Trash ({})",
"upload_dialog_cancel": "Cancel",
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"translated_text_options": "オプション",
"trash_page_delete": "削除",
"trash_page_delete_all": "全て削除",
"trash_page_empty_trash_btn": "コミ箱を空にする",
"trash_page_empty_trash_dialog_content": "ゴミ箱を空にしますか?選択された項目は完全に削除されます。この操作は取り消せません",
"trash_page_empty_trash_dialog_ok": "了解",
"trash_page_info": "ゴミ箱に移動したアイテムは{}日後に削除されます",
"trash_page_no_assets": "ゴミ箱は空です",
"trash_page_restore": "復元",
"trash_page_restore_all": "全て復元",
"trash_page_select_assets_btn": "項目を選択",
"trash_page_select_btn": "選択",
"trash_page_title": "削除({})",
"upload_dialog_cancel": "キャンセル",
"upload_dialog_info": "選択した項目のバックアップをしますか?",
"upload_dialog_ok": "アップロード",
"upload_dialog_title": "アップロード",
"version_announcement_overlay_ack": "了解",
"version_announcement_overlay_release_notes": "更新情報",
"version_announcement_overlay_text_1": "こんにちは、またはこんばんは!新しい",
"version_announcement_overlay_text_2": "のバージョンが公開中です。",
"version_announcement_overlay_text_3": "を確認してみてください。docker-composeや.envファイルが最新の状態に更新されているか、特にWatchTowerなどのツールを使ってDockerイメージを自動アップデートしてる人は確認してください。",
"version_announcement_overlay_title": "サーバーの新バージョンリリース\uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",
"viewer_unstack": "Un-Stack"
"viewer_remove_from_stack": "スタックから外す",
"viewer_stack_use_as_main_asset": "メインの画像として使用する",
"viewer_unstack": "スタックを解除"
}

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "상세정보",
"exif_bottom_sheet_location": "위치",
"exif_bottom_sheet_location_add": "위치 지정",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "진행중",
"experimental_settings_new_asset_list_title": "실험적 사진 그리드 적용",
"experimental_settings_subtitle": "문제시 책임지지 않습니다!",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "INFORMĀCIJA",
"exif_bottom_sheet_location": "ATRAŠANĀS VIETA",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Izstrādes posmā",
"experimental_settings_new_asset_list_title": "Iespējot eksperimentālo fotorežģi",
"experimental_settings_subtitle": "Izmanto uzņemoties risku!",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",

View File

@@ -35,8 +35,8 @@
"app_bar_signout_dialog_title": "Logg ut",
"archive_page_no_archived_assets": "Ingen arkiverte objekter funnet",
"archive_page_title": "Arkiv ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_action_delete_err_read_only": "Kan ikke slette objekt(er) med kun lese-rettighet, hopper over",
"asset_action_share_err_offline": "Kan ikke hente offline objekt(er), hopper over",
"asset_list_layout_settings_dynamic_layout_title": "Dynamisk bildeorganisering",
"asset_list_layout_settings_group_automatically": "Automatisk",
"asset_list_layout_settings_group_by": "Grupper bilder etter",
@@ -142,15 +142,15 @@
"control_bottom_app_bar_archive": "Arkiver",
"control_bottom_app_bar_create_new_album": "Lag nytt album",
"control_bottom_app_bar_delete": "Slett",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_delete_from_immich": "Slett fra Immich",
"control_bottom_app_bar_delete_from_local": "Slett fra enhet",
"control_bottom_app_bar_edit_location": "Endre lokasjon",
"control_bottom_app_bar_edit_time": "Endre Dato og tid",
"control_bottom_app_bar_favorite": "Favoritt",
"control_bottom_app_bar_share": "Del",
"control_bottom_app_bar_share_to": "Del til",
"control_bottom_app_bar_stack": "Stable",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Flytt til søppelkasse",
"control_bottom_app_bar_unarchive": "Fjern fra arkiv",
"control_bottom_app_bar_unfavorite": "Fjern favoritt",
"control_bottom_app_bar_upload": "Last opp",
@@ -165,15 +165,15 @@
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Disse objektene vil bli slettet permanent fra Immich og fra enheten din",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Disse objektene vil bli permanent slettet fra enheten din, men vil fortsatt være tilgjengelige fra Immich serveren",
"delete_dialog_alert_local_non_backed_up": "Noen av objektene er ikke sikkerhetskopiert til Immich og vil bli permanent fjernet fra enheten din",
"delete_dialog_alert_remote": "Disse objektene vil bli permanent slettet fra Immich serveren",
"delete_dialog_cancel": "Avbryt",
"delete_dialog_ok": "Slett",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "Slett uansett",
"delete_dialog_title": "Slett permanent",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Slett kun sikkerhetskopierte objekter",
"delete_local_dialog_ok_force": "Slett uansett",
"delete_shared_link_dialog_content": "Er du sikker på at du vil slette denne delte linken?",
"delete_shared_link_dialog_title": "Slett delt link",
"description_input_hint_text": "Legg til beskrivelse ...",
@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETALJER",
"exif_bottom_sheet_location": "PLASSERING",
"exif_bottom_sheet_location_add": "Legg til lokasjon",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Under utvikling",
"experimental_settings_new_asset_list_title": "Aktiver eksperimentell rutenettsvisning",
"experimental_settings_subtitle": "Bruk på egen risiko!",
@@ -199,7 +200,7 @@
"home_page_archive_err_partner": "Kan ikke arkivere partnerobjekter, hopper over",
"home_page_building_timeline": "Genererer tidslinjen",
"home_page_delete_err_partner": "Kan ikke slette partnerobjekter, hopper over",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_remote_err_local": "Lokale objekter i fjernslettingsvalgene, hopper over",
"home_page_favorite_err_local": "Kan ikke sette favoritt på lokale objekter enda, hopper over",
"home_page_favorite_err_partner": "Kan ikke merke partnerobjekter som favoritt enda, hopper over",
"home_page_first_time_notice": "Hvis dette er første gangen du benytter appen, velg et album (eller flere) for sikkerhetskopiering, slik at tidslinjen kan fylles med dine bilder og videoer.",
@@ -275,12 +276,12 @@
"map_settings_include_show_archived": "Inkluder arkiverte",
"map_settings_only_relative_range": "Datoområde",
"map_settings_only_show_favorites": "Vis kun favoritter",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Karttema",
"map_zoom_to_see_photos": "Zoom ut for å se bilder",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bevegelige bilder",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over",
"multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over",
"notification_permission_dialog_cancel": "Avbryt",
"notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.",
"notification_permission_dialog_settings": "Innstillinger",

View File

@@ -1,6 +1,6 @@
{
"action_common_cancel": "Annuleren",
"action_common_update": "Updaten",
"action_common_update": "Bijwerken",
"add_to_album_bottom_sheet_added": "Toegevoegd aan {album}",
"add_to_album_bottom_sheet_already_exists": "Staat al in {album}",
"advanced_settings_log_level_title": "Log niveau: {}",
@@ -26,17 +26,17 @@
"album_viewer_appbar_share_err_title": "Albumtitel wijzigen mislukt",
"album_viewer_appbar_share_leave": "Verlaat album",
"album_viewer_appbar_share_remove": "Verwijder uit album",
"album_viewer_appbar_share_to": "Deel Naar",
"album_viewer_appbar_share_to": "Delen met",
"album_viewer_page_share_add_users": "Gebruikers toevoegen",
"all_people_page_title": "Personen",
"all_videos_page_title": "Video's",
"app_bar_signout_dialog_content": "Weet je zeker dat je je wilt afmelden?",
"app_bar_signout_dialog_content": "Weet je zeker dat je wilt uitloggen?",
"app_bar_signout_dialog_ok": "Ja",
"app_bar_signout_dialog_title": "Log uit",
"archive_page_no_archived_assets": "Geen gearchiveerde assets gevonden",
"archive_page_title": "Archief ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_action_delete_err_read_only": "Kan alleen-lezen asset(s) niet verwijderen, overslaan",
"asset_action_share_err_offline": "Kan offline asset(s) niet ophalen, overslaan",
"asset_list_layout_settings_dynamic_layout_title": "Dynamische layout",
"asset_list_layout_settings_group_automatically": "Automatisch",
"asset_list_layout_settings_group_by": "Groupeer assets per",
@@ -104,7 +104,7 @@
"backup_err_only_album": "Kan het enige album niet verwijderen",
"backup_info_card_assets": "assets",
"backup_manual_cancelled": "Geannuleerd",
"backup_manual_failed": "Gefaald",
"backup_manual_failed": "Mislukt",
"backup_manual_in_progress": "Het uploaden is al bezig. Probeer het na een tijdje",
"backup_manual_success": "Succes",
"backup_manual_title": "Uploadstatus",
@@ -113,7 +113,7 @@
"cache_settings_clear_cache_button_title": "Wist de cache van de app. Dit zal de presentaties van de app aanzienlijk beïnvloeden totdat de cache opnieuw is opgebouwd.",
"cache_settings_duplicated_assets_clear_button": "MAAK VRIJ",
"cache_settings_duplicated_assets_subtitle": "Foto's en video's op de zwarte lijst van de app",
"cache_settings_duplicated_assets_title": "Gedupliceerde Assets ({})",
"cache_settings_duplicated_assets_title": "Gedupliceerde assets ({})",
"cache_settings_image_cache_size": "Grootte afbeeldingscache ({} assets)",
"cache_settings_statistics_album": "Bibliotheekthumbnails",
"cache_settings_statistics_assets": "{} assets ({})",
@@ -124,7 +124,7 @@
"cache_settings_subtitle": "Beheer het cachegedrag van de Immich app",
"cache_settings_thumbnail_size": "Thumbnail-cachegrootte ({} assets)",
"cache_settings_tile_subtitle": "Beheer het gedrag van lokale opslag",
"cache_settings_tile_title": "Lokale Opslag",
"cache_settings_tile_title": "Lokale opslag",
"cache_settings_title": "Cache-instellingen",
"change_password_form_confirm_password": "Bevestig wachtwoord",
"change_password_form_description": "Hallo {name},\n\nDit is ofwel de eerste keer dat je inlogt, of er is een verzoek gedaan om je wachtwoord te wijzigen. Vul hieronder een nieuw wachtwoord in.",
@@ -142,15 +142,15 @@
"control_bottom_app_bar_archive": "Archiveren",
"control_bottom_app_bar_create_new_album": "Nieuw album maken",
"control_bottom_app_bar_delete": "Verwijderen",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Locatie Bewerken",
"control_bottom_app_bar_edit_time": "Datum & Tijd Bewerken",
"control_bottom_app_bar_delete_from_immich": "Verwijderen van Immich",
"control_bottom_app_bar_delete_from_local": "Verwijderen van apparaat",
"control_bottom_app_bar_edit_location": "Locatie bewerken",
"control_bottom_app_bar_edit_time": "Datum & tijd bewerken",
"control_bottom_app_bar_favorite": "Favoriet",
"control_bottom_app_bar_share": "Delen",
"control_bottom_app_bar_share_to": "Deel Naar",
"control_bottom_app_bar_share_to": "Delen met",
"control_bottom_app_bar_stack": "Stapel",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Verplaatsen naar prullenbak",
"control_bottom_app_bar_unarchive": "Herstellen",
"control_bottom_app_bar_unfavorite": "Onfavoriet",
"control_bottom_app_bar_upload": "Uploaden",
@@ -165,26 +165,27 @@
"daily_title_text_date_year": "E dd MMM yyyy",
"date_format": "E d LLL y • H:mm",
"delete_dialog_alert": "Deze items zullen permanent verwijderd worden van Immich en je apparaat",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Deze items worden permanent verwijderd van je apparaat, maar blijven beschikbaar op de Immich server",
"delete_dialog_alert_local_non_backed_up": "Van sommige items is geen back-up gemaakt in Immich en zullen permanent van je apparaat worden verwijderd",
"delete_dialog_alert_remote": "Deze items worden permanent verwijderd van de Immich server",
"delete_dialog_cancel": "Annuleren",
"delete_dialog_ok": "Verwijderen",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "Toch verwijderen",
"delete_dialog_title": "Permanent verwijderen",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Verwijder alleen met back-up",
"delete_local_dialog_ok_force": "Toch verwijderen",
"delete_shared_link_dialog_content": "Weet je zeker dat je deze gedeelde link wilt verwijderen?",
"delete_shared_link_dialog_title": "Verwijder Gedeelde Link",
"delete_shared_link_dialog_title": "Verwijder gedeelde link",
"description_input_hint_text": "Beschrijving toevoegen...",
"description_input_submit_error": "Beschrijving bijwerken mislukt, controleer het logboek voor meer details",
"edit_date_time_dialog_date_time": "Datum en Tijd",
"edit_date_time_dialog_date_time": "Datum en tijd",
"edit_date_time_dialog_timezone": "Tijdzone",
"edit_location_dialog_title": "Locatie",
"exif_bottom_sheet_description": "Beschrijving toevoegen...",
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATIE",
"exif_bottom_sheet_location_add": "Locatie toevoegen",
"exif_bottom_sheet_people": "MENSEN",
"experimental_settings_new_asset_list_subtitle": "Werk in uitvoering",
"experimental_settings_new_asset_list_title": "Experimenteel fotoraster inschakelen",
"experimental_settings_subtitle": "Gebruik op eigen risico!",
@@ -199,7 +200,7 @@
"home_page_archive_err_partner": "Partner assets kunnen niet gearchiveerd worden, overslaan",
"home_page_building_timeline": "Tijdlijn opbouwen",
"home_page_delete_err_partner": "Partner assets kunnen niet verwijderd worden, overslaan",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_delete_remote_err_local": "Lokale assets staan in verwijder selectie externe assets, overslaan",
"home_page_favorite_err_local": "Lokale assets kunnen nog niet als favoriet worden aangemerkt, overslaan",
"home_page_favorite_err_partner": "Partner assets kunnen nog niet ge-favoriet worden, overslaan",
"home_page_first_time_notice": "Als dit de eerste keer is dat je de app gebruikt, zorg er dan voor dat je een back-up album kiest, zodat de tijdlijn gevuld kan worden met foto's en video's uit het album.",
@@ -259,10 +260,10 @@
"map_location_dialog_yes": "Ja",
"map_location_picker_page_use_location": "Gebruik deze locatie",
"map_location_service_disabled_content": "Locatie service moet ingeschakeld zijn om assets van je huidige locatie weer te geven. Wil je het nu inschakelen?",
"map_location_service_disabled_title": "Locatie Service uitgeschakeld",
"map_location_service_disabled_title": "Locatie service uitgeschakeld",
"map_no_assets_in_bounds": "Geen foto's in dit gebied",
"map_no_location_permission_content": "Locatie toestemming is nodig om assets van je huidige locatie weer te geven. Wil je het nu toestaan?",
"map_no_location_permission_title": "Locatie Toestemming geweigerd",
"map_no_location_permission_title": "Locatie toestemming geweigerd",
"map_settings_dark_mode": "Donkere modus",
"map_settings_date_range_option_all": "Alle",
"map_settings_date_range_option_day": "Afgelopen 24 uur",
@@ -270,17 +271,17 @@
"map_settings_date_range_option_year": "Afgelopen jaar",
"map_settings_date_range_option_years": "Afgelopen {} jaar",
"map_settings_dialog_cancel": "Annuleren",
"map_settings_dialog_save": "Sla op",
"map_settings_dialog_save": "Opslaan",
"map_settings_dialog_title": "Kaart Instellingen",
"map_settings_include_show_archived": "Weergeef Gearchiveerden",
"map_settings_include_show_archived": "Toon gearchiveerde",
"map_settings_only_relative_range": "Datum bereik",
"map_settings_only_show_favorites": "Toon enkel favorieten",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Kaart thema",
"map_zoom_to_see_photos": "Zoom uit om foto's te zien",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Bewegende foto's",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Kan datum van alleen-lezen asset(s) niet wijzigen, overslaan",
"multiselect_grid_edit_gps_err_read_only": "Kan locatie van alleen-lezen asset(s) niet wijzigen, overslaan",
"notification_permission_dialog_cancel": "Annuleren",
"notification_permission_dialog_content": "Om meldingen in te schakelen, ga naar Instellingen en selecteer toestaan.",
"notification_permission_dialog_settings": "Instellingen",
@@ -390,7 +391,7 @@
"shared_link_app_bar_title": "Gedeelde links",
"shared_link_clipboard_copied_massage": "Gekopieerd naar klembord",
"shared_link_clipboard_text": "Link: {}\nWachtwoord: {}",
"shared_link_create_app_bar_title": "Link maken om te delen",
"shared_link_create_app_bar_title": "Gedeelde link maken",
"shared_link_create_error": "Fout bij het maken van een gedeelde link",
"shared_link_create_info": "Laat iedereen met de link de geselecteerde foto(s) zien",
"shared_link_create_submit_button": "Link maken",
@@ -399,7 +400,7 @@
"shared_link_edit_app_bar_title": "Bewerk link",
"shared_link_edit_change_expiry": "Bewerk vervaltijd",
"shared_link_edit_description": "Beschrijving",
"shared_link_edit_description_hint": "Geef de deel beschrijving",
"shared_link_edit_description_hint": "Voer beschrijving voor de gedeelde link in",
"shared_link_edit_expire_after": "Verval na",
"shared_link_edit_expire_after_option_day": "1 dag",
"shared_link_edit_expire_after_option_days": "{} dagen",
@@ -409,9 +410,9 @@
"shared_link_edit_expire_after_option_minutes": "{} minuten",
"shared_link_edit_expire_after_option_never": "Nooit",
"shared_link_edit_password": "Wachtwoord",
"shared_link_edit_password_hint": "Voer het deel wachtwoord in",
"shared_link_edit_password_hint": "Voer wachtwoord voor de gedeelde link in",
"shared_link_edit_show_meta": "Toon metadata",
"shared_link_edit_submit_button": "Update link",
"shared_link_edit_submit_button": "Link bijwerken",
"shared_link_empty": "Je hebt geen gedeelde links",
"shared_link_error_server_url_fetch": "Kan de server url niet ophalen",
"shared_link_expired": "Verlopen",
@@ -452,19 +453,19 @@
"theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen",
"translated_text_options": "Opties",
"trash_page_delete": "Verwijderen",
"trash_page_delete_all": "Verwijder Alle",
"trash_page_delete_all": "Verwijder alle",
"trash_page_empty_trash_btn": "Leeg prullenbak",
"trash_page_empty_trash_dialog_content": "Wil je je weggegooide assets leegmaken? Deze items worden permanent verwijderd van Immich",
"trash_page_empty_trash_dialog_content": "Wil je de prullenbak leegmaken? Deze items worden permanent verwijderd van Immich",
"trash_page_empty_trash_dialog_ok": "Ok",
"trash_page_info": "Verwijderde items worden permanent verwijderd na {} dagen",
"trash_page_no_assets": "Geen verwijderde assets",
"trash_page_restore": "Herstellen",
"trash_page_restore_all": "Herstel Alle",
"trash_page_restore_all": "Herstel alle",
"trash_page_select_assets_btn": "Selecteer assets",
"trash_page_select_btn": "Selecteren",
"trash_page_title": "Prullenbak ({})",
"upload_dialog_cancel": "Annuleren",
"upload_dialog_info": "Wilt u een backup maken van de geselecteerde Asset(s) op de server?",
"upload_dialog_info": "Wil je een backup maken van de geselecteerde asset(s) op de server?",
"upload_dialog_ok": "Uploaden",
"upload_dialog_title": "Asset uploaden",
"version_announcement_overlay_ack": "Bevestig",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "SZCZEGÓŁY",
"exif_bottom_sheet_location": "LOKALIZACJA",
"exif_bottom_sheet_location_add": "Dodaj lokalizację",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Praca w toku",
"experimental_settings_new_asset_list_title": "Włącz eksperymentalną układ zdjęć",
"experimental_settings_subtitle": "Używaj na własne ryzyko!",

View File

@@ -1,8 +1,8 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"add_to_album_bottom_sheet_added": "Added to {album}",
"add_to_album_bottom_sheet_already_exists": "Already in {album}",
"action_common_cancel": "Cancelar",
"action_common_update": "Atualizar",
"add_to_album_bottom_sheet_added": "Adicionar a {album}",
"add_to_album_bottom_sheet_already_exists": "Já pertence a {album}",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Some devices are painfully slow to load thumbnails from assets on the device. Activate this setting to load remote images instead.",
"advanced_settings_prefer_remote_title": "Prefer remote images",
@@ -16,7 +16,7 @@
"album_info_card_backup_album_included": "INCLUÍDO",
"album_thumbnail_card_item": "1 item",
"album_thumbnail_card_items": "{} itens",
"album_thumbnail_card_shared": "Compartilhado",
"album_thumbnail_card_shared": " · Partilhado",
"album_thumbnail_owned": "Owned",
"album_thumbnail_shared_by": "Shared by {}",
"album_viewer_appbar_share_delete": "Deletar álbum",
@@ -29,21 +29,21 @@
"album_viewer_appbar_share_to": "Share To",
"album_viewer_page_share_add_users": "Adicionar usuários",
"all_people_page_title": "People",
"all_videos_page_title": "Videos",
"app_bar_signout_dialog_content": "Are you sure you want to sign out?",
"all_videos_page_title": "Vídeos",
"app_bar_signout_dialog_content": "Tem a certeza que deseja sair?",
"app_bar_signout_dialog_ok": "Yes",
"app_bar_signout_dialog_title": "Sign out",
"app_bar_signout_dialog_title": "Sair",
"archive_page_no_archived_assets": "No archived assets found",
"archive_page_title": "Archive ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_list_layout_settings_dynamic_layout_title": "Dynamic layout",
"asset_action_delete_err_read_only": "Não é possível eliminar o(s) recurso(s) só de leitura, ignorando",
"asset_action_share_err_offline": "Não é possível obter recurso(s) offline, ignorando",
"asset_list_layout_settings_dynamic_layout_title": "Disposição dinâmica",
"asset_list_layout_settings_group_automatically": "Automatic",
"asset_list_layout_settings_group_by": "Group assets by",
"asset_list_layout_settings_group_by_month": "Month",
"asset_list_layout_settings_group_by_month_day": "Month + day",
"asset_list_settings_subtitle": "Configurações de layout da grade de fotos",
"asset_list_settings_title": "Grade de fotos",
"asset_list_layout_settings_group_by": "Agrupar recursos por",
"asset_list_layout_settings_group_by_month": "Mês",
"asset_list_layout_settings_group_by_month_day": "Mês + dia",
"asset_list_settings_subtitle": "Configurações de layout da grelha de fotos",
"asset_list_settings_title": "Grelha de fotos",
"backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})",
"backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para exluir",
"backup_album_selection_page_assets_scatter": "Os itens podem estar espalhados por vários álbuns. Assim, os álbuns podem ser incluídos ou excluídos durante o processo de backup.",
@@ -90,11 +90,11 @@
"backup_controller_page_remainder": "Restante",
"backup_controller_page_remainder_sub": "Fotos e vídeos restantes para fazer backup da seleção",
"backup_controller_page_select": "Selecione",
"backup_controller_page_server_storage": "Espaço no Servidor",
"backup_controller_page_server_storage": "Armazenamento no servidor",
"backup_controller_page_start_backup": "Iniciar Backup",
"backup_controller_page_status_off": "Backup está desligado",
"backup_controller_page_status_on": "Backup está ligado",
"backup_controller_page_storage_format": "{} de {} usado",
"backup_controller_page_storage_format": "{} de {} usados",
"backup_controller_page_to_backup": "Álbuns para fazer backup",
"backup_controller_page_total": "Total",
"backup_controller_page_total_sub": "Todas as fotos e vídeos dos álbuns selecionados",
@@ -118,91 +118,92 @@
"cache_settings_statistics_album": "Miniaturas da biblioteca",
"cache_settings_statistics_assets": "{} itens ({})",
"cache_settings_statistics_full": "Imagens completas",
"cache_settings_statistics_shared": "Miniaturas de álbuns compartilhados",
"cache_settings_statistics_shared": "Miniaturas de álbuns partilhados",
"cache_settings_statistics_thumbnail": "Miniaturas",
"cache_settings_statistics_title": "Uso de cache",
"cache_settings_subtitle": "Controle o comportamento de cache do aplicativo Immich",
"cache_settings_thumbnail_size": "Tamanho do cache de miniaturas ({} itens)",
"cache_settings_tile_subtitle": "Control the local storage behaviour",
"cache_settings_tile_title": "Local Storage",
"cache_settings_tile_subtitle": "Controlar o comportamento do armazenamento local",
"cache_settings_tile_title": "Armazenamento local",
"cache_settings_title": "Configurações de cache",
"change_password_form_confirm_password": "Confirm Password",
"change_password_form_description": "Hi {name},\n\nThis is either the first time you are signing into the system or a request has been made to change your password. Please enter the new password below.",
"change_password_form_new_password": "New Password",
"change_password_form_password_mismatch": "Passwords do not match",
"change_password_form_reenter_new_password": "Re-enter New Password",
"common_add_to_album": "Add to album",
"common_change_password": "Change Password",
"common_create_new_album": "Create new album",
"common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.",
"change_password_form_confirm_password": "Confirme a senha",
"change_password_form_description": "Olá {name},\n\nÉ a primeira vez que entra no sistema ou foi-lhe pedido que alterasse a sua palavra-passe. Introduza a nova palavra-passe abaixo.",
"change_password_form_new_password": "Nova senha",
"change_password_form_password_mismatch": "As senhas não coincidem",
"change_password_form_reenter_new_password": "Re-introduza a nova senha",
"common_add_to_album": "Adicionar ao álbum",
"common_change_password": "Mudar a senha",
"common_create_new_album": "Criar novo álbum",
"common_server_error": "Verifique a sua ligação de rede, certifique-se de que o servidor está acessível e de que as versões da aplicação/servidor são compatíveis.",
"common_shared": "Shared",
"control_bottom_app_bar_add_to_album": "Adicionar ao álbum",
"control_bottom_app_bar_album_info": "{} itens",
"control_bottom_app_bar_album_info_shared": "{} itens · Compartilhado",
"control_bottom_app_bar_album_info_shared": "{} itens · Partilhado",
"control_bottom_app_bar_archive": "Archive",
"control_bottom_app_bar_create_new_album": "Criar novo álbum",
"control_bottom_app_bar_delete": "Deletar",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_delete_from_immich": "Apagar do Immich",
"control_bottom_app_bar_delete_from_local": "Apagar do dispositivo",
"control_bottom_app_bar_edit_location": "Edit Location",
"control_bottom_app_bar_edit_time": "Edit Date & Time",
"control_bottom_app_bar_favorite": "Favorite",
"control_bottom_app_bar_share": "Compartilhar",
"control_bottom_app_bar_share": "Partilhar",
"control_bottom_app_bar_share_to": "Share To",
"control_bottom_app_bar_stack": "Stack",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Mover para o lixo",
"control_bottom_app_bar_unarchive": "Unarchive",
"control_bottom_app_bar_unfavorite": "Unfavorite",
"control_bottom_app_bar_upload": "Upload",
"create_album_page_untitled": "Sem título",
"create_shared_album_page_create": "Criar",
"create_shared_album_page_share": "Compartilhar",
"create_shared_album_page_share": "Partilhar",
"create_shared_album_page_share_add_assets": "ADICIONAR ITENS",
"create_shared_album_page_share_select_photos": "Selecionar Fotos",
"curated_location_page_title": "Places",
"curated_object_page_title": "Things",
"curated_location_page_title": "Sítios",
"curated_object_page_title": "Objetos",
"daily_title_text_date": "E, MMM dd",
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Esses itens serão permanentemente deletados do Immich e do seu dispositivo",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Estes itens serão removidos permanentemente do seu dispositivo, mas continuarão disponíveis no servidor Immich",
"delete_dialog_alert_local_non_backed_up": "Alguns dos itens não estão guardados no Immich e serão removidos permanentemente do seu dispositivo",
"delete_dialog_alert_remote": "Estes itens serão permanentemente eliminados do servidor Immich",
"delete_dialog_cancel": "Cancelar",
"delete_dialog_ok": "Deletar",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "Apagar de qualquer forma",
"delete_dialog_title": "Deletar Permanentemente",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Eliminar apenas existentes na cópia de segurança",
"delete_local_dialog_ok_force": "Apagar de qualquer forma",
"delete_shared_link_dialog_content": "Are you sure you want to delete this shared link?",
"delete_shared_link_dialog_title": "Delete Shared Link",
"description_input_hint_text": "Add description...",
"description_input_submit_error": "Error updating description, check the log for more details",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_date_time_dialog_date_time": "Data e Hora",
"edit_date_time_dialog_timezone": "Fuso horário",
"edit_location_dialog_title": "Location",
"exif_bottom_sheet_description": "Adicionar Descrição...",
"exif_bottom_sheet_details": "DETALHES",
"exif_bottom_sheet_location": "LOCALIZAÇÃO",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Trabalho em andamento",
"experimental_settings_new_asset_list_title": "Ativar visualização de grade experimental",
"experimental_settings_new_asset_list_title": "Ativar visualização de grelha experimental",
"experimental_settings_subtitle": "Use por sua conta e risco!",
"experimental_settings_title": "Experimental",
"favorites_page_no_favorites": "No favorite assets found",
"favorites_page_title": "Favorites",
"favorites_page_title": "Favoritos",
"home_page_add_to_album_conflicts": "Ativos {added} adicionados ao álbum {album}. {failed} ativos já estão no álbum.",
"home_page_add_to_album_err_local": "Can not add local assets to albums yet, skipping",
"home_page_add_to_album_err_local": "Ainda não é possível adicionar recursos locais aos álbuns, ignorando",
"home_page_add_to_album_success": "Ativos {added} adicionados ao álbum {album}.",
"home_page_album_err_partner": "Can not add partner assets to an album yet, skipping",
"home_page_archive_err_local": "Can not archive local assets yet, skipping",
"home_page_archive_err_partner": "Can not archive partner assets, skipping",
"home_page_building_timeline": "Building the timeline",
"home_page_building_timeline": "A construir a timeline",
"home_page_delete_err_partner": "Can not delete partner assets, skipping",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_favorite_err_local": "Can not favorite local assets yet, skipping",
"home_page_delete_remote_err_local": "Recursos locais na seleção remota de eliminação, ignorando",
"home_page_favorite_err_local": "Ainda não é possível adicionar recursos locais favoritos, ignorando",
"home_page_favorite_err_partner": "Can not favorite partner assets yet, skipping",
"home_page_first_time_notice": "If this is your first time using the app, please make sure to choose a backup album(s) so that the timeline can populate photos and videos in the album(s).",
"home_page_first_time_notice": "Se for a primeira vez que utiliza a aplicação, certifique-se de que escolhe um álbum ou álbuns de cópia de segurança, para que a linha cronológica possa preencher as fotografias e os vídeos no(s) álbum(s).",
"home_page_share_err_local": "Can not share local assets via link, skipping",
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
"image_viewer_page_state_provider_download_error": "Download Error",
@@ -210,16 +211,16 @@
"image_viewer_page_state_provider_share_error": "Share Error",
"library_page_albums": "Álbuns",
"library_page_archive": "Archive",
"library_page_device_albums": "Albums on Device",
"library_page_favorites": "Favorites",
"library_page_new_album": "Novo Album",
"library_page_sharing": "Sharing",
"library_page_sort_asset_count": "Number of assets",
"library_page_sort_created": "Created date",
"library_page_device_albums": "Álbuns no dispositivo",
"library_page_favorites": "Favoritos",
"library_page_new_album": "Novo álbum",
"library_page_sharing": "Partilhar",
"library_page_sort_asset_count": "Número de recursos",
"library_page_sort_created": "Data de criação",
"library_page_sort_last_modified": "Last modified",
"library_page_sort_most_oldest_photo": "Oldest photo",
"library_page_sort_most_oldest_photo": "Foto mais antiga",
"library_page_sort_most_recent_photo": "Most recent photo",
"library_page_sort_title": "Album title",
"library_page_sort_title": "Título do álbum",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Enter a valid latitude",
@@ -228,8 +229,8 @@
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"login_disabled": "Login has been disabled",
"login_form_api_exception": "API exception. Please check the server URL and try again.",
"login_form_back_button_text": "Back",
"login_form_api_exception": "Excepção de API. Verifique o URL do servidor e tente novamente.",
"login_form_back_button_text": "Voltar",
"login_form_button_text": "Login",
"login_form_email_hint": "seuemail@email.com",
"login_form_endpoint_hint": "http://ip-do-seu-servidor:porta/api",
@@ -245,15 +246,15 @@
"login_form_handshake_exception": "There was an Handshake Exception with the server. Enable self-signed certificate support in the settings if you are using a self-signed certificate.",
"login_form_label_email": "Email",
"login_form_label_password": "Senha",
"login_form_next_button": "Next",
"login_form_next_button": "Avançar",
"login_form_password_hint": "senha",
"login_form_save_login": "Permanecer logado",
"login_form_server_empty": "Enter a server URL.",
"login_form_server_error": "Could not connect to server.",
"login_form_server_empty": "Introduzir um URL de servidor.",
"login_form_server_error": "Não foi possível ligar ao servidor.",
"login_password_changed_error": "There was an error updating your password",
"login_password_changed_success": "Password updated successfully",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_assets_in_bounds": "{} fotos",
"map_cannot_get_user_location": "Cannot get user's location",
"map_location_dialog_cancel": "Cancel",
"map_location_dialog_yes": "Yes",
@@ -264,83 +265,83 @@
"map_no_location_permission_content": "Location permission is needed to display assets from your current location. Do you want to allow it now?",
"map_no_location_permission_title": "Location Permission denied",
"map_settings_dark_mode": "Dark mode",
"map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_date_range_option_all": "Tudo",
"map_settings_date_range_option_day": "Últimas 24 horas",
"map_settings_date_range_option_days": "Últimos {} dias",
"map_settings_date_range_option_year": "Último ano",
"map_settings_date_range_option_years": "Últimos {} anos",
"map_settings_dialog_cancel": "Cancel",
"map_settings_dialog_save": "Save",
"map_settings_dialog_title": "Map Settings",
"map_settings_include_show_archived": "Include Archived",
"map_settings_only_relative_range": "Date range",
"map_settings_only_show_favorites": "Show Favorite Only",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Tema do mapa",
"map_zoom_to_see_photos": "Zoom out to see photos",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Motion Photos",
"motion_photos_page_title": "Fotos com movimento",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"notification_permission_dialog_cancel": "Cancel",
"notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.",
"notification_permission_dialog_cancel": "Cancelar",
"notification_permission_dialog_content": "Para ativar as notificações, vá a Definições e selecione permitir.",
"notification_permission_dialog_settings": "Settings",
"notification_permission_list_tile_content": "Grant permission to enable notifications.",
"notification_permission_list_tile_enable_button": "Enable Notifications",
"notification_permission_list_tile_title": "Notification Permission",
"partner_page_add_partner": "Add partner",
"notification_permission_list_tile_title": "Permissão de notificações",
"partner_page_add_partner": "Adicionar parceiro",
"partner_page_empty_message": "Your photos are not yet shared with any partner.",
"partner_page_no_more_users": "No more users to add",
"partner_page_partner_add_failed": "Failed to add partner",
"partner_page_select_partner": "Select partner",
"partner_page_select_partner": "Selecionar parceiro",
"partner_page_shared_to_title": "Shared to",
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
"partner_page_stop_sharing_title": "Stop sharing your photos?",
"partner_page_title": "Partner",
"permission_onboarding_back": "Back",
"permission_onboarding_continue_anyway": "Continue anyway",
"permission_onboarding_continue_anyway": "Continuar de qualquer maneira",
"permission_onboarding_get_started": "Get started",
"permission_onboarding_go_to_settings": "Go to settings",
"permission_onboarding_grant_permission": "Grant permission",
"permission_onboarding_log_out": "Log out",
"permission_onboarding_permission_denied": "Permission denied. To use Immich, grant photo and video permissions in Settings.",
"permission_onboarding_permission_granted": "Permission granted! You are all set.",
"permission_onboarding_permission_limited": "Permission limited. To let Immich backup and manage your entire gallery collection, grant photo and video permissions in Settings.",
"permission_onboarding_request": "Immich requires permission to view your photos and videos.",
"permission_onboarding_log_out": "Sair",
"permission_onboarding_permission_denied": "Permissão negada. Para utilizar o Immich, conceda permissões de fotografia e vídeo nas Definições.",
"permission_onboarding_permission_granted": "Autorização concedida! Está tudo pronto.",
"permission_onboarding_permission_limited": "Permissão limitada. Para permitir que o Immich faça cópias de segurança e gira toda a sua coleção de galerias, conceda permissões para fotografias e vídeos nas Definições.",
"permission_onboarding_request": "O Immich requer autorização para ver as suas fotografias e vídeos.",
"profile_drawer_app_logs": "Logs",
"profile_drawer_client_out_of_date_major": "Mobile App is out of date. Please update to the latest major version.",
"profile_drawer_client_out_of_date_minor": "Mobile App is out of date. Please update to the latest minor version.",
"profile_drawer_client_out_of_date_major": "A aplicação móvel está desatualizada. Atualize para a versão principal mais recente.",
"profile_drawer_client_out_of_date_minor": "A aplicação móvel está desatualizada. Por favor, atualize para a versão mais recente.",
"profile_drawer_client_server_up_to_date": "Cliente e Servidor atualizados",
"profile_drawer_documentation": "Documentation",
"profile_drawer_github": "GitHub",
"profile_drawer_server_out_of_date_major": "Server is out of date. Please update to the latest major version.",
"profile_drawer_server_out_of_date_minor": "Server is out of date. Please update to the latest minor version.",
"profile_drawer_server_out_of_date_major": "O servidor está desatualizado. Atualize para a versão principal mais recente.",
"profile_drawer_server_out_of_date_minor": "O servidor está desatualizado. Atualize para a versão mais recente.",
"profile_drawer_settings": "Configurações",
"profile_drawer_sign_out": "Sair",
"profile_drawer_trash": "Trash",
"recently_added_page_title": "Recently Added",
"scaffold_body_error_occurred": "Error occurred",
"recently_added_page_title": "Adicionado recentemente",
"scaffold_body_error_occurred": "Ocorreu um erro",
"search_bar_hint": "Busque suas fotos",
"search_page_categories": "Categories",
"search_page_favorites": "Favorites",
"search_page_motion_photos": "Motion Photos",
"search_page_favorites": "Favoritos",
"search_page_motion_photos": "Fotos com movimento",
"search_page_no_objects": "Nenhuma informação de objeto disponível",
"search_page_no_places": "Nenhuma informação de lugares disponível",
"search_page_no_places": "Nenhuma informação de sítios disponível",
"search_page_people": "People",
"search_page_person_add_name_dialog_cancel": "Cancel",
"search_page_person_add_name_dialog_hint": "Name",
"search_page_person_add_name_dialog_save": "Save",
"search_page_person_add_name_dialog_title": "Add a name",
"search_page_person_add_name_subtitle": "Find them fast by name with search",
"search_page_person_add_name_title": "Add a name",
"search_page_person_edit_name": "Edit name",
"search_page_places": "Lugares",
"search_page_recently_added": "Recently added",
"search_page_person_add_name_dialog_cancel": "Cancelar",
"search_page_person_add_name_dialog_hint": "Nome",
"search_page_person_add_name_dialog_save": "Guardar",
"search_page_person_add_name_dialog_title": "Adicionar um nome",
"search_page_person_add_name_subtitle": "Encontre-os rapidamente pelo nome com a pesquisa",
"search_page_person_add_name_title": "Adicionar um nome",
"search_page_person_edit_name": "Editar nome",
"search_page_places": "Sítios",
"search_page_recently_added": "Adicionado recentemente",
"search_page_screenshots": "Screenshots",
"search_page_selfies": "Selfies",
"search_page_things": "Objetos",
"search_page_videos": "Videos",
"search_page_view_all_button": "View all",
"search_page_your_activity": "Your activity",
"search_page_videos": "Vídeos",
"search_page_view_all_button": "Ver tudo",
"search_page_your_activity": "A sua atividade",
"search_page_your_map": "Your Map",
"search_result_page_new_search_hint": "Nova Busca",
"search_suggestion_list_smart_search_hint_1": "Smart search is enabled by default, to search for metadata use the syntax ",
@@ -348,10 +349,10 @@
"select_additional_user_for_sharing_page_suggestions": "Sugestões",
"select_user_for_sharing_page_err_album": "Falha ao criar o álbum",
"select_user_for_sharing_page_share_suggestions": "Sugestões",
"server_info_box_app_version": "App Version",
"server_info_box_latest_release": "Latest Version",
"server_info_box_app_version": "Versão da app",
"server_info_box_latest_release": "Última versão",
"server_info_box_server_url": "Server URL",
"server_info_box_server_version": "Server Version",
"server_info_box_server_version": "Versão do servidor",
"setting_image_viewer_help": "O visualizador de detalhes carrega primeiro a miniatura pequena, depois carrega a visualização de tamanho médio (se ativado) e, finalmente, carrega o original (se ativado).",
"setting_image_viewer_original_subtitle": "Ative para carregar a imagem original em resolução total (grande!). Desative para reduzir o uso de dados (na rede e no cache do dispositivo).",
"setting_image_viewer_original_title": "Carregar imagem original",
@@ -381,65 +382,65 @@
"shared_album_activity_remove_title": "Delete Activity",
"shared_album_activity_setting_subtitle": "Let others respond",
"shared_album_activity_setting_title": "Comments & likes",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_leave": "Remove user from album",
"shared_album_section_people_action_remove_user": "Remove user from album",
"shared_album_section_people_action_error": "Erro ao sair/remover do álbum",
"shared_album_section_people_action_leave": "Remover utilizador do álbum",
"shared_album_section_people_action_remove_user": "Remover utilizador do álbum",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"share_dialog_preparing": "Preparando...",
"shared_link_app_bar_title": "Shared Links",
"shared_link_app_bar_title": "Links partilhados",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Create link to share",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Let anyone with the link see the selected photo(s)",
"shared_link_create_submit_button": "Create link",
"shared_link_edit_allow_download": "Allow public user to download",
"shared_link_edit_allow_upload": "Allow public user to upload",
"shared_link_edit_allow_download": "Permitir que um utilizador público descarregue",
"shared_link_edit_allow_upload": "Permitir que um utilizador público carregue",
"shared_link_edit_app_bar_title": "Edit link",
"shared_link_edit_change_expiry": "Change expiration time",
"shared_link_edit_change_expiry": "Alterar o prazo de validade",
"shared_link_edit_description": "Description",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_expire_after": "Expire after",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_expire_after_option_day": "1 dia",
"shared_link_edit_expire_after_option_days": "{} dias",
"shared_link_edit_expire_after_option_hour": "1 hora",
"shared_link_edit_expire_after_option_hours": "{} horas",
"shared_link_edit_expire_after_option_minute": "1 minuto",
"shared_link_edit_expire_after_option_minutes": "{} minutos",
"shared_link_edit_expire_after_option_never": "Never",
"shared_link_edit_password": "Password",
"shared_link_edit_password_hint": "Enter the share password",
"shared_link_edit_show_meta": "Show metadata",
"shared_link_edit_submit_button": "Update link",
"shared_link_empty": "You don't have any shared links",
"shared_link_edit_show_meta": "Mostrar metadados",
"shared_link_edit_submit_button": "Atualizar link",
"shared_link_empty": "Não tem links partilhados",
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Expired",
"shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute",
"shared_link_expires_day": "Expira em {} dia",
"shared_link_expires_days": "Expira em {} dias",
"shared_link_expires_hour": "Expira em {} hora",
"shared_link_expires_hours": "Expira em {} horas",
"shared_link_expires_minute": "Expira em {} minuto",
"shared_link_expires_minutes": "Expires in {} minutes",
"shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_second": "Expira em {} segundo",
"shared_link_expires_seconds": "Expires in {} seconds",
"shared_link_info_chip_download": "Download",
"shared_link_info_chip_download": "Descarregar",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload",
"shared_link_manage_links": "Manage Shared links",
"shared_link_manage_links": "Gerir links partilhados",
"share_done": "Done",
"share_invite": "Convidar para álbum",
"sharing_page_album": "Álbuns compartilhados",
"sharing_page_description": "Criar álbuns compartilhados para compartilhas fotos e vídeos com pessoas na sua rede.",
"sharing_page_album": "Álbuns partilhados",
"sharing_page_description": "Crie álbuns partilhados para partilhar fotografias e vídeos com pessoas da sua rede.",
"sharing_page_empty_list": "LISTA VAZIA",
"sharing_silver_appbar_create_shared_album": "Criar um álgum compartilhado",
"sharing_silver_appbar_create_shared_album": "Criar álbum partilhado",
"sharing_silver_appbar_shared_links": "Shared links",
"sharing_silver_appbar_share_partner": "Compartilhar com parceiro",
"sharing_silver_appbar_share_partner": "Partilhar com parceiro",
"tab_controller_nav_library": "Biblioteca",
"tab_controller_nav_photos": "Fotos",
"tab_controller_nav_search": "Buscar",
"tab_controller_nav_sharing": "Compartilhando",
"tab_controller_nav_search": "Procurar",
"tab_controller_nav_sharing": "Partilhar",
"theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento em blocos de ativos",
"theme_setting_asset_list_tiles_per_row_title": "Número de itens por linha ({})",
"theme_setting_dark_mode_switch": "Modo escuro",
@@ -467,11 +468,11 @@
"upload_dialog_info": "Do you want to backup the selected Asset(s) to the server?",
"upload_dialog_ok": "Upload",
"upload_dialog_title": "Upload Asset",
"version_announcement_overlay_ack": "Need Context",
"version_announcement_overlay_ack": "Aceitar",
"version_announcement_overlay_release_notes": "notas de lançamento",
"version_announcement_overlay_text_1": "Olá, há um novo lançamento de",
"version_announcement_overlay_text_2": "por favor, tome o seu tempo para visitar o",
"version_announcement_overlay_text_3": "e certifique-se de que a configuração do docker-compose e do .env estejam atualizadas para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo do servidor.",
"version_announcement_overlay_text_3": "e certifique-se de que a configuração do docker-compose e do .env estejam atualizadas para evitar configurações incorretas, especialmente se usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do servidor.",
"version_announcement_overlay_title": "Nova versão do servidor disponível \uD83C\uDF89",
"viewer_remove_from_stack": "Remove from Stack",
"viewer_stack_use_as_main_asset": "Use as Main Asset",

View File

@@ -1,12 +1,12 @@
{
"action_common_cancel": "Cancel",
"action_common_update": "Update",
"action_common_cancel": "Отмена",
"action_common_update": "Обновить",
"add_to_album_bottom_sheet_added": "Добавлено в {album}",
"add_to_album_bottom_sheet_already_exists": "Уже в {album}",
"advanced_settings_log_level_title": "Log level: {}",
"advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают предпросмотр объектов, находящихся на устройстве. Активируйте эту настройку, чтобы вместо них загружались изображени с сервера.",
"advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают предпросмотр объектов, находящихся на устройстве. Активируйте эту настройку, чтобы вместо них загружались изображения с сервера.",
"advanced_settings_prefer_remote_title": "Предпочитать фото на сервере",
"advanced_settings_self_signed_ssl_subtitle": "Пропускает проверку сертификата SSL для конечной точки сервера. Требуется для самоподписанных сертификатов.",
"advanced_settings_self_signed_ssl_subtitle": "Пропускает проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.",
"advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты",
"advanced_settings_tile_subtitle": "Расширенные настройки пользователя",
"advanced_settings_tile_title": "Расширенные",
@@ -35,14 +35,14 @@
"app_bar_signout_dialog_title": "Выйти из системы",
"archive_page_no_archived_assets": "В архиве сейчас пусто",
"archive_page_title": "Архив ({})",
"asset_action_delete_err_read_only": "Cannot delete read only asset(s), skipping",
"asset_action_share_err_offline": "Cannot fetch offline asset(s), skipping",
"asset_action_delete_err_read_only": "Невозможно удалить объект(ы) только для чтения, пропуск...",
"asset_action_share_err_offline": "Невозможно получить оффлайн-объект(ы), пропуск...",
"asset_list_layout_settings_dynamic_layout_title": "Динамическое расположение",
"asset_list_layout_settings_group_automatically": "Автоматически",
"asset_list_layout_settings_group_by": "Группировать объекты по:",
"asset_list_layout_settings_group_by_month": "Месяцу",
"asset_list_layout_settings_group_by_month_day": "Месяцу и дню",
"asset_list_settings_subtitle": "Настройки макета сетки фотографий",
"asset_list_settings_subtitle": "Настройка макета сетки фотографий",
"asset_list_settings_title": "Сетка фотографий",
"backup_album_selection_page_albums_device": "Альбомов на устройстве ({})",
"backup_album_selection_page_albums_tap": "Нажмите, чтобы включить, нажмите дважды, чтобы исключить",
@@ -65,7 +65,7 @@
"backup_controller_page_background_battery_info_link": "Показать как",
"backup_controller_page_background_battery_info_message": "Для наилучшего фонового резервного копирования отключите любые настройки оптимизации батареи, ограничивающие фоновую активность для Immich.\n\nПоскольку это зависит от устройства, найдите необходимую информацию для производителя вашего устройства.",
"backup_controller_page_background_battery_info_ok": "ОК",
"backup_controller_page_background_battery_info_title": "\nОптимизация батареи",
"backup_controller_page_background_battery_info_title": "Оптимизация батареи",
"backup_controller_page_background_charging": "Только во время зарядки",
"backup_controller_page_background_configure_error": "Не удалось настроить фоновую службу",
"backup_controller_page_background_delay": "Отложить резервное копирование новых объектов: {}",
@@ -112,8 +112,8 @@
"cache_settings_clear_cache_button": "Очистить кэш",
"cache_settings_clear_cache_button_title": "Очищает кэш приложения. Это значительно повлияет на производительность приложения, до тех пор, пока кэш не будет перестроен заново.",
"cache_settings_duplicated_assets_clear_button": "ОЧИСТИТЬ",
"cache_settings_duplicated_assets_subtitle": "Photos and videos that are black listed by the app",
"cache_settings_duplicated_assets_title": "Дублированные ресурсы",
"cache_settings_duplicated_assets_subtitle": "Фото и видео, занесенные приложением в черный список",
"cache_settings_duplicated_assets_title": "Дублирующиеся объекты ({})",
"cache_settings_image_cache_size": "Размер кэша изображений ({} объектов)",
"cache_settings_statistics_album": "Миниатюры библиотеки",
"cache_settings_statistics_assets": "{} объектов ({})",
@@ -140,19 +140,19 @@
"control_bottom_app_bar_album_info": "{} файлов",
"control_bottom_app_bar_album_info_shared": "{} файлов · Общий",
"control_bottom_app_bar_archive": "Архив",
"control_bottom_app_bar_create_new_album": "\nСоздать новый альбом",
"control_bottom_app_bar_create_new_album": "Создать новый альбом",
"control_bottom_app_bar_delete": "Удалить",
"control_bottom_app_bar_delete_from_immich": "Delete from Immich",
"control_bottom_app_bar_delete_from_local": "Delete from device",
"control_bottom_app_bar_edit_location": "Изменить местоположение",
"control_bottom_app_bar_edit_time": "Изменить дату и время",
"control_bottom_app_bar_favorite": "Избранное",
"control_bottom_app_bar_delete_from_immich": "Удалить из Immich\n",
"control_bottom_app_bar_delete_from_local": "Удалить с устройства",
"control_bottom_app_bar_edit_location": "Редактировать местоположение",
"control_bottom_app_bar_edit_time": "Редактировать дату и время",
"control_bottom_app_bar_favorite": "В избранное",
"control_bottom_app_bar_share": "Поделиться",
"control_bottom_app_bar_share_to": "Поделиться",
"control_bottom_app_bar_stack": "Стек",
"control_bottom_app_bar_trash_from_immich": "Move to Trash",
"control_bottom_app_bar_trash_from_immich": "Переместить в корзину",
"control_bottom_app_bar_unarchive": "Восстановить",
"control_bottom_app_bar_unfavorite": "Исключить из избранного",
"control_bottom_app_bar_unfavorite": "Удалить из избранного",
"control_bottom_app_bar_upload": "Загрузить",
"create_album_page_untitled": "Без названия",
"create_shared_album_page_create": "Создать",
@@ -165,26 +165,27 @@
"daily_title_text_date_year": "E, MMM dd, yyyy",
"date_format": "E, LLL d, y • h:mm a",
"delete_dialog_alert": "Эти элементы будут безвозвратно удалены из приложения, а также с вашего устройства",
"delete_dialog_alert_local": "These items will be permanently removed from your device but still be available on the Immich server",
"delete_dialog_alert_local_non_backed_up": "Some of the items aren't backed up to Immich and will be permanently removed from your device",
"delete_dialog_alert_remote": "These items will be permanently deleted from the Immich server",
"delete_dialog_alert_local": "Эти объекты будут безвозвратно удалены с Вашего устройства, но по-прежнему будут доступны на сервере Immich",
"delete_dialog_alert_local_non_backed_up": "Резервные копии некоторых объектов не были загружены в Immich и будут безвозвратно удалены с Вашего устройства",
"delete_dialog_alert_remote": "Эти объекты будут безвозвратно удалены с сервера Immich",
"delete_dialog_cancel": "Отменить",
"delete_dialog_ok": "Удалить",
"delete_dialog_ok_force": "Delete Anyway",
"delete_dialog_ok_force": "Все равно удалить",
"delete_dialog_title": "Удалить навсегда",
"delete_local_dialog_ok_backed_up_only": "Delete Backed Up Only",
"delete_local_dialog_ok_force": "Delete Anyway",
"delete_local_dialog_ok_backed_up_only": "Удалить только резервные копии",
"delete_local_dialog_ok_force": "Все равно удалить",
"delete_shared_link_dialog_content": "Вы уверены, что хотите удалить эту общую ссылку?",
"delete_shared_link_dialog_title": "Удалить общую ссылку",
"description_input_hint_text": "Добавить описание...",
"description_input_submit_error": "Не удалось обновить описание, проверьте логи, чтобы узнать причину",
"edit_date_time_dialog_date_time": "Date and Time",
"edit_date_time_dialog_timezone": "Timezone",
"edit_location_dialog_title": "Location",
"edit_date_time_dialog_date_time": "Дата и время",
"edit_date_time_dialog_timezone": "Часовой пояс",
"edit_location_dialog_title": "Местоположение",
"exif_bottom_sheet_description": "Добавить описание...",
"exif_bottom_sheet_details": "ПОДРОБНОСТИ",
"exif_bottom_sheet_location": "МЕСТОПОЛОЖЕНИЕ",
"exif_bottom_sheet_location": "Местоположение",
"exif_bottom_sheet_location_add": "Добавить местоположение",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Ведутся работы",
"experimental_settings_new_asset_list_title": "Включить экспериментальную сетку фотографий",
"experimental_settings_subtitle": "Используйте на свой страх и риск!",
@@ -194,39 +195,39 @@
"home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.",
"home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропускаем",
"home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.",
"home_page_album_err_partner": "Пока не удается добавить партнерские активы в альбом, пропуск...",
"home_page_album_err_partner": "Пока не удается добавить объекты партнера в альбом, пропуск...",
"home_page_archive_err_local": "Пока невозможно добавить локальные объекты в архив, пропускаем",
"home_page_archive_err_partner": "Невозможно архивировать активы партнеров, пропуск...",
"home_page_archive_err_partner": "Невозможно архивировать объекты партнера, пропуск...",
"home_page_building_timeline": "Построение временной шкалы",
"home_page_delete_err_partner": "Невозможно удалить активы партнера, пропуск...",
"home_page_delete_remote_err_local": "Local assets in delete remote selection, skipping",
"home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропускаем",
"home_page_favorite_err_partner": "Пока не удается выделить партнерские активы, пропуск...",
"home_page_delete_err_partner": "Невозможно удалить объекты партнера, пропуск...",
"home_page_delete_remote_err_local": "Локальные объект(ы) уже в процессе удаления с сервера, пропуск...",
"home_page_favorite_err_local": "Пока не удается добавить в избранное локальные объекты, пропуск...",
"home_page_favorite_err_partner": "Пока не удается добавить в избранное объекты партнера, пропуск...",
"home_page_first_time_notice": "Если вы используете приложение впервые, убедитесь, что вы выбрали резервный(е) альбом(ы), чтобы временная шкала могла заполнить фотографии и видео в альбоме(ах).",
"home_page_share_err_local": "Невозможно поделиться локальными данными по ссылке, пропуск...",
"home_page_upload_err_limit": "Вы можете выгрузить максимум 30 файлов за раз",
"image_viewer_page_state_provider_download_error": "Ошибка загрузки",
"image_viewer_page_state_provider_download_success": "Успешно загружено",
"image_viewer_page_state_provider_share_error": "Ошибка при публикации",
"image_viewer_page_state_provider_share_error": "Ошибка общего доступа",
"library_page_albums": "Альбомы",
"library_page_archive": "Архив",
"library_page_device_albums": "Альбомы на устройстве",
"library_page_favorites": "Избранное",
"library_page_new_album": "Новый альбом",
"library_page_sharing": "Общие",
"library_page_sort_asset_count": "Number of assets",
"library_page_sort_created": "По новизне",
"library_page_sort_asset_count": "Количество объектов",
"library_page_sort_created": "Недавно созданные",
"library_page_sort_last_modified": "Последнее изменение",
"library_page_sort_most_oldest_photo": "Oldest photo",
"library_page_sort_most_recent_photo": "Последняя фотография",
"library_page_sort_title": "По названию альбома",
"location_picker_choose_on_map": "Choose on map",
"location_picker_latitude": "Latitude",
"location_picker_latitude_error": "Enter a valid latitude",
"location_picker_latitude_hint": "Enter your latitude here",
"location_picker_longitude": "Longitude",
"location_picker_longitude_error": "Enter a valid longitude",
"location_picker_longitude_hint": "Enter your longitude here",
"library_page_sort_most_oldest_photo": "Самые старые фото",
"library_page_sort_most_recent_photo": "Самые последние фото",
"library_page_sort_title": "Название альбома",
"location_picker_choose_on_map": "Выбрать на карте",
"location_picker_latitude": "Широта",
"location_picker_latitude_error": "Укажите правильную широту",
"location_picker_latitude_hint": "Укажите широту",
"location_picker_longitude": "Долгота",
"location_picker_longitude_error": "Укажите правильную долготу",
"location_picker_longitude_hint": "Укажите долготу",
"login_disabled": "Вход отключен",
"login_form_api_exception": "Ошибка при попытке взаимодействия с сервером. Проверьте URL-адрес до него и попробуйте еще раз.",
"login_form_back_button_text": "Назад",
@@ -252,35 +253,35 @@
"login_form_server_error": "Нет соединения с сервером.",
"login_password_changed_error": "Произошла ошибка при обновлении пароля",
"login_password_changed_success": "Пароль успешно обновлен",
"map_assets_in_bound": "{} photo",
"map_assets_in_bounds": "{} photos",
"map_assets_in_bound": "{} фото",
"map_assets_in_bounds": "{} фото",
"map_cannot_get_user_location": "Невозможно получить местоположение пользователя",
"map_location_dialog_cancel": "Отмена",
"map_location_dialog_yes": "Да",
"map_location_picker_page_use_location": "Use this location",
"map_location_picker_page_use_location": "Это местоположение",
"map_location_service_disabled_content": "Для отображения объектов в данном месте необходимо включить службу определения местоположения. Хотите включить ее сейчас?",
"map_location_service_disabled_title": "Служба определения местоположения отключена",
"map_no_assets_in_bounds": "Нет фотографий в этой области",
"map_no_location_permission_content": "Для отображения объектов из текущего местоположения необходимо разрешение на определение местоположения. Хотите ли вы разрешить его сейчас?",
"map_no_location_permission_title": "Доступ к местоположению отклонен",
"map_settings_dark_mode": "Темный режим",
"map_settings_date_range_option_all": "All",
"map_settings_date_range_option_day": "Past 24 hours",
"map_settings_date_range_option_days": "Past {} days",
"map_settings_date_range_option_year": "Past year",
"map_settings_date_range_option_years": "Past {} years",
"map_settings_date_range_option_all": "Все",
"map_settings_date_range_option_day": "Прошлые 24 часа",
"map_settings_date_range_option_days": "Прошлые {} дней",
"map_settings_date_range_option_year": "Прошлый год",
"map_settings_date_range_option_years": "Прошлые {} года",
"map_settings_dialog_cancel": "Отмена",
"map_settings_dialog_save": "Сохранить",
"map_settings_dialog_title": "Настройки карты",
"map_settings_include_show_archived": "Включить архивные данные",
"map_settings_only_relative_range": "Период времени",
"map_settings_only_show_favorites": "Показать только избранное",
"map_settings_theme_settings": "Map Theme",
"map_settings_theme_settings": "Тема карты",
"map_zoom_to_see_photos": "Уменьшение масштаба для просмотра фотографий",
"monthly_title_text_date_format": "MMMM y",
"motion_photos_page_title": "Динамические фото",
"multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping",
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
"multiselect_grid_edit_date_time_err_read_only": "Невозможно редактировать дату объектов только для чтения, пропуск...",
"multiselect_grid_edit_gps_err_read_only": "Невозможно редактировать местоположение объектов только для чтения, пропуск...",
"notification_permission_dialog_cancel": "Отмена",
"notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».",
"notification_permission_dialog_settings": "Настройки",
@@ -318,7 +319,7 @@
"profile_drawer_sign_out": "Выйти",
"profile_drawer_trash": "Корзина",
"recently_added_page_title": "Недавно добавленные",
"scaffold_body_error_occurred": "Error occurred",
"scaffold_body_error_occurred": "Возникла ошибка",
"search_bar_hint": "Поиск фотографий",
"search_page_categories": "Категории",
"search_page_favorites": "Избранное",
@@ -332,31 +333,31 @@
"search_page_person_add_name_dialog_title": "Добавить имя",
"search_page_person_add_name_subtitle": "Быстро найдите их по имени с помощью поиска",
"search_page_person_add_name_title": "Добавить имя",
"search_page_person_edit_name": "Изменить имя",
"search_page_person_edit_name": "Редактировать имя",
"search_page_places": "Места",
"search_page_recently_added": "Недавно добавленные",
"search_page_screenshots": "Скриншоты",
"search_page_screenshots": "Снимки экрана",
"search_page_selfies": "Селфи",
"search_page_things": "Предметы",
"search_page_videos": "Видео",
"search_page_view_all_button": "Посмотреть все",
"search_page_your_activity": "Ваша активность",
"search_page_your_activity": "Ваши действия",
"search_page_your_map": "Ваша карта",
"search_result_page_new_search_hint": "Новый поиск",
"search_suggestion_list_smart_search_hint_1": "Интеллектуальный поиск включен по умолчанию, для поиска метаданных используйте специальный синтаксис",
"search_suggestion_list_smart_search_hint_2": "m:ваш-запрос",
"search_suggestion_list_smart_search_hint_2": "m:ваш-поисковый-запрос",
"select_additional_user_for_sharing_page_suggestions": "Предложения",
"select_user_for_sharing_page_err_album": "\nНе удалось создать альбом",
"select_user_for_sharing_page_err_album": "Не удалось создать альбом",
"select_user_for_sharing_page_share_suggestions": "Предложения",
"server_info_box_app_version": "Версия приложения",
"server_info_box_latest_release": "Крайняя версия",
"server_info_box_latest_release": "Последняя версия",
"server_info_box_server_url": "URL сервера",
"server_info_box_server_version": "Версия сервера",
"setting_image_viewer_help": "Средство просмотра деталей сначала загружает маленькую миниатюру, затем загружает предварительный просмотр среднего размера (если включено) и, наконец, загружает оригинал (если включено).",
"setting_image_viewer_original_subtitle": "Включите загрузку оригинального изображения в полном разрешении (большое!). Отключите, чтобы уменьшить объем данных (как в сети, так и в кеше устройства).",
"setting_image_viewer_original_title": "Загрузить исходное изображение",
"setting_image_viewer_preview_subtitle": "Включите загрузку изображения среднего разрешения. Отключите, чтобы загрузить оригинал напрямую или использовать только миниатюру.",
"setting_image_viewer_preview_title": "Загрузить изображение для предварительного просмотра",
"setting_image_viewer_help": "Полноэкранный просмотрщик сначала загружает изображение для предпросмотра в низком разрешении, затем загружает изображение в уменьшенном разрешении относительно оригинала (если включено) и в конце концов загружает оригинал (если включено).",
"setting_image_viewer_original_subtitle": "Включите для загрузки исходного изображения в полном разрешении (большое!).\nОтключите, чтобы уменьшить объем данных (как сети, так и кэша устройства).",
"setting_image_viewer_original_title": "Загружать исходное изображение",
"setting_image_viewer_preview_subtitle": "Включите для загрузки изображения среднего разрешения.\nОтключите, чтобы загружать оригинал напрямую или использовать только миниатюру.",
"setting_image_viewer_preview_title": "Загружать изображение для предварительного просмотра",
"setting_notifications_notify_failures_grace_period": "Уведомлять об ошибках фонового резервного копирования: {}",
"setting_notifications_notify_hours": "{} часов",
"setting_notifications_notify_immediately": "немедленно",
@@ -365,7 +366,7 @@
"setting_notifications_notify_seconds": "{} секунд",
"setting_notifications_single_progress_subtitle": "Подробная информация о ходе загрузки для каждого объекта",
"setting_notifications_single_progress_title": "Показать ход выполнения фонового резервного копирования",
"setting_notifications_subtitle": "Настроить параметры уведомлений",
"setting_notifications_subtitle": "Настройка параметров уведомлени",
"setting_notifications_title": "Уведомления",
"setting_notifications_total_progress_subtitle": "Общий прогресс загрузки (выполнено/всего объектов)",
"setting_notifications_total_progress_title": "Показать общий прогресс фонового резервного копирования",
@@ -375,61 +376,61 @@
"share_add_photos": "Добавить фото",
"share_add_title": "Добавить название",
"share_create_album": "Создать альбом",
"shared_album_activities_input_disable": "Комментарий отключен",
"shared_album_activities_input_disable": "Комментирование отключено",
"shared_album_activities_input_hint": "Скажите что-нибудь",
"shared_album_activity_remove_content": "Хотите ли Вы удалить это действие?",
"shared_album_activity_remove_title": "Удалить действие",
"shared_album_activity_setting_subtitle": "Предоставьте другим возможность отвечать",
"shared_album_activity_remove_content": "Хотите ли Вы удалить это сообщение?",
"shared_album_activity_remove_title": "Удалить сообщение",
"shared_album_activity_setting_subtitle": "Разрешить другим отвечат",
"shared_album_activity_setting_title": "Комментарии и лайки",
"shared_album_section_people_action_error": "Error leaving/removing from album",
"shared_album_section_people_action_error": "Ошибка при выходе/удалении из альбома",
"shared_album_section_people_action_leave": "Удалить пользователя из альбома",
"shared_album_section_people_action_remove_user": "Удалить пользователя из альбома",
"shared_album_section_people_owner_label": "Owner",
"shared_album_section_people_title": "PEOPLE",
"shared_album_section_people_owner_label": "Владелец",
"shared_album_section_people_title": "ЛЮДИ",
"share_dialog_preparing": "Подготовка...",
"shared_link_app_bar_title": "Общие ссылки",
"shared_link_clipboard_copied_massage": "Скопировано в буфер обмена",
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
"shared_link_create_app_bar_title": "Создать ссылку для совместного использования",
"shared_link_create_error": "Error while creating shared link",
"shared_link_create_info": "Позволить любому человеку, имеющему ссылку, увидеть выбранную фотографию (фотографии)",
"shared_link_clipboard_text": "Ссылка: {}\nПароль: {}",
"shared_link_create_app_bar_title": "Создать ссылку общего доступа",
"shared_link_create_error": "Ошибка при создании общей ссылки",
"shared_link_create_info": "Разрешить всем, у кого есть ссылка, просматривать выбранные фото",
"shared_link_create_submit_button": "Создать ссылку",
"shared_link_edit_allow_download": "Разрешить публичному пользователю скачивать",
"shared_link_edit_allow_download": "Разрешить публичному пользователю скачивать файлы",
"shared_link_edit_allow_upload": "Разрешить публичному пользователю загружать файлы",
"shared_link_edit_app_bar_title": "Редактировать ссылку",
"shared_link_edit_change_expiry": "Изменить срок действия доступа",
"shared_link_edit_description": "Описание",
"shared_link_edit_description_hint": "Введите описание совместного доступа",
"shared_link_edit_expire_after": "Истекает после",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{} days",
"shared_link_edit_expire_after_option_hour": "1 hour",
"shared_link_edit_expire_after_option_hours": "{} hours",
"shared_link_edit_expire_after_option_minute": "1 minute",
"shared_link_edit_expire_after_option_minutes": "{} minutes",
"shared_link_edit_description_hint": "Введите описание для общего доступа",
"shared_link_edit_expire_after": "Истекает через",
"shared_link_edit_expire_after_option_day": "1 день",
"shared_link_edit_expire_after_option_days": "{} дней",
"shared_link_edit_expire_after_option_hour": "1 час",
"shared_link_edit_expire_after_option_hours": "{} часов",
"shared_link_edit_expire_after_option_minute": "1 минуту",
"shared_link_edit_expire_after_option_minutes": "{} минут",
"shared_link_edit_expire_after_option_never": "Никогда",
"shared_link_edit_password": "Пароль",
"shared_link_edit_password_hint": "Введите пароль общего доступа",
"shared_link_edit_show_meta": "Показать метаданные",
"shared_link_edit_password_hint": "Введите пароль для общего доступа",
"shared_link_edit_show_meta": "Показывать метаданные",
"shared_link_edit_submit_button": "Обновить ссылку",
"shared_link_empty": "У вас нет общих ссылок",
"shared_link_error_server_url_fetch": "Cannot fetch the server url",
"shared_link_expired": "Expired",
"shared_link_expires_day": "Expires in {} day",
"shared_link_expires_days": "Expires in {} days",
"shared_link_expires_hour": "Expires in {} hour",
"shared_link_expires_hours": "Expires in {} hours",
"shared_link_expires_minute": "Expires in {} minute",
"shared_link_error_server_url_fetch": "Невозможно запросить URL с сервера",
"shared_link_expired": "Срок действия истек",
"shared_link_expires_day": "Истекает через {} день",
"shared_link_expires_days": "Истекает через {} дней",
"shared_link_expires_hour": "Истекает через {} час",
"shared_link_expires_hours": "Истекает через {} часов",
"shared_link_expires_minute": "Истекает через {} минуту",
"shared_link_expires_minutes": "Истекает через {} минут",
"shared_link_expires_never": "Expires ∞",
"shared_link_expires_second": "Expires in {} second",
"shared_link_expires_never": "Истекает ∞",
"shared_link_expires_second": "Истекает через {} секунду",
"shared_link_expires_seconds": "Истекает через {} секунд",
"shared_link_info_chip_download": "Download",
"shared_link_info_chip_download": "Скачать",
"shared_link_info_chip_metadata": "EXIF",
"shared_link_info_chip_upload": "Upload",
"shared_link_info_chip_upload": "Загрузить",
"shared_link_manage_links": "Управление общими ссылками",
"share_done": "Выполнено",
"share_invite": "\nПригласить в альбом",
"share_done": "Готово",
"share_invite": "Пригласить в альбом",
"sharing_page_album": "Общие альбомы",
"sharing_page_description": "Создавайте общие альбомы, чтобы делиться фотографиями и видео с людьми в вашей сети.",
"sharing_page_empty_list": "ПУСТОЙ СПИСОК",
@@ -443,10 +444,10 @@
"theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов",
"theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})",
"theme_setting_dark_mode_switch": "Тёмная тема",
"theme_setting_image_viewer_quality_subtitle": "Настройка качества детального просмотра изображения",
"theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра полноэкранных изображения",
"theme_setting_image_viewer_quality_title": "Качество просмотра изображений",
"theme_setting_system_theme_switch": "Автоматически (Как в системе)",
"theme_setting_theme_subtitle": "Выберите настройки темы приложения",
"theme_setting_system_theme_switch": "Автоматически (как в системе)",
"theme_setting_theme_subtitle": "Настройка темы приложения",
"theme_setting_theme_title": "Тема",
"theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть",
"theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку",
@@ -454,10 +455,10 @@
"trash_page_delete": "Удалить",
"trash_page_delete_all": "Удалить все",
"trash_page_empty_trash_btn": "Очистить корзину",
"trash_page_empty_trash_dialog_content": "Вы хотите очистить свою корзину? Эти объекты будут навсегда удалены из Immich",
"trash_page_empty_trash_dialog_content": "Вы хотите очистить свою корзину? Эти объекты будут навсегда удалены из Immich.",
"trash_page_empty_trash_dialog_ok": "ОК",
"trash_page_info": "Удаленные элементы будут окончательно удалены через {} дней",
"trash_page_no_assets": "Отсутствие удаленных объектов",
"trash_page_no_assets": "Удаленные объекты отсутсвуют",
"trash_page_restore": "Восстановить",
"trash_page_restore_all": "Восстановить все",
"trash_page_select_assets_btn": "Выбранные объекты",
@@ -474,6 +475,6 @@
"version_announcement_overlay_text_3": " и убедитесь, что ваши настройки docker-compose и .env обновлены, чтобы предотвратить любые неправильные настройки, особенно если вы используете WatchTower или любой другой механизм, который обрабатывает обновление вашего серверного приложения автоматически.",
"version_announcement_overlay_title": "Доступна новая версия сервера \uD83C\uDF89",
"viewer_remove_from_stack": "Удалить из стека",
"viewer_stack_use_as_main_asset": "Использование в качестве основного объекта",
"viewer_stack_use_as_main_asset": "Использовать в качестве основного объекта",
"viewer_unstack": "Разобрать стек"
}

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "PODROBNOSTI",
"exif_bottom_sheet_location": "LOKALITA",
"exif_bottom_sheet_location_add": "Nastaviť polohu",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Prebiehajúca práca",
"experimental_settings_new_asset_list_title": "Povolenie experimentálnej mriežky fotografií",
"experimental_settings_subtitle": "Používajte na vlastné riziko!",

View File

@@ -185,6 +185,7 @@
"exif_bottom_sheet_details": "DETAILS",
"exif_bottom_sheet_location": "LOCATION",
"exif_bottom_sheet_location_add": "Add a location",
"exif_bottom_sheet_people": "PEOPLE",
"experimental_settings_new_asset_list_subtitle": "Work in progress",
"experimental_settings_new_asset_list_title": "Enable experimental photo grid",
"experimental_settings_subtitle": "Use at your own risk!",

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