Compare commits
325 Commits
refactor-m
...
feature/Ad
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f19cf206ba | ||
|
|
7ac30995a8 | ||
|
|
fe2ddc3644 | ||
|
|
ec0eb93036 | ||
|
|
d9a41b8ea0 | ||
|
|
f30fac971a | ||
|
|
fe26ccd1b7 | ||
|
|
3f4bbab4eb | ||
|
|
2da9e3152b | ||
|
|
56b85f7479 | ||
|
|
8b43066632 | ||
|
|
20acdcd884 | ||
|
|
22d348beca | ||
|
|
3b0af1c8a9 | ||
|
|
61c8237a4d | ||
|
|
d740f0283a | ||
|
|
4ada28ac99 | ||
|
|
63c01b78e2 | ||
|
|
1423cfd53c | ||
|
|
867eec86f5 | ||
|
|
86e8effd8e | ||
|
|
49d393216a | ||
|
|
75c9f63757 | ||
|
|
63984890df | ||
|
|
1356468c38 | ||
|
|
c23c53bf6f | ||
|
|
0dcfc43461 | ||
|
|
d1fd0076cc | ||
|
|
ff19502035 | ||
|
|
6ef069b537 | ||
|
|
a03e999bde | ||
|
|
ad1ba4be5f | ||
|
|
f89e74181b | ||
|
|
e2c34f17ba | ||
|
|
23b1256592 | ||
|
|
7bbc1d9f68 | ||
|
|
8b24c31d20 | ||
|
|
7f61ac6983 | ||
|
|
4db8f0c666 | ||
|
|
3d6a6f77a8 | ||
|
|
5698f446f7 | ||
|
|
eb74fafb00 | ||
|
|
24da25dbbf | ||
|
|
9b842d4cca | ||
|
|
a99bd94717 | ||
|
|
4b568dcbb3 | ||
|
|
12ab56c885 | ||
|
|
eed6465b41 | ||
|
|
5f6c16080b | ||
|
|
a2aab1f373 | ||
|
|
8e076ecfe4 | ||
|
|
fe702ba6d7 | ||
|
|
869839f642 | ||
|
|
8885e3105e | ||
|
|
6e51c4ec71 | ||
|
|
6bf2e8dbcb | ||
|
|
366f23774a | ||
|
|
fd5e931617 | ||
|
|
d8d87bb565 | ||
|
|
6cc1978b2d | ||
|
|
506d2d0f81 | ||
|
|
f13d13b2ea | ||
|
|
2510684bf7 | ||
|
|
c8eef5ad4d | ||
|
|
0cb3dc6211 | ||
|
|
f11080cc2d | ||
|
|
efcf773ea0 | ||
|
|
dc143046e3 | ||
|
|
e684062569 | ||
|
|
5c0538e52c | ||
|
|
84cf0d1670 | ||
|
|
bfcde05b1c | ||
|
|
b3b15e9b61 | ||
|
|
819e56d9ca | ||
|
|
9a98712db7 | ||
|
|
a185e06399 | ||
|
|
f2be9f7ad1 | ||
|
|
5c879acd5b | ||
|
|
28c664c769 | ||
|
|
fbd85a89e0 | ||
|
|
1c86293035 | ||
|
|
4a9d80298b | ||
|
|
362feb1e62 | ||
|
|
5503bf7a60 | ||
|
|
d20e2e268a | ||
|
|
a708649504 | ||
|
|
a808b8610e | ||
|
|
c70c9067b0 | ||
|
|
082471dfd9 | ||
|
|
9a098b4658 | ||
|
|
9d705097e8 | ||
|
|
6050485ad8 | ||
|
|
fb907d707d | ||
|
|
7d6cfd09e6 | ||
|
|
967c69317b | ||
|
|
128d653fc6 | ||
|
|
8b69114924 | ||
|
|
4b55888d16 | ||
|
|
8fbd650483 | ||
|
|
c778516ce2 | ||
|
|
2969e25ff7 | ||
|
|
c055e1aefe | ||
|
|
5f7f88ff17 | ||
|
|
5053130e35 | ||
|
|
4ef7eb56a3 | ||
|
|
8ecc67a364 | ||
|
|
90f7c3d9ae | ||
|
|
d0381fddec | ||
|
|
7c851893b4 | ||
|
|
ae61ea7984 | ||
|
|
bbcaee82f0 | ||
|
|
16266c9f5a | ||
|
|
6c64a6dab8 | ||
|
|
c0fe98fe27 | ||
|
|
579321251f | ||
|
|
392f9f205c | ||
|
|
57829cee26 | ||
|
|
4be2351d21 | ||
|
|
edbcf17e3a | ||
|
|
eef74ee0ba | ||
|
|
ec58e1065f | ||
|
|
4376fd72b7 | ||
|
|
e4b6efc1f5 | ||
|
|
caea3a0812 | ||
|
|
9c2c85cbe1 | ||
|
|
d350022dec | ||
|
|
502f6e020d | ||
|
|
ca9e02379d | ||
|
|
36ec407c66 | ||
|
|
007eaaceb9 | ||
|
|
94c0e8253a | ||
|
|
5acf6868b7 | ||
|
|
616905211d | ||
|
|
3925445de8 | ||
|
|
52f21fb331 | ||
|
|
ac36effb45 | ||
|
|
02cd8da871 | ||
|
|
17a2043e76 | ||
|
|
34b88bb47a | ||
|
|
f6ba071569 | ||
|
|
6b7a7b0cbc | ||
|
|
b0102f8025 | ||
|
|
9c95adc7fb | ||
|
|
376282e538 | ||
|
|
76d95cd348 | ||
|
|
31dc83f3f2 | ||
|
|
aeb3e0a84f | ||
|
|
8634c59850 | ||
|
|
b13a98646f | ||
|
|
7bf142dc43 | ||
|
|
d8cda6ee40 | ||
|
|
a31bc94460 | ||
|
|
516709ffe1 | ||
|
|
425cf62482 | ||
|
|
58242b3b4a | ||
|
|
9d4aee36e2 | ||
|
|
70d08a2b2a | ||
|
|
f1b98d5f45 | ||
|
|
749eff03d5 | ||
|
|
5f257b9a84 | ||
|
|
0cae20033c | ||
|
|
115ee0d6cc | ||
|
|
bfdd6eac01 | ||
|
|
9eab770e79 | ||
|
|
efd8d8b884 | ||
|
|
25e1c8cc7f | ||
|
|
7c26663013 | ||
|
|
2c88ce8559 | ||
|
|
50b072803d | ||
|
|
1689cecaf7 | ||
|
|
5cd1018db3 | ||
|
|
31e6270a28 | ||
|
|
b3fbd0809b | ||
|
|
129a4a82e0 | ||
|
|
924d11a913 | ||
|
|
425c87bce4 | ||
|
|
25fcda6eeb | ||
|
|
f386b4d377 | ||
|
|
c524fcf084 | ||
|
|
194c567a45 | ||
|
|
411f96ef49 | ||
|
|
4f912de018 | ||
|
|
47203d2760 | ||
|
|
8ab87a8803 | ||
|
|
5b4f894211 | ||
|
|
b1f05fc18b | ||
|
|
dbbefde98d | ||
|
|
5407a28533 | ||
|
|
f5edc87e4d | ||
|
|
bf16b61d43 | ||
|
|
8c882b54cd | ||
|
|
2d7c333c8c | ||
|
|
7c821dd205 | ||
|
|
703361da1a | ||
|
|
fa5aeaf539 | ||
|
|
5f3a42a132 | ||
|
|
9d85272c2b | ||
|
|
d2575d8f00 | ||
|
|
f0a4c945bd | ||
|
|
a3766b879e | ||
|
|
1a190c33a0 | ||
|
|
17a63e37b2 | ||
|
|
bf1f8da884 | ||
|
|
2271984dbd | ||
|
|
b40963ec52 | ||
|
|
735f8d661e | ||
|
|
8794c84e9d | ||
|
|
cef19eed97 | ||
|
|
90c607c1a6 | ||
|
|
52b650093d | ||
|
|
fe4c49c8e3 | ||
|
|
4cad23aaa3 | ||
|
|
feba590de7 | ||
|
|
64f0333306 | ||
|
|
758bcd1e97 | ||
|
|
fb21950ad8 | ||
|
|
758449e9f0 | ||
|
|
d7d4d22fe0 | ||
|
|
03948a69e2 | ||
|
|
61b8eb85b5 | ||
|
|
c5360e78c5 | ||
|
|
23014c263b | ||
|
|
2e5007adef | ||
|
|
c4531fc4d3 | ||
|
|
252d3f5f2c | ||
|
|
ef6c2bf547 | ||
|
|
6aad9fae8e | ||
|
|
45f7401513 | ||
|
|
3c7edba388 | ||
|
|
76a70703a5 | ||
|
|
f78066d4b9 | ||
|
|
48d421e28c | ||
|
|
1492b55c07 | ||
|
|
1d6a4e9318 | ||
|
|
fe42e7410b | ||
|
|
58bf58b393 | ||
|
|
99de52479e | ||
|
|
97574d7296 | ||
|
|
5015210f37 | ||
|
|
0bb1219b5f | ||
|
|
b730aa60ed | ||
|
|
7ec3610753 | ||
|
|
69e88ef985 | ||
|
|
9358b4dc7e | ||
|
|
06f077bac2 | ||
|
|
47f6181d42 | ||
|
|
aac029d92b | ||
|
|
ef245ea2d2 | ||
|
|
e8d05e78ad | ||
|
|
52c9fbea5f | ||
|
|
882163f545 | ||
|
|
96a6cc20b7 | ||
|
|
4efacfbb91 | ||
|
|
a808a840c8 | ||
|
|
3f18acdb1a | ||
|
|
2b41b5efe1 | ||
|
|
9ac95d6845 | ||
|
|
221e197633 | ||
|
|
1b141d5ca9 | ||
|
|
098bab7c9b | ||
|
|
4fccc09fc1 | ||
|
|
c016b65ef2 | ||
|
|
844eed8707 | ||
|
|
6e31ac4c75 | ||
|
|
b287c0cbe8 | ||
|
|
1fcc75fb44 | ||
|
|
ca79e25a6e | ||
|
|
4fd8c1b3c1 | ||
|
|
f3ba994186 | ||
|
|
b4a4abbf51 | ||
|
|
a0aea021a1 | ||
|
|
9033a99587 | ||
|
|
cc0cbd705e | ||
|
|
da580d4685 | ||
|
|
cb6d94c7a7 | ||
|
|
060300de8a | ||
|
|
c2ba1cc202 | ||
|
|
08db77db23 | ||
|
|
92dff839d0 | ||
|
|
fe1e09e51f | ||
|
|
f44669447f | ||
|
|
92412ca2f7 | ||
|
|
64d926581f | ||
|
|
c139e05170 | ||
|
|
0fe62298e1 | ||
|
|
e5794e6cfc | ||
|
|
f6cbc9db06 | ||
|
|
8dab5d3798 | ||
|
|
e864811a85 | ||
|
|
72a55c13b6 | ||
|
|
206412267a | ||
|
|
f780a56e24 | ||
|
|
7bbffccf76 | ||
|
|
05a446c259 | ||
|
|
4f725b95e1 | ||
|
|
64b92cb24c | ||
|
|
19f2f888ee | ||
|
|
d12b1c907d | ||
|
|
947c053c15 | ||
|
|
79592701dd | ||
|
|
39697cd973 | ||
|
|
10e518db42 | ||
|
|
72fa31f9e9 | ||
|
|
9871a04d54 | ||
|
|
ba01b40e7c | ||
|
|
f5a3d7ba23 | ||
|
|
d4a9eed4a1 | ||
|
|
9d8072b994 | ||
|
|
3c1fa22109 | ||
|
|
c0210bd6c0 | ||
|
|
a6ace5151c | ||
|
|
ede9c99adb | ||
|
|
ec7ab209f3 | ||
|
|
61bc24d7ea | ||
|
|
6c95eb22b7 | ||
|
|
aaea5cf1ad | ||
|
|
96d2e9b4c5 | ||
|
|
19740a3560 | ||
|
|
8a481e2ea1 | ||
|
|
ba105d9f19 | ||
|
|
065d885ca0 | ||
|
|
a07ae9b5b2 | ||
|
|
1869b1b41a | ||
|
|
995314446b | ||
|
|
a1691ddc0f |
@@ -11,7 +11,7 @@ body:
|
|||||||
|
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: I have searched the existing feature requests to make sure this is not a duplicate request.
|
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
||||||
options:
|
options:
|
||||||
- label: "Yes"
|
- label: "Yes"
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1 +1 @@
|
|||||||
custom: ['https://buy.immich.app']
|
custom: ['https://buy.immich.app', 'https://immich.store']
|
||||||
|
|||||||
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
7
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -1,6 +1,13 @@
|
|||||||
name: Report an issue with Immich
|
name: Report an issue with Immich
|
||||||
description: Report an issue with Immich
|
description: Report an issue with Immich
|
||||||
body:
|
body:
|
||||||
|
- type: checkboxes
|
||||||
|
attributes:
|
||||||
|
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
||||||
|
options:
|
||||||
|
- label: "Yes"
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
|
|||||||
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
1
.github/PULL_REQUEST_TEMPLATE/config.yml
vendored
@@ -1,2 +1 @@
|
|||||||
blank_issues_enabled: false
|
|
||||||
blank_pull_request_template_enabled: false
|
blank_pull_request_template_enabled: false
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
## Description
|
|
||||||
<!--- Describe your changes in detail -->
|
|
||||||
<!--- Why is this change required? What problem does it solve? -->
|
|
||||||
<!--- If it fixes an open issue, please link to the issue here. -->
|
|
||||||
|
|
||||||
Fixes # (issue)
|
|
||||||
|
|
||||||
|
|
||||||
## How Has This Been Tested?
|
|
||||||
|
|
||||||
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
|
|
||||||
|
|
||||||
- [ ] Test A
|
|
||||||
- [ ] Test B
|
|
||||||
|
|
||||||
## Screenshots (if appropriate):
|
|
||||||
|
|
||||||
|
|
||||||
## Checklist:
|
|
||||||
|
|
||||||
- [ ] I have performed a self-review of my own code
|
|
||||||
- [ ] I have made corresponding changes to the documentation if applicable
|
|
||||||
36
.github/pull_request_template.md
vendored
Normal file
36
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
## Description
|
||||||
|
|
||||||
|
<!--- Describe your changes in detail -->
|
||||||
|
<!--- Why is this change required? What problem does it solve? -->
|
||||||
|
<!--- If it fixes an open issue, please link to the issue here. -->
|
||||||
|
|
||||||
|
Fixes # (issue)
|
||||||
|
|
||||||
|
## How Has This Been Tested?
|
||||||
|
|
||||||
|
<!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration -->
|
||||||
|
|
||||||
|
- [ ] Test A
|
||||||
|
- [ ] Test B
|
||||||
|
|
||||||
|
<details><summary><h2>Screenshots (if appropriate)</h2></summary>
|
||||||
|
|
||||||
|
<!-- Images go below this line. -->
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<!-- API endpoint changes (if relevant)
|
||||||
|
## API Changes
|
||||||
|
The `/api/something` endpoint is now `/api/something-else`
|
||||||
|
-->
|
||||||
|
|
||||||
|
## Checklist:
|
||||||
|
|
||||||
|
- [ ] I have performed a self-review of my own code
|
||||||
|
- [ ] I have made corresponding changes to the documentation if applicable
|
||||||
|
- [ ] I have no unrelated changes in the PR.
|
||||||
|
- [ ] I have confirmed that any new dependencies are strictly necessary.
|
||||||
|
- [ ] I have written tests for new code (if applicable)
|
||||||
|
- [ ] I have followed naming conventions/patterns in the surrounding code
|
||||||
|
- [ ] All code in `src/services` uses repositories implementations for database calls, filesystem operations, etc.
|
||||||
|
- [ ] All code in `src/repositories/` is pretty basic/simple and does not have any immich specific logic (that belongs in `src/services`)
|
||||||
4
.github/workflows/build-mobile.yml
vendored
4
.github/workflows/build-mobile.yml
vendored
@@ -29,9 +29,11 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
mobile:
|
mobile:
|
||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
|
workflow:
|
||||||
|
- '.github/workflows/build-mobile.yml'
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build-sign-android:
|
build-sign-android:
|
||||||
name: Build and sign Android
|
name: Build and sign Android
|
||||||
|
|||||||
6
.github/workflows/cli.yml
vendored
6
.github/workflows/cli.yml
vendored
@@ -56,10 +56,10 @@ jobs:
|
|||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3.3.0
|
uses: docker/setup-qemu-action@v3.5.0
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.8.0
|
uses: docker/setup-buildx-action@v3.10.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -88,7 +88,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6.12.0
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
73
.github/workflows/docker-cleanup.yml
vendored
73
.github/workflows/docker-cleanup.yml
vendored
@@ -1,73 +0,0 @@
|
|||||||
# This workflow runs on certain conditions to check for and potentially
|
|
||||||
# delete container images from the GHCR which no longer have an associated
|
|
||||||
# code branch.
|
|
||||||
# Requires a PAT with the correct scope set in the secrets.
|
|
||||||
#
|
|
||||||
# This workflow will not trigger runs on forked repos.
|
|
||||||
|
|
||||||
name: Docker Cleanup
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types:
|
|
||||||
- "closed"
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- ".github/workflows/docker-cleanup.yml"
|
|
||||||
|
|
||||||
concurrency:
|
|
||||||
group: registry-tags-cleanup
|
|
||||||
cancel-in-progress: false
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
cleanup-images:
|
|
||||||
name: Cleanup Stale Images Tags for ${{ matrix.primary-name }}
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- primary-name: "immich-server"
|
|
||||||
- primary-name: "immich-machine-learning"
|
|
||||||
env:
|
|
||||||
# Requires a personal access token with the OAuth scope delete:packages
|
|
||||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- name: Clean temporary images
|
|
||||||
if: "${{ env.TOKEN != '' }}"
|
|
||||||
uses: stumpylog/image-cleaner-action/ephemeral@v0.9.0
|
|
||||||
with:
|
|
||||||
token: "${{ env.TOKEN }}"
|
|
||||||
owner: "immich-app"
|
|
||||||
is_org: "true"
|
|
||||||
do_delete: "true"
|
|
||||||
package_name: "${{ matrix.primary-name }}"
|
|
||||||
scheme: "pull_request"
|
|
||||||
repo_name: "immich"
|
|
||||||
match_regex: '^pr-(\d+)$|^(\d+)$'
|
|
||||||
|
|
||||||
cleanup-untagged-images:
|
|
||||||
name: Cleanup Untagged Images Tags for ${{ matrix.primary-name }}
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
needs:
|
|
||||||
- cleanup-images
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- primary-name: "immich-server"
|
|
||||||
- primary-name: "immich-machine-learning"
|
|
||||||
- primary-name: "immich-build-cache"
|
|
||||||
env:
|
|
||||||
# Requires a personal access token with the OAuth scope delete:packages
|
|
||||||
TOKEN: ${{ secrets.PACKAGE_DELETE_TOKEN }}
|
|
||||||
steps:
|
|
||||||
- name: Clean untagged images
|
|
||||||
if: "${{ env.TOKEN != '' }}"
|
|
||||||
uses: stumpylog/image-cleaner-action/untagged@v0.9.0
|
|
||||||
with:
|
|
||||||
token: "${{ env.TOKEN }}"
|
|
||||||
owner: "immich-app"
|
|
||||||
do_delete: "true"
|
|
||||||
is_org: "true"
|
|
||||||
package_name: "${{ matrix.primary-name }}"
|
|
||||||
382
.github/workflows/docker.yml
vendored
382
.github/workflows/docker.yml
vendored
@@ -5,7 +5,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
@@ -36,10 +35,12 @@ jobs:
|
|||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
machine-learning:
|
machine-learning:
|
||||||
- 'machine-learning/**'
|
- 'machine-learning/**'
|
||||||
|
workflow:
|
||||||
|
- '.github/workflows/docker.yml'
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
retag_ml:
|
retag_ml:
|
||||||
name: Re-Tag ML
|
name: Re-Tag ML
|
||||||
@@ -48,21 +49,23 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
suffix: ["", "-cuda", "-openvino", "-armnn"]
|
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
- name: Re-tag image
|
- name: Re-tag image
|
||||||
run: |
|
run: |
|
||||||
REGISTRY_NAME="ghcr.io"
|
REGISTRY_NAME="ghcr.io"
|
||||||
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
||||||
TAG_OLD=main${{ matrix.suffix }}
|
TAG_OLD=main${{ matrix.suffix }}
|
||||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
|
||||||
retag_server:
|
retag_server:
|
||||||
name: Re-Tag Server
|
name: Re-Tag Server
|
||||||
@@ -71,7 +74,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
suffix: [""]
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
@@ -84,107 +87,105 @@ jobs:
|
|||||||
REGISTRY_NAME="ghcr.io"
|
REGISTRY_NAME="ghcr.io"
|
||||||
REPOSITORY=${{ github.repository_owner }}/immich-server
|
REPOSITORY=${{ github.repository_owner }}/immich-server
|
||||||
TAG_OLD=main${{ matrix.suffix }}
|
TAG_OLD=main${{ matrix.suffix }}
|
||||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||||
|
|
||||||
build_and_push_ml:
|
build_and_push_ml:
|
||||||
name: Build and Push ML
|
name: Build and Push ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
env:
|
env:
|
||||||
image: immich-machine-learning
|
image: immich-machine-learning
|
||||||
context: machine-learning
|
context: machine-learning
|
||||||
file: machine-learning/Dockerfile
|
file: machine-learning/Dockerfile
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||||
strategy:
|
strategy:
|
||||||
# Prevent a failure in one image from stopping the other builds
|
# Prevent a failure in one image from stopping the other builds
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- platforms: linux/amd64,linux/arm64
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
device: cpu
|
device: cpu
|
||||||
|
|
||||||
- platforms: linux/amd64
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
device: cpu
|
||||||
|
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
device: cuda
|
device: cuda
|
||||||
suffix: -cuda
|
suffix: -cuda
|
||||||
|
|
||||||
- platforms: linux/amd64
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
device: openvino
|
device: openvino
|
||||||
suffix: -openvino
|
suffix: -openvino
|
||||||
|
|
||||||
- platforms: linux/arm64
|
- platforms: linux/amd64
|
||||||
|
runner: mich
|
||||||
|
device: rocm
|
||||||
|
suffix: -rocm
|
||||||
|
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
device: armnn
|
device: armnn
|
||||||
suffix: -armnn
|
suffix: -armnn
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.3.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.8.0
|
uses: docker/setup-buildx-action@v3.10.0
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
# Only push to Docker Hub when making a release
|
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
# Skip when PR from a fork
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate cache key suffix
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
# Disable latest tag
|
|
||||||
latest=false
|
|
||||||
images: |
|
|
||||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
|
||||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
|
||||||
tags: |
|
|
||||||
# Tag with branch name
|
|
||||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
|
||||||
# Tag with pr-number
|
|
||||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
|
||||||
# Tag with git tag on release
|
|
||||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
|
||||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
|
||||||
|
|
||||||
- name: Determine build cache output
|
|
||||||
id: cache-target
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate cache target
|
||||||
|
id: cache-target
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6.12.0
|
id: build
|
||||||
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ${{ env.context }}
|
context: ${{ env.context }}
|
||||||
file: ${{ env.file }}
|
file: ${{ env.file }}
|
||||||
platforms: ${{ matrix.platforms }}
|
platforms: ${{ matrix.platforms }}
|
||||||
# Skip pushing when PR from a fork
|
|
||||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
|
||||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
|
||||||
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||||
build-args: |
|
build-args: |
|
||||||
DEVICE=${{ matrix.device }}
|
DEVICE=${{ matrix.device }}
|
||||||
BUILD_ID=${{ github.run_id }}
|
BUILD_ID=${{ github.run_id }}
|
||||||
@@ -192,100 +193,249 @@ jobs:
|
|||||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge_ml:
|
||||||
|
name: Merge & Push ML
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
|
env:
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||||
|
DOCKER_REPO: altran1502/immich-machine-learning
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- device: cpu
|
||||||
|
- device: cuda
|
||||||
|
suffix: -cuda
|
||||||
|
- device: openvino
|
||||||
|
suffix: -openvino
|
||||||
|
- device: armnn
|
||||||
|
suffix: -armnn
|
||||||
|
needs:
|
||||||
|
- build_and_push_ml
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: ml-digests-${{ matrix.device }}-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
# Disable latest tag
|
||||||
|
latest=false
|
||||||
|
images: |
|
||||||
|
name=${{ env.GHCR_REPO }}
|
||||||
|
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||||
|
tags: |
|
||||||
|
# Tag with branch name
|
||||||
|
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with pr-number
|
||||||
|
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with long commit sha hash
|
||||||
|
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with git tag on release
|
||||||
|
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||||
|
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||||
|
|
||||||
build_and_push_server:
|
build_and_push_server:
|
||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
runs-on: ubuntu-latest
|
runs-on: ${{ matrix.runner }}
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
env:
|
env:
|
||||||
image: immich-server
|
image: immich-server
|
||||||
context: .
|
context: .
|
||||||
file: server/Dockerfile
|
file: server/Dockerfile
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- platforms: linux/amd64,linux/arm64
|
- platform: linux/amd64
|
||||||
device: cpu
|
runner: ubuntu-latest
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
steps:
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up QEMU
|
|
||||||
uses: docker/setup-qemu-action@v3.3.0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
- name: Set up Docker Buildx
|
||||||
uses: docker/setup-buildx-action@v3.8.0
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
- name: Login to Docker Hub
|
|
||||||
# Only push to Docker Hub when making a release
|
|
||||||
if: ${{ github.event_name == 'release' }}
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@v3
|
uses: docker/login-action@v3
|
||||||
# Skip when PR from a fork
|
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate cache key suffix
|
||||||
id: metadata
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
flavor: |
|
|
||||||
# Disable latest tag
|
|
||||||
latest=false
|
|
||||||
images: |
|
|
||||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
|
||||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
|
||||||
tags: |
|
|
||||||
# Tag with branch name
|
|
||||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
|
||||||
# Tag with pr-number
|
|
||||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
|
||||||
# Tag with git tag on release
|
|
||||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
|
||||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
|
||||||
|
|
||||||
- name: Determine build cache output
|
|
||||||
id: cache-target
|
|
||||||
run: |
|
run: |
|
||||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate cache target
|
||||||
|
id: cache-target
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
else
|
else
|
||||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@v6.12.0
|
id: build
|
||||||
|
uses: docker/build-push-action@v6.15.0
|
||||||
with:
|
with:
|
||||||
context: ${{ env.context }}
|
context: ${{ env.context }}
|
||||||
file: ${{ env.file }}
|
file: ${{ env.file }}
|
||||||
platforms: ${{ matrix.platforms }}
|
platforms: ${{ matrix.platform }}
|
||||||
# Skip pushing when PR from a fork
|
|
||||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
|
||||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
|
||||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
|
||||||
tags: ${{ steps.metadata.outputs.tags }}
|
|
||||||
labels: ${{ steps.metadata.outputs.labels }}
|
labels: ${{ steps.metadata.outputs.labels }}
|
||||||
|
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
|
||||||
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||||
build-args: |
|
build-args: |
|
||||||
DEVICE=${{ matrix.device }}
|
DEVICE=cpu
|
||||||
BUILD_ID=${{ github.run_id }}
|
BUILD_ID=${{ github.run_id }}
|
||||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: server-digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge_server:
|
||||||
|
name: Merge & Push Server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
|
env:
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||||
|
DOCKER_REPO: altran1502/immich-server
|
||||||
|
needs:
|
||||||
|
- build_and_push_server
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: server-digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
# Disable latest tag
|
||||||
|
latest=false
|
||||||
|
images: |
|
||||||
|
name=${{ env.GHCR_REPO }}
|
||||||
|
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||||
|
tags: |
|
||||||
|
# Tag with branch name
|
||||||
|
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with pr-number
|
||||||
|
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with long commit sha hash
|
||||||
|
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||||
|
# Tag with git tag on release
|
||||||
|
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||||
|
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||||
|
|
||||||
success-check-server:
|
success-check-server:
|
||||||
name: Docker Build & Push Server Success
|
name: Docker Build & Push Server Success
|
||||||
needs: [build_and_push_server, retag_server]
|
needs: [merge_server, retag_server]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
@@ -298,7 +448,7 @@ jobs:
|
|||||||
|
|
||||||
success-check-ml:
|
success-check-ml:
|
||||||
name: Docker Build & Push ML Success
|
name: Docker Build & Push ML Success
|
||||||
needs: [build_and_push_ml, retag_ml]
|
needs: [merge_ml, retag_ml]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
6
.github/workflows/docs-build.yml
vendored
6
.github/workflows/docs-build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
pre-job:
|
pre-job:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
@@ -25,9 +25,11 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
docs:
|
docs:
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
workflow:
|
||||||
|
- '.github/workflows/docs-build.yml'
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
name: Docs Build
|
name: Docs Build
|
||||||
|
|||||||
33
.github/workflows/preview-label.yaml
vendored
Normal file
33
.github/workflows/preview-label.yaml
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
name: Preview label
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [labeled]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
comment-status:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.action == 'labeled' && github.event.label.name == 'preview' }}
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: mshick/add-pr-comment@v2
|
||||||
|
with:
|
||||||
|
message-id: "preview-status"
|
||||||
|
message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"
|
||||||
|
|
||||||
|
remove-label:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ github.event.action == 'closed' && contains(github.event.pull_request.labels.*.name, 'preview') }}
|
||||||
|
permissions:
|
||||||
|
pull-requests: write
|
||||||
|
steps:
|
||||||
|
- uses: actions/github-script@v7
|
||||||
|
with:
|
||||||
|
script: |
|
||||||
|
github.rest.issues.removeLabel({
|
||||||
|
issue_number: context.payload.pull_request.number,
|
||||||
|
owner: context.repo.owner,
|
||||||
|
repo: context.repo.repo,
|
||||||
|
name: 'preview'
|
||||||
|
})
|
||||||
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
@@ -23,9 +23,11 @@ jobs:
|
|||||||
filters: |
|
filters: |
|
||||||
mobile:
|
mobile:
|
||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
|
workflow:
|
||||||
|
- '.github/workflows/static_analysis.yml'
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
mobile-dart-analyze:
|
mobile-dart-analyze:
|
||||||
name: Run Dart Code Analysis
|
name: Run Dart Code Analysis
|
||||||
|
|||||||
28
.github/workflows/test.yml
vendored
28
.github/workflows/test.yml
vendored
@@ -43,10 +43,12 @@ jobs:
|
|||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
machine-learning:
|
machine-learning:
|
||||||
- 'machine-learning/**'
|
- 'machine-learning/**'
|
||||||
|
workflow:
|
||||||
|
- '.github/workflows/test.yml'
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Test & Lint Server
|
name: Test & Lint Server
|
||||||
@@ -244,25 +246,30 @@ jobs:
|
|||||||
run: npm run check
|
run: npm run check
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
medium-tests-server:
|
server-medium-tests:
|
||||||
name: Medium Tests (Server)
|
name: Medium Tests (Server)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
runs-on: mich
|
runs-on: ubuntu-latest
|
||||||
|
defaults:
|
||||||
|
run:
|
||||||
|
working-directory: ./server
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
|
||||||
submodules: 'recursive'
|
|
||||||
|
|
||||||
- name: Production build
|
- name: Setup Node
|
||||||
if: ${{ !cancelled() }}
|
uses: actions/setup-node@v4
|
||||||
run: docker compose -f e2e/docker-compose.yml build
|
with:
|
||||||
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
|
- name: Run npm install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
|
run: npm run test:medium
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
run: make test-medium
|
|
||||||
|
|
||||||
e2e-tests-server-cli:
|
e2e-tests-server-cli:
|
||||||
name: End-to-End Tests (Server & CLI)
|
name: End-to-End Tests (Server & CLI)
|
||||||
@@ -450,7 +457,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -502,6 +509,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
echo "ERROR: Generated migration files not up to date!"
|
echo "ERROR: Generated migration files not up to date!"
|
||||||
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
echo "Changed files: ${{ steps.verify-changed-files.outputs.changed_files }}"
|
||||||
|
cat ./src/migrations/*-TestMigration.ts
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
- name: Run SQL generation
|
- name: Run SQL generation
|
||||||
|
|||||||
50
.github/workflows/weblate-lock.yml
vendored
Normal file
50
.github/workflows/weblate-lock.yml
vendored
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
name: Weblate checks
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
pre-job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
||||||
|
steps:
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
- id: found_paths
|
||||||
|
uses: dorny/paths-filter@v3
|
||||||
|
with:
|
||||||
|
filters: |
|
||||||
|
i18n:
|
||||||
|
- 'i18n/!(en)**\.json'
|
||||||
|
enforce-lock:
|
||||||
|
name: Check Weblate Lock
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
||||||
|
steps:
|
||||||
|
- name: Check weblate lock
|
||||||
|
run: |
|
||||||
|
if [[ "false" = $(curl https://hosted.weblate.org/api/components/immich/immich/lock/ | jq .locked) ]]; then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
- name: Find Pull Request
|
||||||
|
uses: juliangruber/find-pull-request-action@v1
|
||||||
|
id: find-pr
|
||||||
|
with:
|
||||||
|
branch: chore/translations
|
||||||
|
- name: Fail if existing weblate PR
|
||||||
|
if: ${{ steps.find-pr.outputs.number }}
|
||||||
|
run: exit 1
|
||||||
|
success-check-lock:
|
||||||
|
name: Weblate Lock Check Success
|
||||||
|
needs: [ enforce-lock ]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Any jobs failed?
|
||||||
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
@@ -1 +1 @@
|
|||||||
22.13.1
|
22.14.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 AS core
|
FROM node:22.14.0-alpine3.20@sha256:40be979442621049f40b1d51a26b55e281246b5de4e5f51a18da7beb6e17e3f9 AS core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
639
cli/package-lock.json
generated
639
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.42",
|
"version": "2.2.52",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
@@ -19,8 +19,9 @@
|
|||||||
"@types/byte-size": "^8.1.0",
|
"@types/byte-size": "^8.1.0",
|
||||||
"@types/cli-progress": "^3.11.0",
|
"@types/cli-progress": "^3.11.0",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^22.10.9",
|
"@types/node": "^22.13.5",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||||
"@typescript-eslint/parser": "^8.15.0",
|
"@typescript-eslint/parser": "^8.15.0",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
@@ -31,7 +32,7 @@
|
|||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^56.0.1",
|
"eslint-plugin-unicorn": "^56.0.1",
|
||||||
"globals": "^15.9.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
@@ -62,11 +63,13 @@
|
|||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"chokidar": "^4.0.3",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"fastq": "^1.17.1",
|
"fastq": "^1.17.1",
|
||||||
"lodash-es": "^4.17.21"
|
"lodash-es": "^4.17.21",
|
||||||
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.13.1"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
import * as fs from 'node:fs';
|
import * as fs from 'node:fs';
|
||||||
import * as os from 'node:os';
|
import * as os from 'node:os';
|
||||||
import * as path from 'node:path';
|
import * as path from 'node:path';
|
||||||
import { describe, expect, it, vi } from 'vitest';
|
import { setTimeout as sleep } from 'node:timers/promises';
|
||||||
|
import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
||||||
|
|
||||||
import { Action, checkBulkUpload, defaults, Reason } from '@immich/sdk';
|
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
|
||||||
import { checkForDuplicates, getAlbumName, uploadFiles, UploadOptionsDto } from './asset';
|
import { checkForDuplicates, getAlbumName, startWatch, uploadFiles, UploadOptionsDto } from 'src/commands/asset';
|
||||||
|
|
||||||
vi.mock('@immich/sdk');
|
vi.mock('@immich/sdk');
|
||||||
|
|
||||||
@@ -199,3 +200,112 @@ describe('checkForDuplicates', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('startWatch', () => {
|
||||||
|
let testFolder: string;
|
||||||
|
let checkBulkUploadMocked: MockedFunction<typeof checkBulkUpload>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
|
||||||
|
vi.mocked(getSupportedMediaTypes).mockResolvedValue({
|
||||||
|
image: ['.jpg'],
|
||||||
|
sidecar: ['.xmp'],
|
||||||
|
video: ['.mp4'],
|
||||||
|
});
|
||||||
|
|
||||||
|
testFolder = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'test-startWatch-'));
|
||||||
|
checkBulkUploadMocked = vi.mocked(checkBulkUpload);
|
||||||
|
checkBulkUploadMocked.mockResolvedValue({
|
||||||
|
results: [],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start watching a directory and upload new files', async () => {
|
||||||
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
||||||
|
|
||||||
|
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
|
||||||
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
||||||
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
||||||
|
|
||||||
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
||||||
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
||||||
|
assetBulkUploadCheckDto: {
|
||||||
|
assets: [
|
||||||
|
expect.objectContaining({
|
||||||
|
id: testFilePath,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filter out unsupported files', async () => {
|
||||||
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
||||||
|
const unsupportedFilePath = path.join(testFolder, 'test.txt');
|
||||||
|
|
||||||
|
await startWatch([testFolder], { concurrency: 1 }, { batchSize: 1, debounceTimeMs: 10 });
|
||||||
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
||||||
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
||||||
|
await fs.promises.writeFile(unsupportedFilePath, 'testtxt');
|
||||||
|
|
||||||
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
||||||
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
||||||
|
assetBulkUploadCheckDto: {
|
||||||
|
assets: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: testFilePath,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkBulkUpload).not.toHaveBeenCalledWith({
|
||||||
|
assetBulkUploadCheckDto: {
|
||||||
|
assets: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: unsupportedFilePath,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should filger out ignored patterns', async () => {
|
||||||
|
const testFilePath = path.join(testFolder, 'test.jpg');
|
||||||
|
const ignoredPattern = 'ignored';
|
||||||
|
const ignoredFolder = path.join(testFolder, ignoredPattern);
|
||||||
|
await fs.promises.mkdir(ignoredFolder, { recursive: true });
|
||||||
|
const ignoredFilePath = path.join(ignoredFolder, 'ignored.jpg');
|
||||||
|
|
||||||
|
await startWatch([testFolder], { concurrency: 1, ignore: ignoredPattern }, { batchSize: 1, debounceTimeMs: 10 });
|
||||||
|
await sleep(100); // to debounce the watcher from considering the test file as a existing file
|
||||||
|
await fs.promises.writeFile(testFilePath, 'testjpg');
|
||||||
|
await fs.promises.writeFile(ignoredFilePath, 'ignoredjpg');
|
||||||
|
|
||||||
|
await vi.waitUntil(() => checkBulkUploadMocked.mock.calls.length > 0, 3000);
|
||||||
|
expect(checkBulkUpload).toHaveBeenCalledWith({
|
||||||
|
assetBulkUploadCheckDto: {
|
||||||
|
assets: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: testFilePath,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(checkBulkUpload).not.toHaveBeenCalledWith({
|
||||||
|
assetBulkUploadCheckDto: {
|
||||||
|
assets: expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
id: ignoredFilePath,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await fs.promises.rm(testFolder, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -12,13 +12,18 @@ import {
|
|||||||
getSupportedMediaTypes,
|
getSupportedMediaTypes,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import byteSize from 'byte-size';
|
import byteSize from 'byte-size';
|
||||||
|
import { Matcher, watch as watchFs } from 'chokidar';
|
||||||
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
import { MultiBar, Presets, SingleBar } from 'cli-progress';
|
||||||
import { chunk } from 'lodash-es';
|
import { chunk } from 'lodash-es';
|
||||||
|
import micromatch from 'micromatch';
|
||||||
import { Stats, createReadStream } from 'node:fs';
|
import { Stats, createReadStream } from 'node:fs';
|
||||||
import { stat, unlink } from 'node:fs/promises';
|
import { stat, unlink } from 'node:fs/promises';
|
||||||
import path, { basename } from 'node:path';
|
import path, { basename } from 'node:path';
|
||||||
import { Queue } from 'src/queue';
|
import { Queue } from 'src/queue';
|
||||||
import { BaseOptions, authenticate, crawl, sha1 } from 'src/utils';
|
import { BaseOptions, Batcher, authenticate, crawl, sha1 } from 'src/utils';
|
||||||
|
|
||||||
|
const UPLOAD_WATCH_BATCH_SIZE = 100;
|
||||||
|
const UPLOAD_WATCH_DEBOUNCE_TIME_MS = 10_000;
|
||||||
|
|
||||||
const s = (count: number) => (count === 1 ? '' : 's');
|
const s = (count: number) => (count === 1 ? '' : 's');
|
||||||
|
|
||||||
@@ -36,6 +41,8 @@ export interface UploadOptionsDto {
|
|||||||
albumName?: string;
|
albumName?: string;
|
||||||
includeHidden?: boolean;
|
includeHidden?: boolean;
|
||||||
concurrency: number;
|
concurrency: number;
|
||||||
|
progress?: boolean;
|
||||||
|
watch?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadFile extends File {
|
class UploadFile extends File {
|
||||||
@@ -55,19 +62,94 @@ class UploadFile extends File {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
|
||||||
|
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
||||||
|
const newAssets = await uploadFiles(newFiles, options);
|
||||||
|
await updateAlbums([...newAssets, ...duplicates], options);
|
||||||
|
await deleteFiles(newFiles, options);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const startWatch = async (
|
||||||
|
paths: string[],
|
||||||
|
options: UploadOptionsDto,
|
||||||
|
{
|
||||||
|
batchSize = UPLOAD_WATCH_BATCH_SIZE,
|
||||||
|
debounceTimeMs = UPLOAD_WATCH_DEBOUNCE_TIME_MS,
|
||||||
|
}: { batchSize?: number; debounceTimeMs?: number } = {},
|
||||||
|
) => {
|
||||||
|
const watcherIgnored: Matcher[] = [];
|
||||||
|
const { image, video } = await getSupportedMediaTypes();
|
||||||
|
const extensions = new Set([...image, ...video]);
|
||||||
|
|
||||||
|
if (options.ignore) {
|
||||||
|
watcherIgnored.push((path) => micromatch.contains(path, `**/${options.ignore}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathsBatcher = new Batcher<string>({
|
||||||
|
batchSize,
|
||||||
|
debounceTimeMs,
|
||||||
|
onBatch: async (paths: string[]) => {
|
||||||
|
const uniquePaths = [...new Set(paths)];
|
||||||
|
await uploadBatch(uniquePaths, options);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const onFile = async (path: string, stats?: Stats) => {
|
||||||
|
if (stats?.isDirectory()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const ext = '.' + path.split('.').pop()?.toLowerCase();
|
||||||
|
if (!ext || !extensions.has(ext)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options.progress) {
|
||||||
|
// logging when progress is disabled as it can cause issues with the progress bar rendering
|
||||||
|
console.log(`Change detected: ${path}`);
|
||||||
|
}
|
||||||
|
pathsBatcher.add(path);
|
||||||
|
};
|
||||||
|
const fsWatcher = watchFs(paths, {
|
||||||
|
ignoreInitial: true,
|
||||||
|
ignored: watcherIgnored,
|
||||||
|
alwaysStat: true,
|
||||||
|
awaitWriteFinish: true,
|
||||||
|
depth: options.recursive ? undefined : 1,
|
||||||
|
persistent: true,
|
||||||
|
})
|
||||||
|
.on('add', onFile)
|
||||||
|
.on('change', onFile)
|
||||||
|
.on('error', (error) => console.error(`Watcher error: ${error}`));
|
||||||
|
|
||||||
|
process.on('SIGINT', async () => {
|
||||||
|
console.log('Exiting...');
|
||||||
|
await fsWatcher.close();
|
||||||
|
process.exit();
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
export const upload = async (paths: string[], baseOptions: BaseOptions, options: UploadOptionsDto) => {
|
||||||
await authenticate(baseOptions);
|
await authenticate(baseOptions);
|
||||||
|
|
||||||
const scanFiles = await scan(paths, options);
|
const scanFiles = await scan(paths, options);
|
||||||
|
|
||||||
if (scanFiles.length === 0) {
|
if (scanFiles.length === 0) {
|
||||||
console.log('No files found, exiting');
|
if (options.watch) {
|
||||||
return;
|
console.log('No files found initially.');
|
||||||
|
} else {
|
||||||
|
console.log('No files found, exiting');
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { newFiles, duplicates } = await checkForDuplicates(scanFiles, options);
|
if (options.watch) {
|
||||||
const newAssets = await uploadFiles(newFiles, options);
|
console.log('Watching for changes...');
|
||||||
await updateAlbums([...newAssets, ...duplicates], options);
|
await startWatch(paths, options);
|
||||||
await deleteFiles(newFiles, options);
|
// watcher does not handle the initial scan
|
||||||
|
// as the scan() is a more efficient quick start with batched results
|
||||||
|
}
|
||||||
|
|
||||||
|
await uploadBatch(scanFiles, options);
|
||||||
};
|
};
|
||||||
|
|
||||||
const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
||||||
@@ -85,19 +167,25 @@ const scan = async (pathsToCrawl: string[], options: UploadOptionsDto) => {
|
|||||||
return files;
|
return files;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const checkForDuplicates = async (files: string[], { concurrency, skipHash }: UploadOptionsDto) => {
|
export const checkForDuplicates = async (files: string[], { concurrency, skipHash, progress }: UploadOptionsDto) => {
|
||||||
if (skipHash) {
|
if (skipHash) {
|
||||||
console.log('Skipping hash check, assuming all files are new');
|
console.log('Skipping hash check, assuming all files are new');
|
||||||
return { newFiles: files, duplicates: [] };
|
return { newFiles: files, duplicates: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
const multiBar = new MultiBar(
|
let multiBar: MultiBar | undefined;
|
||||||
{ format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
|
||||||
Presets.shades_classic,
|
|
||||||
);
|
|
||||||
|
|
||||||
const hashProgressBar = multiBar.create(files.length, 0, { message: 'Hashing files ' });
|
if (progress) {
|
||||||
const checkProgressBar = multiBar.create(files.length, 0, { message: 'Checking for duplicates' });
|
multiBar = new MultiBar(
|
||||||
|
{ format: '{message} | {bar} | {percentage}% | ETA: {eta}s | {value}/{total} assets' },
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`Received ${files.length} files, hashing...`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashProgressBar = multiBar?.create(files.length, 0, { message: 'Hashing files ' });
|
||||||
|
const checkProgressBar = multiBar?.create(files.length, 0, { message: 'Checking for duplicates' });
|
||||||
|
|
||||||
const newFiles: string[] = [];
|
const newFiles: string[] = [];
|
||||||
const duplicates: Asset[] = [];
|
const duplicates: Asset[] = [];
|
||||||
@@ -117,7 +205,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkProgressBar.increment(assets.length);
|
checkProgressBar?.increment(assets.length);
|
||||||
},
|
},
|
||||||
{ concurrency, retry: 3 },
|
{ concurrency, retry: 3 },
|
||||||
);
|
);
|
||||||
@@ -137,7 +225,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
|||||||
void checkBulkUploadQueue.push(batch);
|
void checkBulkUploadQueue.push(batch);
|
||||||
}
|
}
|
||||||
|
|
||||||
hashProgressBar.increment();
|
hashProgressBar?.increment();
|
||||||
return results;
|
return results;
|
||||||
},
|
},
|
||||||
{ concurrency, retry: 3 },
|
{ concurrency, retry: 3 },
|
||||||
@@ -155,7 +243,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
|||||||
|
|
||||||
await checkBulkUploadQueue.drained();
|
await checkBulkUploadQueue.drained();
|
||||||
|
|
||||||
multiBar.stop();
|
multiBar?.stop();
|
||||||
|
|
||||||
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
|
console.log(`Found ${newFiles.length} new files and ${duplicates.length} duplicate${s(duplicates.length)}`);
|
||||||
|
|
||||||
@@ -171,7 +259,10 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
|||||||
return { newFiles, duplicates };
|
return { newFiles, duplicates };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptionsDto): Promise<Asset[]> => {
|
export const uploadFiles = async (
|
||||||
|
files: string[],
|
||||||
|
{ dryRun, concurrency, progress }: UploadOptionsDto,
|
||||||
|
): Promise<Asset[]> => {
|
||||||
if (files.length === 0) {
|
if (files.length === 0) {
|
||||||
console.log('All assets were already uploaded, nothing to do.');
|
console.log('All assets were already uploaded, nothing to do.');
|
||||||
return [];
|
return [];
|
||||||
@@ -191,12 +282,20 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo
|
|||||||
return files.map((filepath) => ({ id: '', filepath }));
|
return files.map((filepath) => ({ id: '', filepath }));
|
||||||
}
|
}
|
||||||
|
|
||||||
const uploadProgress = new SingleBar(
|
let uploadProgress: SingleBar | undefined;
|
||||||
{ format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}' },
|
|
||||||
Presets.shades_classic,
|
if (progress) {
|
||||||
);
|
uploadProgress = new SingleBar(
|
||||||
uploadProgress.start(totalSize, 0);
|
{
|
||||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
format: 'Uploading assets | {bar} | {percentage}% | ETA: {eta_formatted} | {value_formatted}/{total_formatted}',
|
||||||
|
},
|
||||||
|
Presets.shades_classic,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
console.log(`Uploading ${files.length} asset${s(files.length)} (${byteSize(totalSize)})`);
|
||||||
|
}
|
||||||
|
uploadProgress?.start(totalSize, 0);
|
||||||
|
uploadProgress?.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||||
|
|
||||||
let duplicateCount = 0;
|
let duplicateCount = 0;
|
||||||
let duplicateSize = 0;
|
let duplicateSize = 0;
|
||||||
@@ -222,7 +321,7 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo
|
|||||||
successSize += stats.size ?? 0;
|
successSize += stats.size ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadProgress.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
uploadProgress?.update(successSize, { value_formatted: byteSize(successSize + duplicateSize) });
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
},
|
},
|
||||||
@@ -235,7 +334,7 @@ export const uploadFiles = async (files: string[], { dryRun, concurrency }: Uplo
|
|||||||
|
|
||||||
await queue.drained();
|
await queue.drained();
|
||||||
|
|
||||||
uploadProgress.stop();
|
uploadProgress?.stop();
|
||||||
|
|
||||||
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
|
console.log(`Successfully uploaded ${successCount} new asset${s(successCount)} (${byteSize(successSize)})`);
|
||||||
if (duplicateCount > 0) {
|
if (duplicateCount > 0) {
|
||||||
|
|||||||
@@ -69,6 +69,13 @@ program
|
|||||||
.default(4),
|
.default(4),
|
||||||
)
|
)
|
||||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||||
|
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
|
||||||
|
.addOption(
|
||||||
|
new Option('--watch', 'Watch for changes and upload automatically')
|
||||||
|
.env('IMMICH_WATCH_CHANGES')
|
||||||
|
.default(false)
|
||||||
|
.implies({ progress: false }),
|
||||||
|
)
|
||||||
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
.argument('[paths...]', 'One or more paths to assets to be uploaded')
|
||||||
.action((paths, options) => upload(paths, program.opts(), options));
|
.action((paths, options) => upload(paths, program.opts(), options));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import mockfs from 'mock-fs';
|
import mockfs from 'mock-fs';
|
||||||
import { readFileSync } from 'node:fs';
|
import { readFileSync } from 'node:fs';
|
||||||
import { CrawlOptions, crawl } from 'src/utils';
|
import { Batcher, CrawlOptions, crawl } from 'src/utils';
|
||||||
|
import { Mock } from 'vitest';
|
||||||
|
|
||||||
interface Test {
|
interface Test {
|
||||||
test: string;
|
test: string;
|
||||||
@@ -303,3 +304,38 @@ describe('crawl', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Batcher', () => {
|
||||||
|
let batcher: Batcher;
|
||||||
|
let onBatch: Mock;
|
||||||
|
beforeEach(() => {
|
||||||
|
onBatch = vi.fn();
|
||||||
|
batcher = new Batcher({ batchSize: 2, onBatch });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onBatch() when a batch limit is reached', async () => {
|
||||||
|
batcher.add('a');
|
||||||
|
batcher.add('b');
|
||||||
|
batcher.add('c');
|
||||||
|
expect(onBatch).toHaveBeenCalledOnce();
|
||||||
|
expect(onBatch).toHaveBeenCalledWith(['a', 'b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onBatch() when flush() is called', async () => {
|
||||||
|
batcher.add('a');
|
||||||
|
batcher.flush();
|
||||||
|
expect(onBatch).toHaveBeenCalledOnce();
|
||||||
|
expect(onBatch).toHaveBeenCalledWith(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onBatch() when debounce time reached', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
batcher = new Batcher({ batchSize: 2, debounceTimeMs: 100, onBatch });
|
||||||
|
batcher.add('a');
|
||||||
|
expect(onBatch).not.toHaveBeenCalled();
|
||||||
|
vi.advanceTimersByTime(200);
|
||||||
|
expect(onBatch).toHaveBeenCalledOnce();
|
||||||
|
expect(onBatch).toHaveBeenCalledWith(['a']);
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -172,3 +172,64 @@ export const sha1 = (filepath: string) => {
|
|||||||
rs.on('end', () => resolve(hash.digest('hex')));
|
rs.on('end', () => resolve(hash.digest('hex')));
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Batches items and calls onBatch to process them
|
||||||
|
* when the batch size is reached or the debounce time has passed.
|
||||||
|
*/
|
||||||
|
export class Batcher<T = unknown> {
|
||||||
|
private items: T[] = [];
|
||||||
|
private readonly batchSize: number;
|
||||||
|
private readonly debounceTimeMs?: number;
|
||||||
|
private readonly onBatch: (items: T[]) => void;
|
||||||
|
private debounceTimer?: NodeJS.Timeout;
|
||||||
|
|
||||||
|
constructor({
|
||||||
|
batchSize,
|
||||||
|
debounceTimeMs,
|
||||||
|
onBatch,
|
||||||
|
}: {
|
||||||
|
batchSize: number;
|
||||||
|
debounceTimeMs?: number;
|
||||||
|
onBatch: (items: T[]) => Promise<void>;
|
||||||
|
}) {
|
||||||
|
this.batchSize = batchSize;
|
||||||
|
this.debounceTimeMs = debounceTimeMs;
|
||||||
|
this.onBatch = onBatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setDebounceTimer() {
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
}
|
||||||
|
if (this.debounceTimeMs) {
|
||||||
|
this.debounceTimer = setTimeout(() => this.flush(), this.debounceTimeMs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private clearDebounceTimer() {
|
||||||
|
if (this.debounceTimer) {
|
||||||
|
clearTimeout(this.debounceTimer);
|
||||||
|
this.debounceTimer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
add(item: T) {
|
||||||
|
this.items.push(item);
|
||||||
|
this.setDebounceTimer();
|
||||||
|
if (this.items.length >= this.batchSize) {
|
||||||
|
this.flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flush() {
|
||||||
|
this.clearDebounceTimer();
|
||||||
|
if (this.items.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onBatch(this.items);
|
||||||
|
|
||||||
|
this.items = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,37 +2,37 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.50.0"
|
version = "4.52.0"
|
||||||
constraints = "4.50.0"
|
constraints = "4.52.0"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
|
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||||
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
|
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||||
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
|
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||||
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
|
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||||
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
|
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||||
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
|
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||||
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
|
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||||
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
|
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||||
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
|
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||||
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
|
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||||
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
|
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||||
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
|
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||||
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
|
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||||
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
|
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||||
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
|
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||||
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
|
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||||
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
|
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||||
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
|
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||||
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
|
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||||
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
|
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||||
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
|
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||||
|
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
|
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||||
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
|
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||||
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
|
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||||
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
|
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||||
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
|
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||||
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
|
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||||
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.50.0"
|
version = "4.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,37 +2,37 @@
|
|||||||
# Manual edits may be lost in future updates.
|
# Manual edits may be lost in future updates.
|
||||||
|
|
||||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||||
version = "4.50.0"
|
version = "4.52.0"
|
||||||
constraints = "4.50.0"
|
constraints = "4.52.0"
|
||||||
hashes = [
|
hashes = [
|
||||||
"h1:0qvD5ZKn2tMZ8cOjQrUSITIC9tKCZbrSaSswV9lOyiU=",
|
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||||
"h1:4N0gplrZ0zOsJv3Kx1VfIx2FwrZHbYU0Un2yfiLZIGQ=",
|
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||||
"h1:81AMQq4kNKU/35U8ElQegUxG4E6xB0erIjG5xVmjIyo=",
|
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||||
"h1:EEQNADUmV3IL6x00yzy04i7OCSLeOMgM9XQkV3w71gA=",
|
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||||
"h1:HD0KI7td6oiSSAnJNn8UPSGf+hKiTo4JVQYfAiU1SqM=",
|
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||||
"h1:Hl+o5LtcvZg2f3l1hh9vaG/DFK6k+dTIZSeM0lXyfpo=",
|
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||||
"h1:ZUO2oIJ6jtZdvl816h0cEIiIeZ/fFCF64+abGEVxZZM=",
|
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||||
"h1:Zio80fnEeUKdlSOhTVskMEFSLUQ6TMsMKnXc+Dy2P2A=",
|
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||||
"h1:aLLvg36evTyqjtXGV2MjAV8imktXFmry7p/xCu9GQC4=",
|
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||||
"h1:azL05eWyy2V8SWkbZZImPWvv8ynG4eqmrbZhjXBDFug=",
|
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||||
"h1:ckMysHY4fJmr7o58XMi+DdgOTB/U/Mf1u1JA9ly3g/I=",
|
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||||
"h1:jxOwjDNjt5WCb4YjjiMsman91O8Y+MAPz6UwJ4a6F+0=",
|
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||||
"h1:u4OfnjSLa4Wk1IUFAzrvMnGgr8MvRHEWVDHEScPK2E8=",
|
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||||
"h1:wQkR1oeSkzlHn3rnVuLJRJLBHlg4EHt7Y64DeTjfkjQ=",
|
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||||
"zh:0ef99ed39472a94e6a0d6fa733cf0a46bce3bf66eba2873efae8846efdddc237",
|
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||||
"zh:2929cbbffcead171d45c88e4a7a59e9c013ea775dafa68b10da8db7cd04b6140",
|
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||||
"zh:462601c87118088e1a718842e367af7d8e7620598d426980a6d6b33de759865e",
|
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||||
"zh:56766eb62a74a9d88d9efb8486dd3a0c5c9db873d0a980ae9ef1e8af27d74231",
|
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||||
"zh:6b4e8810d99498a5a20a5872982a0f1354e79cfc4a7dfe7cc656f1c7eaae47d8",
|
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||||
"zh:6d65bdb4ec94b6eecc8abe26d94e2ca09262dc1e7a9934db829f418be0119920",
|
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||||
"zh:71adeaf31e41a358ec6095004062e43f56ee7d4b2504e5613ab351d511695641",
|
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||||
|
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||||
"zh:89761c15908ccc2cf9c50bb5cb3be45d3ad0c45fc7c608c6b95f48c0288b7160",
|
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||||
"zh:8cc5d7c5939da89cfd01f3e51c84f3576564783acea9db86bd9e32049805ed96",
|
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||||
"zh:987cff8225b1dd436cdcb4fc6228689ae7e4281de6896412a2a9a3325c49f05e",
|
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||||
"zh:991e83ebb89867d71e01a1c215ed159efb425683b0a44707be8579eb0a337f06",
|
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||||
"zh:ab8177ae2d8f5cfa90043a6f867435012cae115f6061b832a7e2462e0ae87a67",
|
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||||
"zh:d1ca34df1398f201274a6a18102975148c10ca15aa43cfc56cc9897620929509",
|
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||||
"zh:d34946f70201baf6dda03e3b294c6bbe40d95d0278e97b9f636ded94822b24ac",
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ terraform {
|
|||||||
required_providers {
|
required_providers {
|
||||||
cloudflare = {
|
cloudflare = {
|
||||||
source = "cloudflare/cloudflare"
|
source = "cloudflare/cloudflare"
|
||||||
version = "4.50.0"
|
version = "4.52.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,13 @@
|
|||||||
# See:
|
#
|
||||||
|
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||||
|
#
|
||||||
|
# Make sure to use the docker-compose.yml of the current release:
|
||||||
|
#
|
||||||
|
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||||
|
#
|
||||||
|
# The compose file on main may not be compatible with the latest release.
|
||||||
|
|
||||||
|
# For development see:
|
||||||
# - https://immich.app/docs/developer/setup
|
# - https://immich.app/docs/developer/setup
|
||||||
# - https://immich.app/docs/developer/troubleshooting
|
# - https://immich.app/docs/developer/troubleshooting
|
||||||
|
|
||||||
@@ -86,12 +95,12 @@ services:
|
|||||||
image: immich-machine-learning-dev:latest
|
image: immich-machine-learning-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference
|
||||||
build:
|
build:
|
||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
- DEVICE=cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference
|
||||||
ports:
|
ports:
|
||||||
- 3003:3003
|
- 3003:3003
|
||||||
volumes:
|
volumes:
|
||||||
@@ -107,13 +116,13 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1,3 +1,12 @@
|
|||||||
|
#
|
||||||
|
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||||
|
#
|
||||||
|
# Make sure to use the docker-compose.yml of the current release:
|
||||||
|
#
|
||||||
|
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||||
|
#
|
||||||
|
# The compose file on main may not be compatible with the latest release.
|
||||||
|
|
||||||
name: immich-prod
|
name: immich-prod
|
||||||
|
|
||||||
services:
|
services:
|
||||||
@@ -29,12 +38,12 @@ services:
|
|||||||
image: immich-machine-learning:latest
|
image: immich-machine-learning:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference
|
||||||
build:
|
build:
|
||||||
context: ../machine-learning
|
context: ../machine-learning
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
args:
|
args:
|
||||||
- DEVICE=cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference
|
- DEVICE=cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference
|
||||||
ports:
|
ports:
|
||||||
- 3003:3003
|
- 3003:3003
|
||||||
volumes:
|
volumes:
|
||||||
@@ -47,14 +56,14 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -91,7 +100,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:6559acbd5d770b15bb3c954629ce190ac3cbbdb2b7f1c30f0385c4e05104e218
|
image: prom/prometheus@sha256:6927e0919a144aa7616fd0137d4816816d42f6b816de3af269ab065250859a62
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -103,7 +112,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
|
image: grafana/grafana:11.5.2-ubuntu@sha256:8b5858c447e06fd7a89006b562ba7bba7c4d5813600c7982374c41852adefaeb
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
#
|
#
|
||||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||||
|
#
|
||||||
|
# Make sure to use the docker-compose.yml of the current release:
|
||||||
#
|
#
|
||||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||||
#
|
#
|
||||||
# The compose file on main may not be compatible with the latest release.
|
# The compose file on main may not be compatible with the latest release.
|
||||||
#
|
|
||||||
|
|
||||||
name: immich
|
name: immich
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ services:
|
|||||||
|
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino] to the image tag.
|
||||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||||
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
# extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
# service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
env_file:
|
env_file:
|
||||||
@@ -48,14 +49,14 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
|||||||
@@ -26,6 +26,13 @@ services:
|
|||||||
capabilities:
|
capabilities:
|
||||||
- gpu
|
- gpu
|
||||||
|
|
||||||
|
rocm:
|
||||||
|
group_add:
|
||||||
|
- video
|
||||||
|
devices:
|
||||||
|
- /dev/dri:/dev/dri
|
||||||
|
- /dev/kfd:/dev/kfd
|
||||||
|
|
||||||
openvino:
|
openvino:
|
||||||
device_cgroup_rules:
|
device_cgroup_rules:
|
||||||
- 'c 189:* rmw'
|
- 'c 189:* rmw'
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ services:
|
|||||||
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
|
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
|
||||||
devices:
|
devices:
|
||||||
- /dev/dri:/dev/dri
|
- /dev/dri:/dev/dri
|
||||||
|
- /dev/dxg:/dev/dxg
|
||||||
volumes:
|
volumes:
|
||||||
- /usr/lib/wsl:/usr/lib/wsl
|
- /usr/lib/wsl:/usr/lib/wsl
|
||||||
environment:
|
environment:
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.13.1
|
22.14.0
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ Instead of these experimental features, we recommend using the URL switching fea
|
|||||||
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
|
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
|
||||||
Please discuss any large PRs with our dev team to ensure your time is not wasted.
|
Please discuss any large PRs with our dev team to ensure your time is not wasted.
|
||||||
|
|
||||||
|
### Why isn't the mobile app updated yet?
|
||||||
|
|
||||||
|
The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Assets
|
## Assets
|
||||||
@@ -93,7 +97,7 @@ Make sure to [set your reverse proxy](/docs/administration/reverse-proxy/) to al
|
|||||||
Also, check the disk space of your reverse proxy.
|
Also, check the disk space of your reverse proxy.
|
||||||
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
In some cases, proxies cache requests to disk before passing them on, and if disk space runs out, the request fails.
|
||||||
|
|
||||||
If you are using Cloudflare Tunnel, please know that they set a maxiumum filesize of 100 MB that cannot be changed.
|
If you are using Cloudflare Tunnel, please know that they set a maximum filesize of 100 MB that cannot be changed.
|
||||||
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
|
At times, files larger than this may work, potentially up to 1 GB. However, the official limit is 100 MB.
|
||||||
If you are having issues, we recommend switching to a different network deployment.
|
If you are having issues, we recommend switching to a different network deployment.
|
||||||
|
|
||||||
@@ -166,7 +170,7 @@ If you aren't able to or prefer not to mount Samba on the host (such as Windows
|
|||||||
Below is an example in the `docker-compose.yml`.
|
Below is an example in the `docker-compose.yml`.
|
||||||
|
|
||||||
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
||||||
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
correlates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
||||||
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
||||||
|
|
||||||
```diff
|
```diff
|
||||||
|
|||||||
@@ -77,9 +77,7 @@ docker start immich_postgres # Start Postgres server
|
|||||||
sleep 10 # Wait for Postgres server to start up
|
sleep 10 # Wait for Postgres server to start up
|
||||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||||
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
|
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
|
||||||
cat < "/dump.sql" \
|
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
||||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
|
||||||
| psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
|
||||||
exit # Exit the Docker shell
|
exit # Exit the Docker shell
|
||||||
docker compose up -d # Start remainder of Immich apps
|
docker compose up -d # Start remainder of Immich apps
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -70,4 +70,4 @@ When installing a new version of pgvecto.rs, you will need to manually update th
|
|||||||
|
|
||||||
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
||||||
|
|
||||||
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
|
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ The default Immich log level is `Log` (commonly known as `Info`). The Immich adm
|
|||||||
Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters
|
Through this setting, you can manage all the settings related to machine learning in Immich, from the setting of remote machine learning to the model and its parameters
|
||||||
You can choose to disable a certain type of machine learning, for example smart search or facial recognition.
|
You can choose to disable a certain type of machine learning, for example smart search or facial recognition.
|
||||||
|
|
||||||
|
### URL
|
||||||
|
|
||||||
|
The built in (`http://immich-machine-learning:3003`) machine learning server will be configured by default, but you can change this or add additional servers.
|
||||||
|
|
||||||
|
Hosting the `immich-machine-learning` container on a machine with a more powerful GPU can be helpful to for processing a large number of photos (such as during batch import) or for faster search.
|
||||||
|
|
||||||
|
If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.
|
||||||
|
|
||||||
### Smart Search
|
### Smart Search
|
||||||
|
|
||||||
The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
|
The [smart search](/docs/features/searching) settings allow you to change the [CLIP model](https://openai.com/research/clip). Larger models will typically provide [more accurate search results](https://github.com/immich-app/immich/discussions/11862) but consume more processing power and RAM. When [changing the CLIP model](/docs/FAQ#can-i-use-a-custom-clip-model) it is mandatory to re-run the Smart Search job on all images to fully apply the change.
|
||||||
|
|||||||
@@ -50,19 +50,18 @@ The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users contr
|
|||||||
|
|
||||||
The Immich backend is divided into several services, which are run as individual docker containers.
|
The Immich backend is divided into several services, which are run as individual docker containers.
|
||||||
|
|
||||||
1. `immich-server` - Handle and respond to REST API requests
|
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
||||||
1. `immich-microservices` - Execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
|
||||||
1. `immich-machine-learning` - Execute machine learning models
|
1. `immich-machine-learning` - Execute machine learning models
|
||||||
1. `postgres` - Persistent data storage
|
1. `postgres` - Persistent data storage
|
||||||
1. `redis`- Queue management for `immich-microservices`
|
1. `redis`- Queue management for background jobs
|
||||||
|
|
||||||
### Immich Server
|
### Immich Server
|
||||||
|
|
||||||
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, with [TypeORM](https://typeorm.io/) for database management. The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`infra/`) from core business logic (`domain/`).
|
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, [Express](https://expressjs.com/) server, and the query builder [Kysely](https://kysely.dev/). The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`src/repositories`) from core business logic (`src/services`).
|
||||||
|
|
||||||
#### REST Endpoints
|
### API Endpoints
|
||||||
|
|
||||||
The server is a list of HTTP endpoints and associated handlers (controllers). Each controller usually implements the following CRUD operations:
|
An incoming HTTP request is mapped to a controller (`src/controllers`). Controllers are collections of HTTP endpoints. Each controller usually implements the following CRUD operations for its respective resource type:
|
||||||
|
|
||||||
- `POST` `/<type>` - **Create**
|
- `POST` `/<type>` - **Create**
|
||||||
- `GET` `/<type>` - **Read** (all)
|
- `GET` `/<type>` - **Read** (all)
|
||||||
@@ -70,13 +69,13 @@ The server is a list of HTTP endpoints and associated handlers (controllers). Ea
|
|||||||
- `PUT` `/<type>/:id` - **Updated** (by id)
|
- `PUT` `/<type>/:id` - **Updated** (by id)
|
||||||
- `DELETE` `/<type>/:id` - **Delete** (by id)
|
- `DELETE` `/<type>/:id` - **Delete** (by id)
|
||||||
|
|
||||||
#### DTOs
|
### Domain Transfer Objects (DTOs)
|
||||||
|
|
||||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
||||||
|
|
||||||
### Microservices
|
### Background Jobs
|
||||||
|
|
||||||
The Immich Microservices image uses the same `Dockerfile` as the Immich Server, but with a different entrypoint. The Immich Microservices service mainly handles executing jobs, which include the following:
|
Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src/utils/misc.ts#L266) to run background jobs. These jobs include:
|
||||||
|
|
||||||
- Thumbnail Generation
|
- Thumbnail Generation
|
||||||
- Metadata Extraction
|
- Metadata Extraction
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
|||||||
|
|
||||||
### Mobile app
|
### Mobile app
|
||||||
|
|
||||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
|
||||||
|
|
||||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,8 @@ Navigating to Administration > Settings > Machine Learning Settings > Facial Rec
|
|||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa.
|
It's better to only tweak the parameters here than to set them to something very different unless you're ready to test a variety of options. If you do need to set a parameter to a strict setting, relaxing other settings can be a good option to compensate, and vice versa.
|
||||||
|
|
||||||
|
You can learn how the tune the result in this [Guide](/docs/guides/better-facial-clusters)
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Facial recognition model
|
### Facial recognition model
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ If your photos are on a network drive, automatic file watching likely won't work
|
|||||||
|
|
||||||
#### Troubleshooting
|
#### Troubleshooting
|
||||||
|
|
||||||
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watches` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
||||||
|
|
||||||
```
|
```
|
||||||
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'
|
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'
|
||||||
@@ -68,7 +68,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
|
|||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion.
|
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -111,11 +111,10 @@ These actions must be performed by the Immich administrator.
|
|||||||
- Click on Administration -> Libraries
|
- Click on Administration -> Libraries
|
||||||
- Click on Create External Library
|
- Click on Create External Library
|
||||||
- Select which user owns the library, this can not be changed later
|
- Select which user owns the library, this can not be changed later
|
||||||
|
- Enter `/mnt/media/christmas-trip` then click Add
|
||||||
|
- Click on Save
|
||||||
- Click the drop-down menu on the newly created library
|
- Click the drop-down menu on the newly created library
|
||||||
- Click on Rename Library and rename it to "Christmas Trip"
|
- Click on Rename Library and rename it to "Christmas Trip"
|
||||||
- Click Edit Import Paths
|
|
||||||
- Click on Add Path
|
|
||||||
- Enter `/mnt/media/christmas-trip` then click Add
|
|
||||||
|
|
||||||
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
|
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
|||||||
|
|
||||||
- ARM NN (Mali)
|
- ARM NN (Mali)
|
||||||
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
|
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
|
||||||
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
|
- ROCm (AMD GPUs)
|
||||||
|
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
|
||||||
|
|
||||||
## Limitations
|
## Limitations
|
||||||
|
|
||||||
@@ -41,21 +42,26 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
|||||||
- The installed driver must be >= 535 (it must support CUDA 12.2).
|
- The installed driver must be >= 535 (it must support CUDA 12.2).
|
||||||
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
|
- On Linux (except for WSL2), you also need to have [NVIDIA Container Toolkit][nvct] installed.
|
||||||
|
|
||||||
|
#### ROCm
|
||||||
|
|
||||||
|
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`.
|
||||||
|
|
||||||
#### OpenVINO
|
#### OpenVINO
|
||||||
|
|
||||||
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
|
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
|
||||||
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
|
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
|
||||||
|
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
|
||||||
|
|
||||||
## Setup
|
## Setup
|
||||||
|
|
||||||
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
1. If you do not already have it, download the latest [`hwaccel.ml.yml`][hw-file] file and ensure it's in the same folder as the `docker-compose.yml`.
|
||||||
2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
2. In the `docker-compose.yml` under `immich-machine-learning`, uncomment the `extends` section and change `cpu` to the appropriate backend.
|
||||||
3. Still in `immich-machine-learning`, add one of -[armnn, cuda, openvino] to the `image` section's tag at the end of the line.
|
3. Still in `immich-machine-learning`, add one of -[armnn, cuda, rocm, openvino] to the `image` section's tag at the end of the line.
|
||||||
4. Redeploy the `immich-machine-learning` container with these updated settings.
|
4. Redeploy the `immich-machine-learning` container with these updated settings.
|
||||||
|
|
||||||
### Confirming Device Usage
|
### Confirming Device Usage
|
||||||
|
|
||||||
You can confirm the device is being recognized and used by checking its utilization. There are many tools to display this, such as `nvtop` for NVIDIA or Intel and `intel_gpu_top` for Intel.
|
You can confirm the device is being recognized and used by checking its utilization. There are many tools to display this, such as `nvtop` for NVIDIA or Intel, `intel_gpu_top` for Intel, and `radeontop` for AMD.
|
||||||
|
|
||||||
You can also check the logs of the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, or when you search with text in Immich, you should either see a log for `Available ORT providers` containing the relevant provider (e.g. `CUDAExecutionProvider` in the case of CUDA), or a `Loaded ANN model` log entry without errors in the case of ARM NN.
|
You can also check the logs of the `immich-machine-learning` container. When a Smart Search or Face Detection job begins, or when you search with text in Immich, you should either see a log for `Available ORT providers` containing the relevant provider (e.g. `CUDAExecutionProvider` in the case of CUDA), or a `Loaded ANN model` log entry without errors in the case of ARM NN.
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ The filters smart search allows you to search by include:
|
|||||||
- Not in any album
|
- Not in any album
|
||||||
- Archived
|
- Archived
|
||||||
- Favorited
|
- Favorited
|
||||||
|
- Rating
|
||||||
|
|
||||||
<Tabs>
|
<Tabs>
|
||||||
<TabItem value="Computer" label="Computer" default>
|
<TabItem value="Computer" label="Computer" default>
|
||||||
|
|||||||
@@ -8,22 +8,23 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
|
|
||||||
## Image formats
|
## Image formats
|
||||||
|
|
||||||
| Format | Extension(s) | Supported? | Notes |
|
| Format | Extension(s) | Supported? | Notes |
|
||||||
| :-------- | :---------------------------- | :----------------: | :-------------- |
|
| :---------- | :---------------------------- | :----------------: | :-------------- |
|
||||||
| `AVIF` | `.avif` | :white_check_mark: | |
|
| `AVIF` | `.avif` | :white_check_mark: | |
|
||||||
| `BMP` | `.bmp` | :white_check_mark: | |
|
| `BMP` | `.bmp` | :white_check_mark: | |
|
||||||
| `GIF` | `.gif` | :white_check_mark: | |
|
| `GIF` | `.gif` | :white_check_mark: | |
|
||||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||||
| `PNG` | `.webp` | :white_check_mark: | |
|
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
| `PNG` | `.webp` | :white_check_mark: | |
|
||||||
| `RAW` | `.raw` | :white_check_mark: | |
|
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||||
| `RW2` | `.rw2` | :white_check_mark: | |
|
| `RAW` | `.raw` | :white_check_mark: | |
|
||||||
| `SVG` | `.svg` | :white_check_mark: | |
|
| `RW2` | `.rw2` | :white_check_mark: | |
|
||||||
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
|
| `SVG` | `.svg` | :white_check_mark: | |
|
||||||
| `WEBP` | `.webp` | :white_check_mark: | |
|
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
|
||||||
|
| `WEBP` | `.webp` | :white_check_mark: | |
|
||||||
|
|
||||||
## Video formats
|
## Video formats
|
||||||
|
|
||||||
|
|||||||
72
docs/docs/guides/better-facial-clusters.md
Normal file
72
docs/docs/guides/better-facial-clusters.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# Better Facial Recognition Clusters
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This guide explains how to optimize facial recognition in systems with large image libraries. By following these steps, you'll achieve better clustering of faces, reducing the need for manual merging.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Important Notes
|
||||||
|
|
||||||
|
- **Best Suited For:** Large image libraries after importing a significant number of images.
|
||||||
|
- **Warning:** This method deletes all previously assigned names.
|
||||||
|
- **Tip:** **Always take a [backup](/docs/administration/backup-and-restore#database) before proceeding!**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Step-by-Step Instructions
|
||||||
|
|
||||||
|
### Objective
|
||||||
|
|
||||||
|
To enhance face clustering and ensure the model effectively identifies faces using qualitative initial data.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Steps
|
||||||
|
|
||||||
|
#### 1. Adjust Machine Learning Settings
|
||||||
|
|
||||||
|
Navigate to:
|
||||||
|
**Admin → Administration → Settings → Machine Learning Settings**
|
||||||
|
|
||||||
|
Make the following changes:
|
||||||
|
|
||||||
|
- **Maximum recognition distance (Optional):**
|
||||||
|
Lower this value, e.g., to **0.4**, if the library contains people with similar facial features.
|
||||||
|
- **Minimum recognized faces:**
|
||||||
|
Set this to a **high value** (e.g., 20 For libraries with a large amount of assets (~100K+), and 10 for libraries with medium amount of assets (~40K+)).
|
||||||
|
> A high value ensures clusters only include faces that appear at least 20/`value` times in the library, improving the initial clustering process.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. Run Reset Jobs
|
||||||
|
|
||||||
|
Go to:
|
||||||
|
**Admin → Administration → Settings → Jobs**
|
||||||
|
|
||||||
|
Perform the following:
|
||||||
|
|
||||||
|
1. **FACIAL RECOGNITION → Reset**
|
||||||
|
|
||||||
|
> These reset jobs rebuild the recognition model based on the new settings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. Refine Recognition with Lower Thresholds
|
||||||
|
|
||||||
|
Once the reset jobs are complete, refine the recognition as follows:
|
||||||
|
|
||||||
|
- **Step 1:**
|
||||||
|
Return to **Minimum recognized faces** in Machine Learning Settings and lower the value to **10** (In medium libraries we will lower the value from 10 to 5).
|
||||||
|
|
||||||
|
> Run the job: **FACIAL RECOGNITION → MISSING Mode**
|
||||||
|
|
||||||
|
- **Step 2:**
|
||||||
|
Lower the value again to **3**.
|
||||||
|
> Run the job: **FACIAL RECOGNITION → MISSING Mode**
|
||||||
|
|
||||||
|
:::tip try different values
|
||||||
|
For certain libraries with a larger or smaller amount of assets, other settings will be better or worse. It is recommended to try different values **before assigning names** and see which settings work best for your library.
|
||||||
|
:::
|
||||||
|
|
||||||
|
---
|
||||||
@@ -6,7 +6,7 @@ This guide explains how to store generated and raw files with docker's volume mo
|
|||||||
It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools, especially `profile/`.
|
It is important to remember to update the backup settings after following the guide to back up the new backup paths if using automatic backup tools, especially `profile/`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server
|
In our `.env` file, we will define the paths we want to use. Note that you don't have to define all of these: UPLOAD_LOCATION will be the base folder that files are stored in by default, with the other paths acting as overrides.
|
||||||
|
|
||||||
```diff title=".env"
|
```diff title=".env"
|
||||||
# You can find documentation for all the supported environment variables [here](/docs/install/environment-variables)
|
# You can find documentation for all the supported environment variables [here](/docs/install/environment-variables)
|
||||||
@@ -21,7 +21,7 @@ In our `.env` file, we will define variables that will help us in the future whe
|
|||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
After defining the locations of these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container.
|
After defining the locations of these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container. These paths are where the mount attaches inside of the container, so don't change those.
|
||||||
|
|
||||||
```diff title="docker-compose.yml"
|
```diff title="docker-compose.yml"
|
||||||
services:
|
services:
|
||||||
@@ -35,7 +35,8 @@ services:
|
|||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
```
|
```
|
||||||
|
|
||||||
Restart Immich to register the changes.
|
After making this change, you have to move the files over to the new folders to make sure Immich can find everything it needs. If you haven't uploaded anything important yet, you can also reset Immich entirely by deleting the database folder.
|
||||||
|
Then restart Immich to register the changes:
|
||||||
|
|
||||||
```
|
```
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
|
|||||||
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
|
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
|
||||||
```
|
```
|
||||||
|
|
||||||
|
```sql title="Find by ID"
|
||||||
|
SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
|
||||||
|
```
|
||||||
|
|
||||||
|
```sql title="Find by partial ID"
|
||||||
|
SELECT * FROM "assets" WHERE "id"::text LIKE '%ab431d3a%';
|
||||||
|
```
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
|
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
|
||||||
:::
|
:::
|
||||||
|
|||||||
@@ -23,12 +23,12 @@ name: immich_remote_ml
|
|||||||
services:
|
services:
|
||||||
immich-machine-learning:
|
immich-machine-learning:
|
||||||
container_name: immich_machine_learning
|
container_name: immich_machine_learning
|
||||||
# For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag.
|
# For hardware acceleration, add one of -[armnn, cuda, rocm, openvino] to the image tag.
|
||||||
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
# Example tag: ${IMMICH_VERSION:-release}-cuda
|
||||||
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
image: ghcr.io/immich-app/immich-machine-learning:${IMMICH_VERSION:-release}
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.ml.yml
|
# file: hwaccel.ml.yml
|
||||||
# service: # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
# service: # set to one of [armnn, cuda, rocm, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable
|
||||||
volumes:
|
volumes:
|
||||||
- model-cache:/cache
|
- model-cache:/cache
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ You can alternatively download these two files from your browser and move them t
|
|||||||
</CodeBlock>
|
</CodeBlock>
|
||||||
|
|
||||||
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
||||||
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication.
|
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
|
||||||
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
||||||
- Set your timezone by uncommenting the `TZ=` line.
|
- Set your timezone by uncommenting the `TZ=` line.
|
||||||
- Populate custom database information if necessary.
|
- Populate custom database information if necessary.
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Just restarting the containers does not replace the environment within the conta
|
|||||||
|
|
||||||
In order to recreate the container using docker compose, run `docker compose up -d`.
|
In order to recreate the container using docker compose, run `docker compose up -d`.
|
||||||
In most cases docker will recognize that the `.env` file has changed and recreate the affected containers.
|
In most cases docker will recognize that the `.env` file has changed and recreate the affected containers.
|
||||||
If this should not work, try running `docker compose up -d --force-recreate`.
|
If this does not work, try running `docker compose up -d --force-recreate`.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -20,8 +20,8 @@ If this should not work, try running `docker compose up -d --force-recreate`.
|
|||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :----------------- | :------------------------------ | :-------: | :----------------------- |
|
| :----------------- | :------------------------------ | :-------: | :----------------------- |
|
||||||
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning |
|
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning |
|
||||||
| `UPLOAD_LOCATION` | Host Path for uploads | | server |
|
| `UPLOAD_LOCATION` | Host path for uploads | | server |
|
||||||
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
|
| `DB_DATA_LOCATION` | Host path for Postgres database | | database |
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
|
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
|
||||||
@@ -33,15 +33,15 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
| :---------------------------------- | :---------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_MEDIA_LOCATION` | Media Location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `./upload`<sup>\*3</sup> | server | api, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||||
| `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | |
|
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
| `IMMICH_TRUSTED_PROXIES` | List of comma separated IPs set as trusted proxies | | server | api |
|
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/docs/administration/system-integrity) | | server | api, microservices |
|
||||||
|
|
||||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||||
@@ -50,7 +50,7 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
|
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
|
||||||
|
|
||||||
\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
|
\*3: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
|
||||||
It only need to be set if the Immich deployment method is changing.
|
It only needs to be set if the Immich deployment method is changing.
|
||||||
|
|
||||||
## Workers
|
## Workers
|
||||||
|
|
||||||
@@ -75,12 +75,12 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
|||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
|
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
|
||||||
| `DB_URL` | Database URL | | server |
|
| `DB_URL` | Database URL | | server |
|
||||||
| `DB_HOSTNAME` | Database Host | `database` | server |
|
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||||
| `DB_PORT` | Database Port | `5432` | server |
|
| `DB_PORT` | Database port | `5432` | server |
|
||||||
| `DB_USERNAME` | Database User | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_PASSWORD` | Database Password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database Name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
|
|
||||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||||
@@ -103,18 +103,18 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW
|
|||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :--------------- | :------------- | :-----: | :--------- |
|
| :--------------- | :------------- | :-----: | :--------- |
|
||||||
| `REDIS_URL` | Redis URL | | server |
|
| `REDIS_URL` | Redis URL | | server |
|
||||||
| `REDIS_SOCKET` | Redis Socket | | server |
|
| `REDIS_SOCKET` | Redis socket | | server |
|
||||||
| `REDIS_HOSTNAME` | Redis Host | `redis` | server |
|
| `REDIS_HOSTNAME` | Redis host | `redis` | server |
|
||||||
| `REDIS_PORT` | Redis Port | `6379` | server |
|
| `REDIS_PORT` | Redis port | `6379` | server |
|
||||||
| `REDIS_USERNAME` | Redis Username | | server |
|
| `REDIS_USERNAME` | Redis username | | server |
|
||||||
| `REDIS_PASSWORD` | Redis Password | | server |
|
| `REDIS_PASSWORD` | Redis password | | server |
|
||||||
| `REDIS_DBINDEX` | Redis DB Index | `0` | server |
|
| `REDIS_DBINDEX` | Redis DB index | `0` | server |
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
||||||
|
|
||||||
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
||||||
More info can be found in the upstream [ioredis] documentation.
|
More information can be found in the upstream [ioredis] documentation.
|
||||||
|
|
||||||
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
|
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
|
||||||
:::
|
:::
|
||||||
@@ -168,6 +168,8 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||||
|
| `MACHINE_LEARNING_PING_TIMEOUT` | How long (ms) to wait for a PING response when checking if an ML server is available | `2000` | server |
|
||||||
|
| `MACHINE_LEARNING_AVAILABILITY_BACKOFF_TIME` | How long to ignore ML servers that are offline before trying again | `30000` | server |
|
||||||
|
|
||||||
\*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.
|
\*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.
|
||||||
|
|
||||||
@@ -179,7 +181,11 @@ Redis (Sentinel) URL example JSON before encoding:
|
|||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
Other machine learning parameters can be tuned from the admin UI.
|
While the `textual` model is the only one required for smart search, some users may experience slow first searches
|
||||||
|
due to backups triggering loading of the other models into memory, which blocks other requests until completed.
|
||||||
|
To avoid this, you can preload the other models (`visual`, `recognition`, and `detection`) if you have enough RAM to do so.
|
||||||
|
|
||||||
|
Additional machine learning parameters can be tuned from the admin UI.
|
||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
@@ -210,7 +216,7 @@ the `_FILE` variable should be set to the path of a file containing the variable
|
|||||||
details on how to use Docker Secrets in the Postgres image.
|
details on how to use Docker Secrets in the Postgres image.
|
||||||
|
|
||||||
\*2: See [this comment][docker-secrets-example] for an example of how
|
\*2: See [this comment][docker-secrets-example] for an example of how
|
||||||
to use use a Docker secret for the password in the Redis container.
|
to use a Docker secret for the password in the Redis container.
|
||||||
|
|
||||||
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||||
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ The script will perform the following actions:
|
|||||||
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
|
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
|
||||||
2. Start the containers.
|
2. Start the containers.
|
||||||
|
|
||||||
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
|
The web application and mobile app will be available at `http://<machine-ip-address>:2283`
|
||||||
|
|
||||||
The directory which is used to store the library files is `./immich-app` relative to the current directory.
|
The directory which is used to store the library files is `./immich-app` relative to the current directory.
|
||||||
|
|
||||||
|
|||||||
76
docs/docs/install/synology.md
Normal file
76
docs/docs/install/synology.md
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
---
|
||||||
|
sidebar_position: 85
|
||||||
|
---
|
||||||
|
|
||||||
|
# Synology [Community]
|
||||||
|
|
||||||
|
:::note
|
||||||
|
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||||
|
|
||||||
|
Community support can be found in the dedicated channel on the [Discord Server](https://discord.immich.app/).
|
||||||
|
|
||||||
|
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
||||||
|
:::
|
||||||
|
|
||||||
|
Immich can easily be installed on a Synology NAS using Container Manager within DSM. If you have not installed Container Manager already, you can install it in the Packages Center. Refer to the [Container Manager docs](https://kb.synology.com/en-us/DSM/help/ContainerManager/docker_desc?version=7) for more information on using Container Manager.
|
||||||
|
|
||||||
|
## Step 1 - Download the required files
|
||||||
|
|
||||||
|
Create a directory of your choice (e.g. `./immich-app`) to house Immich. In general, it's a best practice to have all Docker-based applications running under the `./docker` directory, so in this case, your directory structure will look like `./docker/immich-app`.
|
||||||
|
|
||||||
|
Now create a `./postgres` and `./library` directory as sub-directories of the `./docker/immich-app`.
|
||||||
|
|
||||||
|
When you're all done, you should have the following:
|
||||||
|
|
||||||
|
- `./docker/immich-app/postgres`
|
||||||
|
- `./docker/immich-app/library`
|
||||||
|
|
||||||
|
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
|
||||||
|
|
||||||
|
## Step 2 - Populate the .env file with custom values
|
||||||
|
|
||||||
|
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
|
||||||
|
|
||||||
|
## Step 3 - Create a new project in Container Manager
|
||||||
|
|
||||||
|
Open Container Manager, and select the "**Project**" action on the left navigation bar and then click "**Create**".
|
||||||
|

|
||||||
|
|
||||||
|
In the settings of your new project, set "**Project name**" to a name you'll remember, such as _immich-app_. When setting the "**Path**", select the `./docker/immich-app` directory you created earlier. Doing so will prompt a message to use the existing `docker-compose.yml` already present in the directory for your project. Click "**OK**" to continue.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
The following screen will give you the option to further customize your `docker-compose.yml` file, giving you a warning regarding the `start_interval` property. Under the `healthcheck` heading, remove the `start_interval: 30s` completely and click "**Next**".
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Skip the section asking to set-up a portal for Web Station, and then complete the wizard which will build and start the containers for your project.
|
||||||
|
|
||||||
|
Once your containers are successfully running, navigate to the "**Container**" section of Container Manager, right-click on the "**immich-server**" container, and choose the "**Details**".
|
||||||
|
|
||||||
|
Scroll to the bottom of the "**Details**" section, and find the `IP Address` of the container, located in the `Network` section. Take note of the container's IP address as you will need it for **Step 4**.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Step 4 - Configure Firewall Settings
|
||||||
|
|
||||||
|
Once your project completes the build process, your containers will start. In order to be able to access Immich from your browser, you need to configure the firewall settings for your Synology NAS.
|
||||||
|
|
||||||
|
Open "**Control Panel**" on your Synology NAS, and select "**Security**". Navigate to "**Firewall**"
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Click "**Edit Rules**" and add the following firewall rules:
|
||||||
|
|
||||||
|
- Add a "**Source IP**" rule for the IP address of your container that you obtained in Step 3 above
|
||||||
|
- Add a "**Ports**" rule for the port specified in the `docker-compose.yml`, which should be `2283`
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
Read the [Post Installation](/docs/install/post-install.mdx) steps or setup optional features below.
|
||||||
|
|
||||||
|
### Setting up optional features
|
||||||
|
|
||||||
|
- [External Libraries](/docs/features/libraries.md): Adding your existing photo library to Immich
|
||||||
|
- [Hardware Transcoding](/docs/features/hardware-transcoding.md): Speeding up video transcoding
|
||||||
|
- [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md): Speeding up various machine learning tasks in Immich
|
||||||
@@ -41,7 +41,7 @@ className="border rounded-xl"
|
|||||||
:::info Permissions
|
:::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.
|
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
|
||||||
|
|
||||||
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
|
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, Immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Installing the Immich Application
|
## Installing the Immich Application
|
||||||
@@ -160,6 +160,10 @@ The image above has example values.
|
|||||||
|
|
||||||
### Additional Storage [(External Libraries)](/docs/features/libraries)
|
### Additional Storage [(External Libraries)](/docs/features/libraries)
|
||||||
|
|
||||||
|
:::danger Advanced Users Only
|
||||||
|
This feature should only be used by advanced users. If this is your first time installing Immich, then DO NOT mount an external library until you have a working setup. Also, your mount path MUST be something unique and should NOT be your library or upload location or a Linux directory like `/lib`. The picture below shows a valid example.
|
||||||
|
:::
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/truenas10.webp').default}
|
src={require('./img/truenas10.webp').default}
|
||||||
width="40%"
|
width="40%"
|
||||||
@@ -168,7 +172,7 @@ className="border rounded-xl"
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
|
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
|
||||||
The **Mount Path** is the loaction you will need to copy and paste into the External Library settings within Immich.
|
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
|
||||||
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
|
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
|
||||||
|
|
||||||
<!-- A section for Labels would go here but I don't know what they do. -->
|
<!-- A section for Labels would go here but I don't know what they do. -->
|
||||||
@@ -194,7 +198,7 @@ The **CPU** value was specified in a different format with a default of `4000m`
|
|||||||
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passtrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
|
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
|||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
|
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||||
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
||||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283/api`
|
Login to the mobile app with the server endpoint URL at `http://<machine-ip-address>:2283`
|
||||||
|
|
||||||
<img src={require('./img/sign-in-phone.webp').default} width='50%' title='Mobile App Sign In' />
|
<img src={require('./img/sign-in-phone.webp').default} width='50%' title='Mobile App Sign In' />
|
||||||
|
|||||||
@@ -110,9 +110,9 @@ const config = {
|
|||||||
label: 'API',
|
label: 'API',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/blog',
|
href: 'https://immich.store',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
label: 'Blog',
|
label: 'Merch',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
href: 'https://github.com/immich-app/immich',
|
href: 'https://github.com/immich-app/immich',
|
||||||
|
|||||||
21
docs/package-lock.json
generated
21
docs/package-lock.json
generated
@@ -28,6 +28,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "~3.7.0",
|
"@docusaurus/module-type-aliases": "~3.7.0",
|
||||||
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
@@ -3698,6 +3700,13 @@
|
|||||||
"node": ">=18.0"
|
"node": ">=18.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@docusaurus/tsconfig": {
|
||||||
|
"version": "3.7.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@docusaurus/tsconfig/-/tsconfig-3.7.0.tgz",
|
||||||
|
"integrity": "sha512-vRsyj3yUZCjscgfgcFYjIsTcAru/4h4YH2/XAE8Rs7wWdnng98PgWKvP5ovVc4rmRpRg2WChVW0uOy2xHDvDBQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@docusaurus/types": {
|
"node_modules/@docusaurus/types": {
|
||||||
"version": "3.7.0",
|
"version": "3.7.0",
|
||||||
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.7.0.tgz",
|
||||||
@@ -14061,9 +14070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.1",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
"integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
|
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
@@ -15725,9 +15734,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prettier": {
|
"node_modules/prettier": {
|
||||||
"version": "3.4.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.2.tgz",
|
||||||
"integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
|
"integrity": "sha512-lc6npv5PH7hVqozBR7lkBNOGXV9vMwROAPlumdBkX0wTbbzPu/U1hk5yL8p2pt4Xoc+2mkT8t/sow2YrV/M5qg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "~3.7.0",
|
"@docusaurus/module-type-aliases": "~3.7.0",
|
||||||
|
"@docusaurus/tsconfig": "^3.7.0",
|
||||||
|
"@docusaurus/types": "^3.7.0",
|
||||||
"prettier": "^3.2.4",
|
"prettier": "^3.2.4",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
@@ -55,6 +57,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.13.1"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,11 @@ const guides: CommunityGuidesProps[] = [
|
|||||||
description: 'How to configure an existing fail2ban installation to block incorrect login attempts.',
|
description: 'How to configure an existing fail2ban installation to block incorrect login attempts.',
|
||||||
url: 'https://github.com/immich-app/immich/discussions/3243#discussioncomment-6681948',
|
url: 'https://github.com/immich-app/immich/discussions/3243#discussioncomment-6681948',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Immich remote access with NordVPN Meshnet',
|
||||||
|
description: 'Access Immich with an end-to-end encrypted connection.',
|
||||||
|
url: 'https://meshnet.nordvpn.com/how-to/remote-files-media-access/immich-remote-access',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
|
||||||
|
|||||||
@@ -99,6 +99,11 @@ const projects: CommunityProjectProps[] = [
|
|||||||
description: 'Downloads a configurable number of random photos based on people or album ID.',
|
description: 'Downloads a configurable number of random photos based on people or album ID.',
|
||||||
url: 'https://github.com/jon6fingrs/immich-dl',
|
url: 'https://github.com/jon6fingrs/immich-dl',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Immich Upload Optimizer',
|
||||||
|
description: 'Automatically optimize files uploaded to Immich in order to save storage space',
|
||||||
|
url: 'https://github.com/miguelangel-nubla/immich-upload-optimizer',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||||
|
|||||||
@@ -24,10 +24,13 @@ export default function VersionSwitcher(): JSX.Element {
|
|||||||
{ label: 'Next', url: 'https://main.preview.immich.app' },
|
{ label: 'Next', url: 'https://main.preview.immich.app' },
|
||||||
{ label: 'Latest', url: 'https://immich.app' },
|
{ label: 'Latest', url: 'https://immich.app' },
|
||||||
...archiveVersions,
|
...archiveVersions,
|
||||||
];
|
].map(({ label, url }) => ({
|
||||||
|
label,
|
||||||
|
url: new URL(url),
|
||||||
|
}));
|
||||||
setVersions(allVersions);
|
setVersions(allVersions);
|
||||||
|
|
||||||
const activeVersion = allVersions.find((version) => new URL(version.url).origin === window.location.origin);
|
const activeVersion = allVersions.find((version) => version.url.origin === window.location.origin);
|
||||||
if (activeVersion) {
|
if (activeVersion) {
|
||||||
setLabel(activeVersion.label);
|
setLabel(activeVersion.label);
|
||||||
}
|
}
|
||||||
@@ -44,12 +47,12 @@ export default function VersionSwitcher(): JSX.Element {
|
|||||||
return (
|
return (
|
||||||
versions.length > 0 && (
|
versions.length > 0 && (
|
||||||
<DropdownNavbarItem
|
<DropdownNavbarItem
|
||||||
className="navbar__item"
|
className="version-switcher-34ab39"
|
||||||
label={label}
|
label={label}
|
||||||
mobile={windowSize === 'mobile'}
|
mobile={windowSize === 'mobile'}
|
||||||
items={versions.map(({ label, url }) => ({
|
items={versions.map(({ label, url }) => ({
|
||||||
label,
|
label,
|
||||||
to: url,
|
to: new URL(location.pathname + location.search + location.hash, url).href,
|
||||||
target: '_self',
|
target: '_self',
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -75,6 +75,11 @@ div[class^='announcementBar_'] {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* workaround for version switcher PR 15894 */
|
||||||
|
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
code {
|
code {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,13 @@ function HomepageHeader() {
|
|||||||
>
|
>
|
||||||
Demo
|
Demo
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||||
|
to="https://immich.store"
|
||||||
|
>
|
||||||
|
Buy Merch
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
||||||
|
|||||||
62
docs/static/archived-versions.json
vendored
62
docs/static/archived-versions.json
vendored
@@ -1,12 +1,44 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v1.128.0",
|
||||||
|
"url": "https://v1.128.0.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.127.0",
|
||||||
|
"url": "https://v1.127.0.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.126.1",
|
||||||
|
"url": "https://v1.126.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.126.0",
|
||||||
|
"url": "https://v1.126.0.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.7",
|
||||||
|
"url": "https://v1.125.7.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.6",
|
||||||
|
"url": "https://v1.125.6.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.5",
|
||||||
|
"url": "https://v1.125.5.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.3",
|
||||||
|
"url": "https://v1.125.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.2",
|
||||||
|
"url": "https://v1.125.2.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.125.1",
|
"label": "v1.125.1",
|
||||||
"url": "https://v1.125.1.archive.immich.app"
|
"url": "https://v1.125.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"label": "v1.125.0",
|
|
||||||
"url": "https://v1.125.0.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "v1.124.2",
|
"label": "v1.124.2",
|
||||||
"url": "https://v1.124.2.archive.immich.app"
|
"url": "https://v1.124.2.archive.immich.app"
|
||||||
@@ -169,46 +201,46 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.105.1",
|
"label": "v1.105.1",
|
||||||
"url": "https://v1.105.1.archive.immich.app/"
|
"url": "https://v1.105.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.105.0",
|
"label": "v1.105.0",
|
||||||
"url": "https://v1.105.0.archive.immich.app/"
|
"url": "https://v1.105.0.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.104.0",
|
"label": "v1.104.0",
|
||||||
"url": "https://v1.104.0.archive.immich.app/"
|
"url": "https://v1.104.0.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.103.1",
|
"label": "v1.103.1",
|
||||||
"url": "https://v1.103.1.archive.immich.app/"
|
"url": "https://v1.103.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.103.0",
|
"label": "v1.103.0",
|
||||||
"url": "https://v1.103.0.archive.immich.app/"
|
"url": "https://v1.103.0.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.102.3",
|
"label": "v1.102.3",
|
||||||
"url": "https://v1.102.3.archive.immich.app/"
|
"url": "https://v1.102.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.102.2",
|
"label": "v1.102.2",
|
||||||
"url": "https://v1.102.2.archive.immich.app/"
|
"url": "https://v1.102.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.102.1",
|
"label": "v1.102.1",
|
||||||
"url": "https://v1.102.1.archive.immich.app/"
|
"url": "https://v1.102.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.102.0",
|
"label": "v1.102.0",
|
||||||
"url": "https://v1.102.0.archive.immich.app/"
|
"url": "https://v1.102.0.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.101.0",
|
"label": "v1.101.0",
|
||||||
"url": "https://v1.101.0.archive.immich.app/"
|
"url": "https://v1.101.0.archive.immich.app"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.100.0",
|
"label": "v1.100.0",
|
||||||
"url": "https://v1.100.0.archive.immich.app/"
|
"url": "https://v1.100.0.archive.immich.app"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
BIN
docs/static/img/synology-container-manager-container-details.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-container-details.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 164 KiB |
BIN
docs/static/img/synology-container-manager-create-project.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-create-project.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
BIN
docs/static/img/synology-container-manager-customize-docker-compose.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-customize-docker-compose.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
BIN
docs/static/img/synology-container-manager-set-path.png
vendored
Normal file
BIN
docs/static/img/synology-container-manager-set-path.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 49 KiB |
BIN
docs/static/img/synology-firewall-rules.png
vendored
Normal file
BIN
docs/static/img/synology-firewall-rules.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 81 KiB |
@@ -5,7 +5,7 @@ module.exports = {
|
|||||||
preflight: false, // disable Tailwind's reset
|
preflight: false, // disable Tailwind's reset
|
||||||
},
|
},
|
||||||
content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
|
content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
|
||||||
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
|
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settings
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
{
|
{
|
||||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||||
"extends": "@tsconfig/docusaurus/tsconfig.json",
|
"extends": "@docusaurus/tsconfig",
|
||||||
|
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": "."
|
||||||
"module": "Node16"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.13.1
|
22.14.0
|
||||||
|
|||||||
@@ -34,10 +34,10 @@ services:
|
|||||||
- 2285:2285
|
- 2285:2285
|
||||||
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:6.2-alpine@sha256:905c4ee67b8e0aa955331960d2aa745781e6bd89afc44a8584bfd13bc890f0ae
|
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
|
|||||||
963
e2e/package-lock.json
generated
963
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.125.1",
|
"version": "1.128.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.10.9",
|
"@types/node": "^22.13.5",
|
||||||
"@types/oidc-provider": "^8.5.1",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/pg": "^8.11.0",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^56.0.1",
|
"eslint-plugin-unicorn": "^56.0.1",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^15.9.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"oidc-provider": "^8.5.1",
|
"oidc-provider": "^8.5.1",
|
||||||
@@ -53,6 +53,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.13.1"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,79 +22,92 @@ const user1NotShared = 'user1NotShared';
|
|||||||
const user2SharedUser = 'user2SharedUser';
|
const user2SharedUser = 'user2SharedUser';
|
||||||
const user2SharedLink = 'user2SharedLink';
|
const user2SharedLink = 'user2SharedLink';
|
||||||
const user2NotShared = 'user2NotShared';
|
const user2NotShared = 'user2NotShared';
|
||||||
|
const user4DeletedAsset = 'user4DeletedAsset';
|
||||||
|
const user4Empty = 'user4Empty';
|
||||||
|
|
||||||
describe('/albums', () => {
|
describe('/albums', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user1: LoginResponseDto;
|
let user1: LoginResponseDto;
|
||||||
let user1Asset1: AssetMediaResponseDto;
|
let user1Asset1: AssetMediaResponseDto;
|
||||||
let user1Asset2: AssetMediaResponseDto;
|
let user1Asset2: AssetMediaResponseDto;
|
||||||
|
let user4Asset1: AssetMediaResponseDto;
|
||||||
let user1Albums: AlbumResponseDto[];
|
let user1Albums: AlbumResponseDto[];
|
||||||
let user2: LoginResponseDto;
|
let user2: LoginResponseDto;
|
||||||
let user2Albums: AlbumResponseDto[];
|
let user2Albums: AlbumResponseDto[];
|
||||||
|
let deletedAssetAlbum: AlbumResponseDto;
|
||||||
let user3: LoginResponseDto; // deleted
|
let user3: LoginResponseDto; // deleted
|
||||||
|
let user4: LoginResponseDto;
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
|
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
|
|
||||||
[user1, user2, user3] = await Promise.all([
|
[user1, user2, user3, user4] = await Promise.all([
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
utils.userSetup(admin.accessToken, createUserDto.user1),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
utils.userSetup(admin.accessToken, createUserDto.user2),
|
||||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
utils.userSetup(admin.accessToken, createUserDto.user3),
|
||||||
|
utils.userSetup(admin.accessToken, createUserDto.user4),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
[user1Asset1, user1Asset2] = await Promise.all([
|
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
|
||||||
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
|
utils.createAsset(user1.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
user1Albums = await Promise.all([
|
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
|
||||||
utils.createAlbum(user1.accessToken, {
|
Promise.all([
|
||||||
albumName: user1SharedEditorUser,
|
utils.createAlbum(user1.accessToken, {
|
||||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
|
albumName: user1SharedEditorUser,
|
||||||
assetIds: [user1Asset1.id],
|
albumUsers: [
|
||||||
}),
|
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
||||||
utils.createAlbum(user1.accessToken, {
|
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
||||||
albumName: user1SharedLink,
|
],
|
||||||
assetIds: [user1Asset1.id],
|
assetIds: [user1Asset1.id],
|
||||||
}),
|
}),
|
||||||
utils.createAlbum(user1.accessToken, {
|
utils.createAlbum(user1.accessToken, {
|
||||||
albumName: user1NotShared,
|
albumName: user1SharedLink,
|
||||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
assetIds: [user1Asset1.id],
|
||||||
}),
|
}),
|
||||||
utils.createAlbum(user1.accessToken, {
|
utils.createAlbum(user1.accessToken, {
|
||||||
albumName: user1SharedViewerUser,
|
albumName: user1NotShared,
|
||||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||||
assetIds: [user1Asset1.id],
|
}),
|
||||||
|
utils.createAlbum(user1.accessToken, {
|
||||||
|
albumName: user1SharedViewerUser,
|
||||||
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||||
|
assetIds: [user1Asset1.id],
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
Promise.all([
|
||||||
|
utils.createAlbum(user2.accessToken, {
|
||||||
|
albumName: user2SharedUser,
|
||||||
|
albumUsers: [
|
||||||
|
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||||
|
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||||
|
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||||
|
]),
|
||||||
|
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
|
||||||
|
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
|
||||||
|
utils.createAlbum(user3.accessToken, {
|
||||||
|
albumName: 'Deleted',
|
||||||
|
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
user2Albums = await Promise.all([
|
|
||||||
utils.createAlbum(user2.accessToken, {
|
|
||||||
albumName: user2SharedUser,
|
|
||||||
albumUsers: [
|
|
||||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
|
||||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
|
||||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await utils.createAlbum(user3.accessToken, {
|
|
||||||
albumName: 'Deleted',
|
|
||||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
|
||||||
});
|
|
||||||
|
|
||||||
await addAssetsToAlbum(
|
|
||||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
|
||||||
{ headers: asBearerAuth(user1.accessToken) },
|
|
||||||
);
|
|
||||||
|
|
||||||
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
|
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
addAssetsToAlbum(
|
||||||
|
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||||
|
{ headers: asBearerAuth(user1.accessToken) },
|
||||||
|
),
|
||||||
|
addAssetsToAlbum(
|
||||||
|
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
|
||||||
|
{ headers: asBearerAuth(user4.accessToken) },
|
||||||
|
),
|
||||||
// add shared link to user1SharedLink album
|
// add shared link to user1SharedLink album
|
||||||
utils.createSharedLink(user1.accessToken, {
|
utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Album,
|
type: SharedLinkType.Album,
|
||||||
@@ -107,7 +120,11 @@ describe('/albums', () => {
|
|||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
[user2Albums[0]] = await Promise.all([
|
||||||
|
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||||
|
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||||
|
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums', () => {
|
describe('GET /albums', () => {
|
||||||
@@ -284,6 +301,25 @@ describe('/albums', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return empty albums and albums where all assets are deleted', async () => {
|
||||||
|
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: user4.userId,
|
||||||
|
albumName: user4DeletedAsset,
|
||||||
|
shared: false,
|
||||||
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
ownerId: user4.userId,
|
||||||
|
albumName: user4Empty,
|
||||||
|
shared: false,
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums/:id', () => {
|
describe('GET /albums/:id', () => {
|
||||||
@@ -362,6 +398,26 @@ describe('/albums', () => {
|
|||||||
shared: true,
|
shared: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not count trashed assets', async () => {
|
||||||
|
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual({
|
||||||
|
...user2Albums[0],
|
||||||
|
assets: [],
|
||||||
|
assetCount: 1,
|
||||||
|
lastModifiedAssetTimestamp: expect.any(String),
|
||||||
|
endDate: expect.any(String),
|
||||||
|
startDate: expect.any(String),
|
||||||
|
albumUsers: expect.any(Array),
|
||||||
|
shared: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums/statistics', () => {
|
describe('GET /albums/statistics', () => {
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ import {
|
|||||||
AssetMediaStatus,
|
AssetMediaStatus,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
|
getAssetInfo,
|
||||||
|
getMyUser,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
getAssetInfo,
|
|
||||||
getConfig,
|
|
||||||
getMyUser,
|
|
||||||
updateConfig,
|
updateConfig,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
@@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client';
|
|||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
|
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -41,14 +40,10 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
|||||||
return dto;
|
return dto;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|
||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||||
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
||||||
|
|
||||||
const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
|
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
const filepath = join(tempDir, filename);
|
const filepath = join(tempDir, filename);
|
||||||
await writeFile(filepath, bytes);
|
await writeFile(filepath, bytes);
|
||||||
@@ -230,7 +225,7 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get the asset faces', async () => {
|
it('should get the asset faces', async () => {
|
||||||
const config = await getSystemConfig(admin.accessToken);
|
const config = await utils.getSystemConfig(admin.accessToken);
|
||||||
config.metadata.faces.import = true;
|
config.metadata.faces.import = true;
|
||||||
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
@@ -703,6 +698,20 @@ describe('/asset', () => {
|
|||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set the negative rating', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({ rating: -1 });
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: user1Assets[0].id,
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
rating: -1,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
expect(status).toEqual(200);
|
||||||
|
});
|
||||||
|
|
||||||
it('should reject invalid rating', async () => {
|
it('should reject invalid rating', async () => {
|
||||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
|
|||||||
225
e2e/src/api/specs/jobs.e2e-spec.ts
Normal file
225
e2e/src/api/specs/jobs.e2e-spec.ts
Normal file
@@ -0,0 +1,225 @@
|
|||||||
|
import { JobCommand, JobName, LoginResponseDto, updateConfig } from '@immich/sdk';
|
||||||
|
import { cpSync, rmSync } from 'node:fs';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename } from 'node:path';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
|
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
||||||
|
import request from 'supertest';
|
||||||
|
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
describe('/jobs', () => {
|
||||||
|
let admin: LoginResponseDto;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
await utils.resetDatabase();
|
||||||
|
admin = await utils.adminSetup({ onboarding: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('PUT /jobs', () => {
|
||||||
|
afterEach(async () => {
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.FaceDetection, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.SmartSearch, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.DuplicateDetection, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const config = await utils.getSystemConfig(admin.accessToken);
|
||||||
|
config.machineLearning.duplicateDetection.enabled = false;
|
||||||
|
config.machineLearning.enabled = false;
|
||||||
|
config.metadata.faces.import = false;
|
||||||
|
config.machineLearning.clip.enabled = false;
|
||||||
|
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/jobs/metadataExtraction');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should queue metadata extraction for missing assets', async () => {
|
||||||
|
const path = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Pause,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
{
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo?.make).toBeNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Empty,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Start,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
{
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not re-extract metadata for existing assets', async () => {
|
||||||
|
const path = `${testAssetDir}/temp/metadata/asset.jpg`;
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`, path);
|
||||||
|
|
||||||
|
const { id } = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
{
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo?.model).toBe('NIKON D700');
|
||||||
|
}
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, path);
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
|
command: JobCommand.Start,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
|
{
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo?.model).toBe('NIKON D700');
|
||||||
|
}
|
||||||
|
|
||||||
|
rmSync(path);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should queue thumbnail extraction for assets missing thumbs', async () => {
|
||||||
|
const path = `${testAssetDir}/albums/nature/tanners_ridge.jpg`;
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Pause,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { id } = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||||
|
|
||||||
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
expect(assetBefore.thumbhash).toBeNull();
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Empty,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Start,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||||
|
|
||||||
|
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
expect(assetAfter.thumbhash).not.toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not reload existing thumbnail when running thumb job for missing assets', async () => {
|
||||||
|
const path = `${testAssetDir}/temp/thumbs/asset1.jpg`;
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, path);
|
||||||
|
|
||||||
|
const { id } = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(path), filename: basename(path) },
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||||
|
|
||||||
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/notocactus_minimus.jpg`, path);
|
||||||
|
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Resume,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// This runs the missing thumbnail job
|
||||||
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
|
command: JobCommand.Start,
|
||||||
|
force: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.MetadataExtraction);
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, JobName.ThumbnailGeneration);
|
||||||
|
|
||||||
|
const assetAfter = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
|
|
||||||
|
// Asset 1 thumbnail should be untouched since its thumb should not have been reloaded, even though the file was changed
|
||||||
|
expect(assetAfter.thumbhash).toEqual(assetBefore.thumbhash);
|
||||||
|
|
||||||
|
rmSync(path);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||||
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
@@ -8,8 +8,6 @@ import request from 'supertest';
|
|||||||
import { utimes } from 'utimes';
|
import { utimes } from 'utimes';
|
||||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
|
||||||
|
|
||||||
describe('/libraries', () => {
|
describe('/libraries', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let user: LoginResponseDto;
|
let user: LoginResponseDto;
|
||||||
@@ -298,6 +296,8 @@ describe('/libraries', () => {
|
|||||||
expect(status).toBe(204);
|
expect(status).toBe(204);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||||
@@ -312,15 +312,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||||
@@ -340,13 +332,7 @@ describe('/libraries', () => {
|
|||||||
exclusionPatterns: ['**/directoryA'],
|
exclusionPatterns: ['**/directoryA'],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -360,13 +346,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -385,13 +365,7 @@ describe('/libraries', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
|
||||||
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -413,13 +387,7 @@ describe('/libraries', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
|
||||||
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -471,13 +439,7 @@ describe('/libraries', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
|
||||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
|
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -501,23 +463,12 @@ describe('/libraries', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
@@ -548,21 +499,12 @@ describe('/libraries', () => {
|
|||||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
@@ -584,6 +526,47 @@ describe('/libraries', () => {
|
|||||||
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not reimport a modified file more than once', async () => {
|
||||||
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
|
ownerId: admin.userId,
|
||||||
|
importPaths: [`${testAssetDirInternal}/temp/reimport`],
|
||||||
|
});
|
||||||
|
|
||||||
|
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
cpSync(`${testAssetDir}/albums/nature/el_torcal_rocks.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||||
|
|
||||||
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
|
libraryId: library.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(assets.count).toEqual(1);
|
||||||
|
|
||||||
|
const asset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
|
expect(asset).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
originalFileName: 'asset.jpg',
|
||||||
|
exifInfo: expect.objectContaining({
|
||||||
|
model: 'NIKON D750',
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should set an asset offline if its file is missing', async () => {
|
it('should set an asset offline if its file is missing', async () => {
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
const library = await utils.createLibrary(admin.accessToken, {
|
||||||
ownerId: admin.userId,
|
ownerId: admin.userId,
|
||||||
@@ -592,21 +575,14 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.count).toBe(1);
|
expect(assets.count).toBe(1);
|
||||||
|
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||||
@@ -624,8 +600,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.count).toBe(1);
|
expect(assets.count).toBe(1);
|
||||||
@@ -636,13 +611,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
|
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||||
@@ -662,8 +631,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||||
libraryId: library.id,
|
libraryId: library.id,
|
||||||
@@ -673,8 +641,7 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
expect(trashedAsset.isTrashed).toBe(true);
|
expect(trashedAsset.isTrashed).toBe(true);
|
||||||
@@ -696,19 +663,12 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||||
|
|
||||||
const { status } = await request(app)
|
await utils.scan(admin.accessToken, library.id);
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -725,11 +685,7 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -752,10 +708,7 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -779,10 +732,7 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -806,19 +756,13 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -841,18 +785,12 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -875,18 +813,12 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -910,19 +842,13 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -946,18 +872,12 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -981,18 +901,12 @@ describe('/libraries', () => {
|
|||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
@@ -1015,22 +929,13 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
expect(offlineAsset.isTrashed).toBe(true);
|
expect(offlineAsset.isTrashed).toBe(true);
|
||||||
@@ -1044,15 +949,7 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
@@ -1074,22 +971,13 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||||
@@ -1110,15 +998,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/another-path`],
|
importPaths: [`${testAssetDirInternal}/temp/another-path`],
|
||||||
});
|
});
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
@@ -1142,22 +1022,13 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
{
|
{
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||||
@@ -1174,15 +1045,7 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
{
|
await utils.scan(admin.accessToken, library.id);
|
||||||
const { status } = await request(app)
|
|
||||||
.post(`/libraries/${library.id}/scan`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
||||||
.send();
|
|
||||||
expect(status).toBe(204);
|
|
||||||
}
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||||
|
|
||||||
@@ -1302,8 +1165,7 @@ describe('/libraries', () => {
|
|||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
importPaths: [`${testAssetDirInternal}/temp`],
|
||||||
});
|
});
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/libraries/${library.id}`)
|
.delete(`/libraries/${library.id}`)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
import { getPerson, LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||||
import { uuidDto } from 'src/fixtures';
|
import { uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -195,6 +195,7 @@ describe('/people', () => {
|
|||||||
.send({
|
.send({
|
||||||
name: 'New Person',
|
name: 'New Person',
|
||||||
birthDate: '1990-01-01',
|
birthDate: '1990-01-01',
|
||||||
|
color: '#333',
|
||||||
});
|
});
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
@@ -203,6 +204,22 @@ describe('/people', () => {
|
|||||||
birthDate: '1990-01-01T00:00:00.000Z',
|
birthDate: '1990-01-01T00:00:00.000Z',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create a favorite person', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/people`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({
|
||||||
|
name: 'New Favorite Person',
|
||||||
|
isFavorite: true,
|
||||||
|
});
|
||||||
|
expect(status).toBe(201);
|
||||||
|
expect(body).toMatchObject({
|
||||||
|
id: expect.any(String),
|
||||||
|
name: 'New Favorite Person',
|
||||||
|
isFavorite: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /people/:id', () => {
|
describe('PUT /people/:id', () => {
|
||||||
@@ -216,6 +233,7 @@ describe('/people', () => {
|
|||||||
{ key: 'name', type: 'string' },
|
{ key: 'name', type: 'string' },
|
||||||
{ key: 'featureFaceAssetId', type: 'string' },
|
{ key: 'featureFaceAssetId', type: 'string' },
|
||||||
{ key: 'isHidden', type: 'boolean value' },
|
{ key: 'isHidden', type: 'boolean value' },
|
||||||
|
{ key: 'isFavorite', type: 'boolean value' },
|
||||||
]) {
|
]) {
|
||||||
it(`should not allow null ${key}`, async () => {
|
it(`should not allow null ${key}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -255,6 +273,42 @@ describe('/people', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ birthDate: null });
|
expect(body).toMatchObject({ birthDate: null });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should set a color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/people/${visiblePerson.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ color: '#555' });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ color: '#555' });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear a color', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/people/${visiblePerson.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ color: null });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body.color).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should mark a person as favorite', async () => {
|
||||||
|
const person = await utils.createPerson(admin.accessToken, {
|
||||||
|
name: 'visible_person',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(person.isFavorite).toBe(false);
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/people/${person.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ isFavorite: true });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({ isFavorite: true });
|
||||||
|
|
||||||
|
const person2 = await getPerson({ id: person.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
expect(person2).toMatchObject({ id: person.id, isFavorite: true });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /people/:id/merge', () => {
|
describe('POST /people/:id/merge', () => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
const today = DateTime.now();
|
const today = DateTime.now();
|
||||||
@@ -462,6 +462,55 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /search/random', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
utils.createAsset(admin.accessToken),
|
||||||
|
]);
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/search/random')
|
||||||
|
.send({ size: 1 })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const assets: AssetResponseDto[] = body;
|
||||||
|
expect(assets.length).toBe(1);
|
||||||
|
expect(assets[0].ownerId).toBe(admin.userId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/search/random')
|
||||||
|
.send({ size: 2 })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
|
||||||
|
const assets: AssetResponseDto[] = body;
|
||||||
|
expect(assets.length).toBe(2);
|
||||||
|
expect(assets[0].ownerId).toBe(admin.userId);
|
||||||
|
expect(assets[1].ownerId).toBe(admin.userId);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('GET /search/explore', () => {
|
describe('GET /search/explore', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/search/explore');
|
const { status, body } = await request(app).get('/search/explore');
|
||||||
|
|||||||
@@ -89,13 +89,13 @@ describe('/shared-links', () => {
|
|||||||
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /share/${key}', () => {
|
describe('GET /share/:key', () => {
|
||||||
it('should have correct asset count in meta tag for non-empty album', async () => {
|
it('should have correct asset count in meta tag for non-empty album', async () => {
|
||||||
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
|
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
|
||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.header['content-type']).toContain('text/html');
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
expect(resp.text).toContain(
|
expect(resp.text).toContain(
|
||||||
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
`<meta name="description" content="${metadataAlbum.assets.length} shared photos & videos" />`,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,14 +103,14 @@ describe('/shared-links', () => {
|
|||||||
const resp = await request(shareUrl).get(`/${linkWithAlbum.key}`);
|
const resp = await request(shareUrl).get(`/${linkWithAlbum.key}`);
|
||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.header['content-type']).toContain('text/html');
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
expect(resp.text).toContain(`<meta name="description" content="0 shared photos & videos" />`);
|
expect(resp.text).toContain(`<meta name="description" content="0 shared photos & videos" />`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have correct asset count in meta tag for shared asset', async () => {
|
it('should have correct asset count in meta tag for shared asset', async () => {
|
||||||
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
const resp = await request(shareUrl).get(`/${linkWithAssets.key}`);
|
||||||
expect(resp.status).toBe(200);
|
expect(resp.status).toBe(200);
|
||||||
expect(resp.header['content-type']).toContain('text/html');
|
expect(resp.header['content-type']).toContain('text/html');
|
||||||
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`);
|
expect(resp.text).toContain(`<meta name="description" content="1 shared photos & videos" />`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have fqdn og:image meta tag for shared asset', async () => {
|
it('should have fqdn og:image meta tag for shared asset', async () => {
|
||||||
@@ -139,7 +139,10 @@ describe('/shared-links', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ id: linkWithAlbum.id }),
|
expect.objectContaining({ id: linkWithAlbum.id }),
|
||||||
expect.objectContaining({ id: linkWithAssets.id }),
|
expect.objectContaining({
|
||||||
|
id: linkWithAssets.id,
|
||||||
|
assets: expect.arrayContaining([expect.objectContaining({ id: asset1.id })]),
|
||||||
|
}),
|
||||||
expect.objectContaining({ id: linkWithPassword.id }),
|
expect.objectContaining({ id: linkWithPassword.id }),
|
||||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||||
@@ -147,6 +150,30 @@ describe('/shared-links', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should filter on albumId', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/shared-links?albumId=${album.id}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(2);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ id: linkWithAlbum.id }),
|
||||||
|
expect.objectContaining({ id: linkWithPassword.id }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should find 0 albums', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/shared-links?albumId=${uuidDto.notFound}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not get shared links created by other users', async () => {
|
it('should not get shared links created by other users', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/shared-links')
|
.get('/shared-links')
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { LoginResponseDto, getAssetInfo, getAssetStatistics, scanLibrary } from '@immich/sdk';
|
import { LoginResponseDto, getAssetInfo, getAssetStatistics } from '@immich/sdk';
|
||||||
import { existsSync } from 'node:fs';
|
import { existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -6,8 +6,6 @@ import { app, asBearerAuth, testAssetDir, testAssetDirInternal, utils } from 'sr
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
|
||||||
|
|
||||||
describe('/trash', () => {
|
describe('/trash', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let ws: Socket;
|
let ws: Socket;
|
||||||
@@ -81,8 +79,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.items.length).toBe(1);
|
expect(assets.items.length).toBe(1);
|
||||||
@@ -90,8 +87,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
||||||
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
||||||
@@ -116,8 +112,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.items.length).toBe(1);
|
expect(assets.items.length).toBe(1);
|
||||||
@@ -125,8 +120,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
const assetBefore = await utils.getAssetInfo(admin.accessToken, asset.id);
|
||||||
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
expect(assetBefore).toMatchObject({ isTrashed: true, isOffline: true });
|
||||||
@@ -180,8 +174,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
expect(assets.count).toBe(1);
|
expect(assets.count).toBe(1);
|
||||||
@@ -189,9 +182,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
|
||||||
|
|
||||||
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
||||||
@@ -201,6 +192,8 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
const after = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
expect(after).toStrictEqual(expect.objectContaining({ id: assetId, isOffline: true }));
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -238,7 +231,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||||
@@ -247,7 +240,7 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||||
|
|
||||||
await scan(admin.accessToken, library.id);
|
await utils.scan(admin.accessToken, library.id);
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||||
|
|
||||||
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
const before = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
@@ -261,6 +254,8 @@ describe('/trash', () => {
|
|||||||
|
|
||||||
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
const after = await utils.getAssetInfo(admin.accessToken, assetId);
|
||||||
expect(after.isTrashed).toBe(true);
|
expect(after.isTrashed).toBe(true);
|
||||||
|
|
||||||
|
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -356,5 +356,24 @@ describe('/admin/users', () => {
|
|||||||
expect(status).toBe(403);
|
expect(status).toBe(403);
|
||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should restore a user', async () => {
|
||||||
|
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
|
||||||
|
|
||||||
|
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/admin/users/${user.userId}/restore`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
id: user.userId,
|
||||||
|
email: user.userEmail,
|
||||||
|
status: 'active',
|
||||||
|
deletedAt: null,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import {
|
|||||||
CheckExistingAssetsDto,
|
CheckExistingAssetsDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
|
JobCommandDto,
|
||||||
|
JobName,
|
||||||
MetadataSearchDto,
|
MetadataSearchDto,
|
||||||
Permission,
|
Permission,
|
||||||
PersonCreateDto,
|
PersonCreateDto,
|
||||||
@@ -26,9 +28,12 @@ import {
|
|||||||
deleteAssets,
|
deleteAssets,
|
||||||
getAllJobsStatus,
|
getAllJobsStatus,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
|
getConfig,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
login,
|
login,
|
||||||
|
scanLibrary,
|
||||||
searchAssets,
|
searchAssets,
|
||||||
|
sendJobCommand,
|
||||||
setBaseUrl,
|
setBaseUrl,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
tagAssets,
|
tagAssets,
|
||||||
@@ -76,6 +81,7 @@ export const immichCli = (args: string[]) =>
|
|||||||
export const immichAdmin = (args: string[]) =>
|
export const immichAdmin = (args: string[]) =>
|
||||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||||
|
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||||
|
|
||||||
const executeCommand = (command: string, args: string[]) => {
|
const executeCommand = (command: string, args: string[]) => {
|
||||||
let _resolve: (value: CommandResponse) => void;
|
let _resolve: (value: CommandResponse) => void;
|
||||||
@@ -116,6 +122,7 @@ const execPromise = promisify(exec);
|
|||||||
const onEvent = ({ event, id }: { event: EventType; id: string }) => {
|
const onEvent = ({ event, id }: { event: EventType; id: string }) => {
|
||||||
// console.log(`Received event: ${event} [id=${id}]`);
|
// console.log(`Received event: ${event} [id=${id}]`);
|
||||||
const set = events[event];
|
const set = events[event];
|
||||||
|
|
||||||
set.add(id);
|
set.add(id);
|
||||||
|
|
||||||
const idCallback = idCallbacks[id];
|
const idCallback = idCallbacks[id];
|
||||||
@@ -410,6 +417,8 @@ export const utils = {
|
|||||||
rmSync(path, { recursive: true });
|
rmSync(path, { recursive: true });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
getSystemConfig: (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
|
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
|
||||||
@@ -474,6 +483,9 @@ export const utils = {
|
|||||||
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
||||||
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
|
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
|
||||||
|
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
|
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
{
|
{
|
||||||
@@ -546,6 +558,14 @@ export const utils = {
|
|||||||
await immichCli(['login', app, `${key.secret}`]);
|
await immichCli(['login', app, `${key.secret}`]);
|
||||||
return key.secret;
|
return key.secret;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
scan: async (accessToken: string, id: string) => {
|
||||||
|
await scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
||||||
|
|
||||||
|
await utils.waitForQueueFinish(accessToken, 'library');
|
||||||
|
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
||||||
|
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
utils.initSdk();
|
utils.initSdk();
|
||||||
|
|||||||
23
i18n/af.json
23
i18n/af.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Voeg vennoot by",
|
"add_partner": "Voeg vennoot by",
|
||||||
"add_path": "Voeg pad by",
|
"add_path": "Voeg pad by",
|
||||||
"add_photos": "Voeg foto's by",
|
"add_photos": "Voeg foto's by",
|
||||||
"add_to": "Voeg na...",
|
"add_to": "Voeg by…",
|
||||||
"add_to_album": "Voeg na album",
|
"add_to_album": "Voeg na album",
|
||||||
"add_to_shared_album": "Voeg na gedeelde album",
|
"add_to_shared_album": "Voeg na gedeelde album",
|
||||||
"add_url": "Voeg URL by",
|
"add_url": "Voeg URL by",
|
||||||
@@ -57,6 +57,23 @@
|
|||||||
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
|
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
|
||||||
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
|
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
|
||||||
"external_library_management": "Eksterne Biblioteek-opsies",
|
"external_library_management": "Eksterne Biblioteek-opsies",
|
||||||
"face_detection": "Gesigsopsporing"
|
"face_detection": "Gesigsopsporing",
|
||||||
}
|
"failed_job_command": "Opdrag {command} het misluk vir werk: {job}",
|
||||||
|
"force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.",
|
||||||
|
"forcing_refresh_library_files": "Forseer herlaai van alle biblioteeklêers",
|
||||||
|
"image_format": "Formaat",
|
||||||
|
"image_format_description": "WebP produseer kleiner lêers as JPEG, maar is stadiger om te enkodeer.",
|
||||||
|
"image_prefer_embedded_preview": "Verkies ingebedde voorskou",
|
||||||
|
"image_prefer_wide_gamut": "Verkies wye spektrum",
|
||||||
|
"image_preview_description": "Mediumgrootte prent met gestroopte metadata, wat gebruik word wanneer 'n enkele bate bekyk word en vir masjienleer",
|
||||||
|
"image_preview_quality_description": "Voorskou kwaliteit van 1-100. Hoër is beter, maar produseer groter lêers en kan app-reaksie verminder. Die stel van 'n lae waarde kan masjienleerkwaliteit beïnvloed.",
|
||||||
|
"image_preview_title": "Voorskou Instellings",
|
||||||
|
"image_quality": "Kwaliteit",
|
||||||
|
"image_resolution": "Resolusie",
|
||||||
|
"image_resolution_description": "Hoër resolusies kan meer detail bewaar, maar neem langer om te enkodeer, het groter lêergroottes en kan app-reaksie verminder.",
|
||||||
|
"image_settings": "Prent Instellings",
|
||||||
|
"image_settings_description": "Bestuur die kwaliteit en resolusie van gegenereerde beelde"
|
||||||
|
},
|
||||||
|
"search_by_description": "Soek by beskrywing",
|
||||||
|
"search_by_description_example": "Stapdag in Sapa"
|
||||||
}
|
}
|
||||||
|
|||||||
101
i18n/ar.json
101
i18n/ar.json
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
|
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
|
||||||
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
|
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
|
||||||
"scanning_library": "مسح المكتبة",
|
"scanning_library": "مسح المكتبة",
|
||||||
"search_jobs": "البحث عن وظائف...",
|
"search_jobs": "البحث عن وظائف…",
|
||||||
"send_welcome_email": "إرسال بريد ترحيبي",
|
"send_welcome_email": "إرسال بريد ترحيبي",
|
||||||
"server_external_domain_settings": "إسم النطاق الخارجي",
|
"server_external_domain_settings": "إسم النطاق الخارجي",
|
||||||
"server_external_domain_settings_description": "إسم النطاق لروابط المشاركة العامة، بما في ذلك http(s)://",
|
"server_external_domain_settings_description": "إسم النطاق لروابط المشاركة العامة، بما في ذلك http(s)://",
|
||||||
@@ -250,8 +250,16 @@
|
|||||||
"storage_template_user_label": "<code>{label}</code> هو تسمية التخزين الخاصة بالمستخدم",
|
"storage_template_user_label": "<code>{label}</code> هو تسمية التخزين الخاصة بالمستخدم",
|
||||||
"system_settings": "إعدادات النظام",
|
"system_settings": "إعدادات النظام",
|
||||||
"tag_cleanup_job": "تنظيف العلامة",
|
"tag_cleanup_job": "تنظيف العلامة",
|
||||||
|
"template_email_available_tags": "يمكنك استخدام المتغيرات التالية في القالب الخاص بك: {tags}",
|
||||||
|
"template_email_if_empty": "إذا كان القالب فارغا، فسيتم استخدام البريد الإلكتروني الافتراضي.",
|
||||||
|
"template_email_invite_album": "قالب دعوة الألبوم",
|
||||||
"template_email_preview": "عرض مسبق",
|
"template_email_preview": "عرض مسبق",
|
||||||
"template_email_settings": "نماذج البريد الالكتروني",
|
"template_email_settings": "نماذج البريد الالكتروني",
|
||||||
|
"template_email_settings_description": "إدارة قوالب إشعارات البريد الإلكتروني المخصصة",
|
||||||
|
"template_email_update_album": "تحديث قالب الألبوم",
|
||||||
|
"template_email_welcome": "قالب البريد الإلكتروني الترحيبي",
|
||||||
|
"template_settings": "قوالب الإشعارات",
|
||||||
|
"template_settings_description": "إدارة القوالب المخصصة للإشعارات.",
|
||||||
"theme_custom_css_settings": "CSS مخصص",
|
"theme_custom_css_settings": "CSS مخصص",
|
||||||
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
|
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
|
||||||
"theme_settings": "إعدادات السمة",
|
"theme_settings": "إعدادات السمة",
|
||||||
@@ -281,6 +289,8 @@
|
|||||||
"transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)",
|
"transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)",
|
||||||
"transcoding_constant_rate_factor_description": "مستوى جودة الفيديو. القيم النموذجية هي 23 لـ H.264، 28 لـ HEVC، 31 لـ VP9، و 35 لـ AV1. كلما كانت القيمة أقل كان ذلك أفضل، ولكن يؤدي إلى ملفات أكبر.",
|
"transcoding_constant_rate_factor_description": "مستوى جودة الفيديو. القيم النموذجية هي 23 لـ H.264، 28 لـ HEVC، 31 لـ VP9، و 35 لـ AV1. كلما كانت القيمة أقل كان ذلك أفضل، ولكن يؤدي إلى ملفات أكبر.",
|
||||||
"transcoding_disabled_description": "لا تقم بتحويل أي مقاطع فيديو، قد تؤدي إلى عدم تشغيلها على بعض العملاء",
|
"transcoding_disabled_description": "لا تقم بتحويل أي مقاطع فيديو، قد تؤدي إلى عدم تشغيلها على بعض العملاء",
|
||||||
|
"transcoding_encoding_options": "خيارات الترميز",
|
||||||
|
"transcoding_encoding_options_description": "اضبط برامج الترميز والدقة والجودة والخيارات الأخرى لمقاطع الفيديو المشفرة",
|
||||||
"transcoding_hardware_acceleration": "التسريع العتادي",
|
"transcoding_hardware_acceleration": "التسريع العتادي",
|
||||||
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
|
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
|
||||||
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
|
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
|
||||||
@@ -293,6 +303,8 @@
|
|||||||
"transcoding_max_keyframe_interval": "الحد الأقصى للفاصل الزمني للإطار الرئيسي",
|
"transcoding_max_keyframe_interval": "الحد الأقصى للفاصل الزمني للإطار الرئيسي",
|
||||||
"transcoding_max_keyframe_interval_description": "يضبط الحد الأقصى لمسافة الإطار بين الإطارات الرئيسية. تؤدي القيم المنخفضة إلى زيادة سوء كفاءة الضغط، ولكنها تعمل على تحسين أوقات البحث وقد تعمل على تحسين الجودة في المشاهد ذات الحركة السريعة. 0 يضبط هذه القيمة تلقائيًا.",
|
"transcoding_max_keyframe_interval_description": "يضبط الحد الأقصى لمسافة الإطار بين الإطارات الرئيسية. تؤدي القيم المنخفضة إلى زيادة سوء كفاءة الضغط، ولكنها تعمل على تحسين أوقات البحث وقد تعمل على تحسين الجودة في المشاهد ذات الحركة السريعة. 0 يضبط هذه القيمة تلقائيًا.",
|
||||||
"transcoding_optimal_description": "مقاطع الفيديو ذات الدقة الأعلى من الدقة المستهدفة أو بتنسيق غير مقبول",
|
"transcoding_optimal_description": "مقاطع الفيديو ذات الدقة الأعلى من الدقة المستهدفة أو بتنسيق غير مقبول",
|
||||||
|
"transcoding_policy": "سياسة تحويل الترميز",
|
||||||
|
"transcoding_policy_description": "اضبط متى سيتم تحويل ترميز الفيديو",
|
||||||
"transcoding_preferred_hardware_device": "الجهاز المفضل",
|
"transcoding_preferred_hardware_device": "الجهاز المفضل",
|
||||||
"transcoding_preferred_hardware_device_description": "ينطبق فقط على VAAPI وQSV. يضبط عقدة dri المستخدمة لتحويل ترميز الأجهزة.",
|
"transcoding_preferred_hardware_device_description": "ينطبق فقط على VAAPI وQSV. يضبط عقدة dri المستخدمة لتحويل ترميز الأجهزة.",
|
||||||
"transcoding_preset_preset": "الضبط المُسبق (-preset)",
|
"transcoding_preset_preset": "الضبط المُسبق (-preset)",
|
||||||
@@ -301,7 +313,7 @@
|
|||||||
"transcoding_reference_frames_description": "عدد الإطارات التي يجب الرجوع إليها عند ضغط إطار معين. تعمل القيم الأعلى على تحسين كفاءة الضغط، ولكنها تبطئ عملية التشفير. 0 يضبط هذه القيمة تلقائيًا.",
|
"transcoding_reference_frames_description": "عدد الإطارات التي يجب الرجوع إليها عند ضغط إطار معين. تعمل القيم الأعلى على تحسين كفاءة الضغط، ولكنها تبطئ عملية التشفير. 0 يضبط هذه القيمة تلقائيًا.",
|
||||||
"transcoding_required_description": "فقط مقاطع الفيديو ذات التنسيق غير المقبول",
|
"transcoding_required_description": "فقط مقاطع الفيديو ذات التنسيق غير المقبول",
|
||||||
"transcoding_settings": "إعدادات تحويل ترميز الفيديو",
|
"transcoding_settings": "إعدادات تحويل ترميز الفيديو",
|
||||||
"transcoding_settings_description": "إدارة معلومات الدقة والترميز لملفات الفيديو",
|
"transcoding_settings_description": "إدارة مقاطع الفيديو التي يجب تحويل ترميزها وكيفية معالجتها",
|
||||||
"transcoding_target_resolution": "القرار المستهدف",
|
"transcoding_target_resolution": "القرار المستهدف",
|
||||||
"transcoding_target_resolution_description": "يمكن أن تحافظ الدقة الأعلى على المزيد من التفاصيل ولكنها تستغرق وقتًا أطول للتشفير، ولها أحجام ملفات أكبر، ويمكن أن تقلل من استجابة التطبيق.",
|
"transcoding_target_resolution_description": "يمكن أن تحافظ الدقة الأعلى على المزيد من التفاصيل ولكنها تستغرق وقتًا أطول للتشفير، ولها أحجام ملفات أكبر، ويمكن أن تقلل من استجابة التطبيق.",
|
||||||
"transcoding_temporal_aq": "التكميم التكيفي الزمني",
|
"transcoding_temporal_aq": "التكميم التكيفي الزمني",
|
||||||
@@ -314,7 +326,7 @@
|
|||||||
"transcoding_transcode_policy_description": "سياسة تحديد متى يجب ترميز الفيديو. سيتم دائمًا ترميز مقاطع الفيديو HDR (ما لم يتم تعطيل الترميز).",
|
"transcoding_transcode_policy_description": "سياسة تحديد متى يجب ترميز الفيديو. سيتم دائمًا ترميز مقاطع الفيديو HDR (ما لم يتم تعطيل الترميز).",
|
||||||
"transcoding_two_pass_encoding": "الترميز بمرورين",
|
"transcoding_two_pass_encoding": "الترميز بمرورين",
|
||||||
"transcoding_two_pass_encoding_setting_description": "ترميز بمرورين لإنتاج مقاطع فيديو بترميز أفضل. عند تمكين الحد الأقصى لمعدل البت (مطلوب لكي يعمل مع H.264 و HEVC)، يستخدم هذا الوضع نطاق معدل البت استنادًا إلى الحد الأقصى لمعدل البت ويتجاهل CRF. بالنسبة لـ VP9، يمكن استخدام CRF إذا تم تعطيل الحد الأقصى لمعدل البت.",
|
"transcoding_two_pass_encoding_setting_description": "ترميز بمرورين لإنتاج مقاطع فيديو بترميز أفضل. عند تمكين الحد الأقصى لمعدل البت (مطلوب لكي يعمل مع H.264 و HEVC)، يستخدم هذا الوضع نطاق معدل البت استنادًا إلى الحد الأقصى لمعدل البت ويتجاهل CRF. بالنسبة لـ VP9، يمكن استخدام CRF إذا تم تعطيل الحد الأقصى لمعدل البت.",
|
||||||
"transcoding_video_codec": "كود الفيديو",
|
"transcoding_video_codec": "ترميز الفيديو",
|
||||||
"transcoding_video_codec_description": "يتمتع VP9 بكفاءة عالية وتوافق مع الويب، ولكنه يستغرق وقتًا أطول في تحويل التعليمات البرمجية. يعمل HEVC بشكل مشابه، لكن توافقه مع الويب أقل. H.264 متوافق على نطاق واسع وسريع في تحويل التعليمات البرمجية، ولكنه ينتج ملفات أكبر بكثير. AV1 هو برنامج الترميز الأكثر كفاءة ولكنه يفتقر إلى الدعم على الأجهزة القديمة.",
|
"transcoding_video_codec_description": "يتمتع VP9 بكفاءة عالية وتوافق مع الويب، ولكنه يستغرق وقتًا أطول في تحويل التعليمات البرمجية. يعمل HEVC بشكل مشابه، لكن توافقه مع الويب أقل. H.264 متوافق على نطاق واسع وسريع في تحويل التعليمات البرمجية، ولكنه ينتج ملفات أكبر بكثير. AV1 هو برنامج الترميز الأكثر كفاءة ولكنه يفتقر إلى الدعم على الأجهزة القديمة.",
|
||||||
"trash_enabled_description": "تفعيل ميزات سلة المهملات",
|
"trash_enabled_description": "تفعيل ميزات سلة المهملات",
|
||||||
"trash_number_of_days": "عدد الأيام",
|
"trash_number_of_days": "عدد الأيام",
|
||||||
@@ -394,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
|
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
|
||||||
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
|
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
|
||||||
"asset_added_to_album": "تمت إضافته إلى الألبوم",
|
"asset_added_to_album": "تمت إضافته إلى الألبوم",
|
||||||
"asset_adding_to_album": "جارٍ الإضافة إلى الألبوم...",
|
"asset_adding_to_album": "جارٍ الإضافة إلى الألبوم…",
|
||||||
"asset_description_updated": "تم تحديث وصف المحتوى",
|
"asset_description_updated": "تم تحديث وصف المحتوى",
|
||||||
"asset_filename_is_offline": "الأصل {filename} غير متصل",
|
"asset_filename_is_offline": "الأصل {filename} غير متصل",
|
||||||
"asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة",
|
"asset_has_unassigned_faces": "يحتوي الأصل على وجوه غير مخصصة",
|
||||||
"asset_hashing": "التجزئة...",
|
"asset_hashing": "التجزئة…",
|
||||||
"asset_offline": "المحتوى غير اتصال",
|
"asset_offline": "المحتوى غير اتصال",
|
||||||
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
|
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
|
||||||
"asset_skipped": "تم تخطيه",
|
"asset_skipped": "تم تخطيه",
|
||||||
"asset_skipped_in_trash": "في سلة المهملات",
|
"asset_skipped_in_trash": "في سلة المهملات",
|
||||||
"asset_uploaded": "تم الرفع",
|
"asset_uploaded": "تم الرفع",
|
||||||
"asset_uploading": "جارٍ الرفع...",
|
"asset_uploading": "جارٍ الرفع…",
|
||||||
"assets": "المحتويات",
|
"assets": "المحتويات",
|
||||||
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
|
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
|
||||||
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
|
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
|
||||||
@@ -511,6 +523,10 @@
|
|||||||
"date_range": "نطاق الموعد",
|
"date_range": "نطاق الموعد",
|
||||||
"day": "يوم",
|
"day": "يوم",
|
||||||
"deduplicate_all": "إلغاء تكرار الكل",
|
"deduplicate_all": "إلغاء تكرار الكل",
|
||||||
|
"deduplication_criteria_1": "حجم الصورة بوحدات البايت",
|
||||||
|
"deduplication_criteria_2": "عدد بيانات EXIF",
|
||||||
|
"deduplication_info": "معلومات إلغاء البيانات المكررة",
|
||||||
|
"deduplication_info_description": "لتحديد الأصول مسبقا تلقائيا وإزالة التكرارات بكميات كبيرة، ننظر إلى:",
|
||||||
"default_locale": "اللغة الافتراضية",
|
"default_locale": "اللغة الافتراضية",
|
||||||
"default_locale_description": "تنسيق التواريخ والأرقام بناءً على لغة المتصفح الخاص بك",
|
"default_locale_description": "تنسيق التواريخ والأرقام بناءً على لغة المتصفح الخاص بك",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
@@ -726,6 +742,7 @@
|
|||||||
"external": "خارجي",
|
"external": "خارجي",
|
||||||
"external_libraries": "المكتبات الخارجية",
|
"external_libraries": "المكتبات الخارجية",
|
||||||
"face_unassigned": "غير معين",
|
"face_unassigned": "غير معين",
|
||||||
|
"failed_to_load_assets": "فشل تحميل الأصول",
|
||||||
"favorite": "مفضل",
|
"favorite": "مفضل",
|
||||||
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
|
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
|
||||||
"favorites": "المفضلة",
|
"favorites": "المفضلة",
|
||||||
@@ -746,10 +763,13 @@
|
|||||||
"get_help": "الحصول على المساعدة",
|
"get_help": "الحصول على المساعدة",
|
||||||
"getting_started": "البدء",
|
"getting_started": "البدء",
|
||||||
"go_back": "الرجوع للخلف",
|
"go_back": "الرجوع للخلف",
|
||||||
|
"go_to_folder": "اذهب إلى المجلد",
|
||||||
"go_to_search": "اذهب إلى البحث",
|
"go_to_search": "اذهب إلى البحث",
|
||||||
"group_albums_by": "تجميع الألبومات حسب...",
|
"group_albums_by": "تجميع الألبومات حسب...",
|
||||||
|
"group_country": "مجموعة البلد",
|
||||||
"group_no": "بدون تجميع",
|
"group_no": "بدون تجميع",
|
||||||
"group_owner": "تجميع حسب المالك",
|
"group_owner": "تجميع حسب المالك",
|
||||||
|
"group_places_by": "تجميع الأماكن حسب",
|
||||||
"group_year": "تجميع حسب السنة",
|
"group_year": "تجميع حسب السنة",
|
||||||
"has_quota": "محدد بحصة",
|
"has_quota": "محدد بحصة",
|
||||||
"hi_user": "مرحبا {name} ({email})",
|
"hi_user": "مرحبا {name} ({email})",
|
||||||
@@ -782,6 +802,7 @@
|
|||||||
"include_shared_albums": "تضمين الألبومات المشتركة",
|
"include_shared_albums": "تضمين الألبومات المشتركة",
|
||||||
"include_shared_partner_assets": "تضمين محتويات الشريك المشتركة",
|
"include_shared_partner_assets": "تضمين محتويات الشريك المشتركة",
|
||||||
"individual_share": "حصة فردية",
|
"individual_share": "حصة فردية",
|
||||||
|
"individual_shares": "المشاركات الفردية",
|
||||||
"info": "معلومات",
|
"info": "معلومات",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "كل يوم الساعة الواحدة ظهرا",
|
"day_at_onepm": "كل يوم الساعة الواحدة ظهرا",
|
||||||
@@ -804,6 +825,7 @@
|
|||||||
"latest_version": "احدث اصدار",
|
"latest_version": "احدث اصدار",
|
||||||
"latitude": "خط العرض",
|
"latitude": "خط العرض",
|
||||||
"leave": "مغادرة",
|
"leave": "مغادرة",
|
||||||
|
"lens_model": "نموذج العدسات",
|
||||||
"let_others_respond": "دع الآخرين يستجيبون",
|
"let_others_respond": "دع الآخرين يستجيبون",
|
||||||
"level": "المستوى",
|
"level": "المستوى",
|
||||||
"library": "مكتبة",
|
"library": "مكتبة",
|
||||||
@@ -966,6 +988,7 @@
|
|||||||
"pick_a_location": "اختر موقعًا",
|
"pick_a_location": "اختر موقعًا",
|
||||||
"place": "مكان",
|
"place": "مكان",
|
||||||
"places": "الأماكن",
|
"places": "الأماكن",
|
||||||
|
"places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}",
|
||||||
"play": "تشغيل",
|
"play": "تشغيل",
|
||||||
"play_memories": "تشغيل الذكريات",
|
"play_memories": "تشغيل الذكريات",
|
||||||
"play_motion_photo": "تشغيل الصور المتحركة",
|
"play_motion_photo": "تشغيل الصور المتحركة",
|
||||||
@@ -1025,6 +1048,7 @@
|
|||||||
"reassigned_assets_to_new_person": "تمت إعادة تعيين {count, plural, one {# المحتوى} other {# المحتويات}} إلى شخص جديد",
|
"reassigned_assets_to_new_person": "تمت إعادة تعيين {count, plural, one {# المحتوى} other {# المحتويات}} إلى شخص جديد",
|
||||||
"reassing_hint": "تعيين المحتويات المحددة لشخص موجود",
|
"reassing_hint": "تعيين المحتويات المحددة لشخص موجود",
|
||||||
"recent": "حديث",
|
"recent": "حديث",
|
||||||
|
"recent-albums": "ألبومات الحديثة",
|
||||||
"recent_searches": "عمليات البحث الأخيرة",
|
"recent_searches": "عمليات البحث الأخيرة",
|
||||||
"refresh": "تحديث",
|
"refresh": "تحديث",
|
||||||
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
|
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
|
||||||
@@ -1046,6 +1070,7 @@
|
|||||||
"remove_from_album": "إزالة من الألبوم",
|
"remove_from_album": "إزالة من الألبوم",
|
||||||
"remove_from_favorites": "إزالة من المفضلة",
|
"remove_from_favorites": "إزالة من المفضلة",
|
||||||
"remove_from_shared_link": "إزالة من الرابط المشترك",
|
"remove_from_shared_link": "إزالة من الرابط المشترك",
|
||||||
|
"remove_url": "إزالة عنوان URL",
|
||||||
"remove_user": "إزالة المستخدم",
|
"remove_user": "إزالة المستخدم",
|
||||||
"removed_api_key": "تم إزالة مفتاح API: {name}",
|
"removed_api_key": "تم إزالة مفتاح API: {name}",
|
||||||
"removed_from_archive": "تمت إزالتها من الأرشيف",
|
"removed_from_archive": "تمت إزالتها من الأرشيف",
|
||||||
@@ -1084,15 +1109,18 @@
|
|||||||
"scan_library": "مسح",
|
"scan_library": "مسح",
|
||||||
"scan_settings": "إعدادات الفحص",
|
"scan_settings": "إعدادات الفحص",
|
||||||
"scanning_for_album": "جارٍ الفحص عن ألبوم...",
|
"scanning_for_album": "جارٍ الفحص عن ألبوم...",
|
||||||
"search": "بحث",
|
"search": "البحث",
|
||||||
"search_albums": "بحث في الألبومات",
|
"search_albums": "البحث في الألبومات",
|
||||||
"search_by_context": "البحث حسب السياق",
|
"search_by_context": "البحث حسب السياق",
|
||||||
"search_by_filename": "إبحث بإسم الملف أو نوعه",
|
"search_by_description": "البحث حسب الوصف",
|
||||||
|
"search_by_description_example": "يوم المشي لمسافات طويلة في سابا",
|
||||||
|
"search_by_filename": "البحث بإسم الملف أو نوعه",
|
||||||
"search_by_filename_example": "كـ IMG_1234.JPG أو PNG",
|
"search_by_filename_example": "كـ IMG_1234.JPG أو PNG",
|
||||||
"search_camera_make": "البحث حسب الشركة المصنعة للكاميرا...",
|
"search_camera_make": "البحث حسب الشركة المصنعة للكاميرا...",
|
||||||
"search_camera_model": "البحث حسب موديل الكاميرا...",
|
"search_camera_model": "البحث حسب موديل الكاميرا...",
|
||||||
"search_city": "البحث حسب المدينة...",
|
"search_city": "البحث حسب المدينة...",
|
||||||
"search_country": "البحث حسب الدولة...",
|
"search_country": "البحث حسب الدولة...",
|
||||||
|
"search_for": "البحث عن",
|
||||||
"search_for_existing_person": "البحث عن شخص موجود",
|
"search_for_existing_person": "البحث عن شخص موجود",
|
||||||
"search_no_people": "لا يوجد أشخاص",
|
"search_no_people": "لا يوجد أشخاص",
|
||||||
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
|
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
|
||||||
@@ -1104,36 +1132,37 @@
|
|||||||
"search_tags": "البحث عن العلامات...",
|
"search_tags": "البحث عن العلامات...",
|
||||||
"search_timezone": "البحث حسب المنطقة الزمنية...",
|
"search_timezone": "البحث حسب المنطقة الزمنية...",
|
||||||
"search_type": "نوع البحث",
|
"search_type": "نوع البحث",
|
||||||
"search_your_photos": "ابحث عن صورك",
|
"search_your_photos": "البحث عن صورك",
|
||||||
"searching_locales": "جارٍ البحث في اللغات...",
|
"searching_locales": "جارٍ البحث في اللغات...",
|
||||||
"second": "ثانية",
|
"second": "ثانية",
|
||||||
"see_all_people": "عرض جميع الأشخاص",
|
"see_all_people": "عرض جميع الأشخاص",
|
||||||
"select_album_cover": "حدد غلاف الألبوم",
|
"select_album_cover": "تحديد غلاف الألبوم",
|
||||||
"select_all": "تحديد الكل",
|
"select_all": "تحديد الكل",
|
||||||
"select_all_duplicates": "تحديد جميع النسخ المكررة",
|
"select_all_duplicates": "تحديد جميع النسخ المكررة",
|
||||||
"select_avatar_color": "حدد لون الصورة الشخصية",
|
"select_avatar_color": "تحديد لون الصورة الشخصية",
|
||||||
"select_face": "اختيار وجه",
|
"select_face": "تحديد وجه",
|
||||||
"select_featured_photo": "حدد الصورة المميزة",
|
"select_featured_photo": "تحديد الصورة المميزة",
|
||||||
"select_from_computer": "اختر من الجهاز",
|
"select_from_computer": "تحديد من الحاسب الآلي",
|
||||||
"select_keep_all": "حدد الاحتفاظ بالكل",
|
"select_keep_all": "تحديد الأحتفاظ بالكل",
|
||||||
"select_library_owner": "اختر مالِك المكتبة",
|
"select_library_owner": "تحديد مالِك المكتبة",
|
||||||
"select_new_face": "اختيار وجه جديد",
|
"select_new_face": "تحديد وجه جديد",
|
||||||
"select_photos": "حدد الصور",
|
"select_photos": "تحديد الصور",
|
||||||
"select_trash_all": "حدّد حذف الكلِ",
|
"select_trash_all": "تحديد حذف الكلِ",
|
||||||
"selected": "المُحدّد",
|
"selected": "التحديد",
|
||||||
"selected_count": "{count, plural, other {# محددة }}",
|
"selected_count": "{count, plural, other {# محددة }}",
|
||||||
"send_message": "أرسل رسالة",
|
"send_message": "إرسال رسالة",
|
||||||
"send_welcome_email": "أرسل بريدًا إلكترونيًا ترحيبيًا",
|
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
|
||||||
"server_offline": "الخادم غير متصل",
|
"server_offline": "الخادم غير متصل",
|
||||||
"server_online": "الخادم متصل",
|
"server_online": "الخادم متصل",
|
||||||
"server_stats": "إحصائيات الخادم",
|
"server_stats": "إحصائيات الخادم",
|
||||||
"server_version": "إصدار الخادم",
|
"server_version": "إصدار الخادم",
|
||||||
"set": "تعيين",
|
"set": "تحديد",
|
||||||
"set_as_album_cover": "تعيين كغلاف للألبوم",
|
"set_as_album_cover": "تحديد كغلاف للألبوم",
|
||||||
"set_as_profile_picture": "تعيين كصورة الملف الشخصي",
|
"set_as_featured_photo": "تحديد كصورة مميزة",
|
||||||
|
"set_as_profile_picture": "تحديد كصورة الملف الشخصي",
|
||||||
"set_date_of_birth": "تحديد تاريخ الميلاد",
|
"set_date_of_birth": "تحديد تاريخ الميلاد",
|
||||||
"set_profile_picture": "تعيين صورة الملف الشخصي",
|
"set_profile_picture": "تحديد صورة الملف الشخصي",
|
||||||
"set_slideshow_to_fullscreen": "اضبط عرض الشرائح على وضع ملء الشاشة",
|
"set_slideshow_to_fullscreen": "تحديد عرض الشرائح على وضع ملء الشاشة",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
"settings_saved": "تم حفظ الإعدادات",
|
"settings_saved": "تم حفظ الإعدادات",
|
||||||
"share": "مشاركة",
|
"share": "مشاركة",
|
||||||
@@ -1144,6 +1173,7 @@
|
|||||||
"shared_from_partner": "صور من {partner}",
|
"shared_from_partner": "صور من {partner}",
|
||||||
"shared_link_options": "خيارات الرابط المشترك",
|
"shared_link_options": "خيارات الرابط المشترك",
|
||||||
"shared_links": "روابط مشتركة",
|
"shared_links": "روابط مشتركة",
|
||||||
|
"shared_links_description": "وصف الروابط المشتركة",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
|
||||||
"shared_with_partner": "تمت المشاركة مع {partner}",
|
"shared_with_partner": "تمت المشاركة مع {partner}",
|
||||||
"sharing": "مشاركة",
|
"sharing": "مشاركة",
|
||||||
@@ -1155,17 +1185,18 @@
|
|||||||
"show_all_people": "إظهار جميع الأشخاص",
|
"show_all_people": "إظهار جميع الأشخاص",
|
||||||
"show_and_hide_people": "إظهار وإخفاء الأشخاص",
|
"show_and_hide_people": "إظهار وإخفاء الأشخاص",
|
||||||
"show_file_location": "إظهار موقع الملف",
|
"show_file_location": "إظهار موقع الملف",
|
||||||
"show_gallery": "عرض المعرض",
|
"show_gallery": "إظهار المعرض",
|
||||||
"show_hidden_people": "إظهار الأشخاص المخفيين",
|
"show_hidden_people": "إظهار الأشخاص المخفيين",
|
||||||
"show_in_timeline": "عرض في المخطط الزمني",
|
"show_in_timeline": "إظهار في المخطط الزمني",
|
||||||
"show_in_timeline_setting_description": "عرض الصور ومقاطع الفيديو من هذا المستخدم في المخطط الزمني الخاص بك",
|
"show_in_timeline_setting_description": "إظهار الصور ومقاطع الفيديو من هذا المستخدم في المخطط الزمني الخاص بك",
|
||||||
"show_keyboard_shortcuts": "إظهار اختصارات لوحة المفاتيح",
|
"show_keyboard_shortcuts": "إظهار اختصارات لوحة المفاتيح",
|
||||||
"show_metadata": "عرض البيانات الوصفية",
|
"show_metadata": "إظهار البيانات الوصفية",
|
||||||
"show_or_hide_info": "إظهار أو إخفاء المعلومات",
|
"show_or_hide_info": "إظهار أو إخفاء المعلومات",
|
||||||
"show_password": "عرض كلمة المرور",
|
"show_password": "إظهار كلمة المرور",
|
||||||
"show_person_options": "إظهار خيارات الشخص",
|
"show_person_options": "إظهار خيارات الشخص",
|
||||||
"show_progress_bar": "إظهار شريط التقدم",
|
"show_progress_bar": "إظهار شريط التقدم",
|
||||||
"show_search_options": "إظهار خيارات البحث",
|
"show_search_options": "إظهار خيارات البحث",
|
||||||
|
"show_shared_links": "عرض الروابط المشتركة",
|
||||||
"show_slideshow_transition": "إظهار انتقال عرض الشرائح",
|
"show_slideshow_transition": "إظهار انتقال عرض الشرائح",
|
||||||
"show_supporter_badge": "شارة المؤيد",
|
"show_supporter_badge": "شارة المؤيد",
|
||||||
"show_supporter_badge_description": "إظهار شارة المؤيد",
|
"show_supporter_badge_description": "إظهار شارة المؤيد",
|
||||||
@@ -1173,7 +1204,7 @@
|
|||||||
"sidebar": "الشريط الجانبي",
|
"sidebar": "الشريط الجانبي",
|
||||||
"sidebar_display_description": "عرض رابط للعرض في الشريط الجانبي",
|
"sidebar_display_description": "عرض رابط للعرض في الشريط الجانبي",
|
||||||
"sign_out": "خروج",
|
"sign_out": "خروج",
|
||||||
"sign_up": "تسجيل",
|
"sign_up": "التسجيل",
|
||||||
"size": "الحجم",
|
"size": "الحجم",
|
||||||
"skip_to_content": "تخطي إلى المحتوى",
|
"skip_to_content": "تخطي إلى المحتوى",
|
||||||
"skip_to_folders": "تخطي إلى المجلدات",
|
"skip_to_folders": "تخطي إلى المجلدات",
|
||||||
@@ -1185,6 +1216,7 @@
|
|||||||
"sort_items": "عدد العناصر",
|
"sort_items": "عدد العناصر",
|
||||||
"sort_modified": "تم تعديل التاريخ",
|
"sort_modified": "تم تعديل التاريخ",
|
||||||
"sort_oldest": "أقدم صورة",
|
"sort_oldest": "أقدم صورة",
|
||||||
|
"sort_people_by_similarity": "رتب الأشخاص حسب التشابه",
|
||||||
"sort_recent": "أحدث صورة",
|
"sort_recent": "أحدث صورة",
|
||||||
"sort_title": "العنوان",
|
"sort_title": "العنوان",
|
||||||
"source": "المصدر",
|
"source": "المصدر",
|
||||||
@@ -1252,6 +1284,7 @@
|
|||||||
"unfavorite": "أزل التفضيل",
|
"unfavorite": "أزل التفضيل",
|
||||||
"unhide_person": "أظهر الشخص",
|
"unhide_person": "أظهر الشخص",
|
||||||
"unknown": "غير معروف",
|
"unknown": "غير معروف",
|
||||||
|
"unknown_country": "بلد غير معروف",
|
||||||
"unknown_year": "سنة غير معروفة",
|
"unknown_year": "سنة غير معروفة",
|
||||||
"unlimited": "غير محدود",
|
"unlimited": "غير محدود",
|
||||||
"unlink_motion_video": "إلغاء ربط فيديو الحركة",
|
"unlink_motion_video": "إلغاء ربط فيديو الحركة",
|
||||||
|
|||||||
14
i18n/ca.json
14
i18n/ca.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Afegir company/a",
|
"add_partner": "Afegir company/a",
|
||||||
"add_path": "Afegir una ruta",
|
"add_path": "Afegir una ruta",
|
||||||
"add_photos": "Afegir fotografies",
|
"add_photos": "Afegir fotografies",
|
||||||
"add_to": "Afegir a...",
|
"add_to": "Afegir a…",
|
||||||
"add_to_album": "Afegir a un l'àlbum",
|
"add_to_album": "Afegir a un l'àlbum",
|
||||||
"add_to_shared_album": "Afegir a un àlbum compartit",
|
"add_to_shared_album": "Afegir a un àlbum compartit",
|
||||||
"add_url": "Afegir URL",
|
"add_url": "Afegir URL",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Restablir configuracions per defecte",
|
"reset_settings_to_default": "Restablir configuracions per defecte",
|
||||||
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
|
"reset_settings_to_recent_saved": "Restablir la configuració guardada més recent",
|
||||||
"scanning_library": "Escanejant biblioteca",
|
"scanning_library": "Escanejant biblioteca",
|
||||||
"search_jobs": "Tasques de cerca...",
|
"search_jobs": "Cercar treballs…",
|
||||||
"send_welcome_email": "Enviar correu electrònic de benvinguda",
|
"send_welcome_email": "Enviar correu electrònic de benvinguda",
|
||||||
"server_external_domain_settings": "Domini extern",
|
"server_external_domain_settings": "Domini extern",
|
||||||
"server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
|
"server_external_domain_settings_description": "Domini per enllaços públics compartits, incloent http(s)://",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Són la mateixa persona?",
|
"are_these_the_same_person": "Són la mateixa persona?",
|
||||||
"are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
|
"are_you_sure_to_do_this": "Esteu segurs que voleu fer-ho?",
|
||||||
"asset_added_to_album": "Afegit a l'àlbum",
|
"asset_added_to_album": "Afegit a l'àlbum",
|
||||||
"asset_adding_to_album": "Afegint a l'àlbum...",
|
"asset_adding_to_album": "Afegint a l'àlbum…",
|
||||||
"asset_description_updated": "La descripció del recurs s'ha actualitzat",
|
"asset_description_updated": "La descripció del recurs s'ha actualitzat",
|
||||||
"asset_filename_is_offline": "L'element {filename} està fora de línia",
|
"asset_filename_is_offline": "L'element {filename} està fora de línia",
|
||||||
"asset_has_unassigned_faces": "L'element té cares no assignades",
|
"asset_has_unassigned_faces": "L'element té cares no assignades",
|
||||||
"asset_hashing": "Hashing...",
|
"asset_hashing": "Hashing…",
|
||||||
"asset_offline": "Element fora de línia",
|
"asset_offline": "Element fora de línia",
|
||||||
"asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.",
|
"asset_offline_description": "Aquest recurs extern ja no es troba al disc. Poseu-vos en contacte amb el vostre administrador d'Immich per obtenir ajuda.",
|
||||||
"asset_skipped": "Saltat",
|
"asset_skipped": "Saltat",
|
||||||
"asset_skipped_in_trash": "A la paperera",
|
"asset_skipped_in_trash": "A la paperera",
|
||||||
"asset_uploaded": "Carregat",
|
"asset_uploaded": "Carregat",
|
||||||
"asset_uploading": "S'està carregant...",
|
"asset_uploading": "S'està carregant…",
|
||||||
"assets": "Elements",
|
"assets": "Elements",
|
||||||
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}",
|
"assets_added_count": "{count, plural, one {Afegit un element} other {Afegits # elements}}",
|
||||||
"assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
|
"assets_added_to_album_count": "{count, plural, one {Afegit un element} other {Afegits # elements}} a l'àlbum",
|
||||||
@@ -822,6 +822,7 @@
|
|||||||
"latest_version": "Última versió",
|
"latest_version": "Última versió",
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
"leave": "Marxar",
|
"leave": "Marxar",
|
||||||
|
"lens_model": "Model de lents",
|
||||||
"let_others_respond": "Deixa que els altres responguin",
|
"let_others_respond": "Deixa que els altres responguin",
|
||||||
"level": "Nivell",
|
"level": "Nivell",
|
||||||
"library": "Bibilioteca",
|
"library": "Bibilioteca",
|
||||||
@@ -1094,7 +1095,7 @@
|
|||||||
"review_duplicates": "Revisar duplicats",
|
"review_duplicates": "Revisar duplicats",
|
||||||
"role": "Rol",
|
"role": "Rol",
|
||||||
"role_editor": "Editor",
|
"role_editor": "Editor",
|
||||||
"role_viewer": "Visor",
|
"role_viewer": "Visualitzador",
|
||||||
"save": "Desa",
|
"save": "Desa",
|
||||||
"saved_api_key": "Clau d'API guardada",
|
"saved_api_key": "Clau d'API guardada",
|
||||||
"saved_profile": "Perfil guardat",
|
"saved_profile": "Perfil guardat",
|
||||||
@@ -1113,6 +1114,7 @@
|
|||||||
"search_camera_model": "Buscar per model de càmera...",
|
"search_camera_model": "Buscar per model de càmera...",
|
||||||
"search_city": "Buscar per ciutat...",
|
"search_city": "Buscar per ciutat...",
|
||||||
"search_country": "Buscar per país...",
|
"search_country": "Buscar per país...",
|
||||||
|
"search_for": "Cercar",
|
||||||
"search_for_existing_person": "Busca una persona existent",
|
"search_for_existing_person": "Busca una persona existent",
|
||||||
"search_no_people": "Cap persona",
|
"search_no_people": "Cap persona",
|
||||||
"search_no_people_named": "Cap persona anomenada \"{name}\"",
|
"search_no_people_named": "Cap persona anomenada \"{name}\"",
|
||||||
|
|||||||
21
i18n/cs.json
21
i18n/cs.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Přidat partnera",
|
"add_partner": "Přidat partnera",
|
||||||
"add_path": "Přidat cestu",
|
"add_path": "Přidat cestu",
|
||||||
"add_photos": "Přidat fotky",
|
"add_photos": "Přidat fotky",
|
||||||
"add_to": "Přidat do...",
|
"add_to": "Přidat do…",
|
||||||
"add_to_album": "Přidat do alba",
|
"add_to_album": "Přidat do alba",
|
||||||
"add_to_shared_album": "Přidat do sdíleného alba",
|
"add_to_shared_album": "Přidat do sdíleného alba",
|
||||||
"add_url": "Přidat URL",
|
"add_url": "Přidat URL",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Obnovení výchozího nastavení",
|
"reset_settings_to_default": "Obnovení výchozího nastavení",
|
||||||
"reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
|
"reset_settings_to_recent_saved": "Obnovit poslední uložené nastavení",
|
||||||
"scanning_library": "Prohledat knihovnu",
|
"scanning_library": "Prohledat knihovnu",
|
||||||
"search_jobs": "Hledat úlohy...",
|
"search_jobs": "Hledat úlohy…",
|
||||||
"send_welcome_email": "Odeslat uvítací e-mail",
|
"send_welcome_email": "Odeslat uvítací e-mail",
|
||||||
"server_external_domain_settings": "Externí doména",
|
"server_external_domain_settings": "Externí doména",
|
||||||
"server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://",
|
"server_external_domain_settings_description": "Doména pro veřejně sdílené odkazy, včetně http(s)://",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Jedná se o stejnou osobu?",
|
"are_these_the_same_person": "Jedná se o stejnou osobu?",
|
||||||
"are_you_sure_to_do_this": "Opravdu to chcete udělat?",
|
"are_you_sure_to_do_this": "Opravdu to chcete udělat?",
|
||||||
"asset_added_to_album": "Přidáno do alba",
|
"asset_added_to_album": "Přidáno do alba",
|
||||||
"asset_adding_to_album": "Přidávání do alba...",
|
"asset_adding_to_album": "Přidávání do alba…",
|
||||||
"asset_description_updated": "Popis položky byl aktualizován",
|
"asset_description_updated": "Popis položky byl aktualizován",
|
||||||
"asset_filename_is_offline": "Položka {filename} je offline",
|
"asset_filename_is_offline": "Položka {filename} je offline",
|
||||||
"asset_has_unassigned_faces": "Položka má nepřiřazené obličeje",
|
"asset_has_unassigned_faces": "Položka má nepřiřazené obličeje",
|
||||||
"asset_hashing": "Hashování...",
|
"asset_hashing": "Hashování…",
|
||||||
"asset_offline": "Offline položka",
|
"asset_offline": "Offline položka",
|
||||||
"asset_offline_description": "Toto externí položka se již na disku nenachází. Obraťte se na Immich správce a požádejte o pomoc.",
|
"asset_offline_description": "Toto externí položka se již na disku nenachází. Obraťte se na Immich správce a požádejte o pomoc.",
|
||||||
"asset_skipped": "Přeskočeno",
|
"asset_skipped": "Přeskočeno",
|
||||||
"asset_skipped_in_trash": "V koši",
|
"asset_skipped_in_trash": "V koši",
|
||||||
"asset_uploaded": "Nahráno",
|
"asset_uploaded": "Nahráno",
|
||||||
"asset_uploading": "Nahrávání...",
|
"asset_uploading": "Nahrávání…",
|
||||||
"assets": "Položky",
|
"assets": "Položky",
|
||||||
"assets_added_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}}",
|
"assets_added_count": "{count, plural, one {Přidána # položka} few {Přidány # položky} other {Přidáno # položek}}",
|
||||||
"assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}",
|
"assets_added_to_album_count": "Do alba {count, plural, one {byla přidána # položka} few {byly přidány # položky} other {bylo přidáno # položek}}",
|
||||||
@@ -766,8 +766,10 @@
|
|||||||
"go_to_folder": "Přejít do složky",
|
"go_to_folder": "Přejít do složky",
|
||||||
"go_to_search": "Přejít na vyhledávání",
|
"go_to_search": "Přejít na vyhledávání",
|
||||||
"group_albums_by": "Seskupit alba podle...",
|
"group_albums_by": "Seskupit alba podle...",
|
||||||
|
"group_country": "Seskupit podle země",
|
||||||
"group_no": "Neseskupovat",
|
"group_no": "Neseskupovat",
|
||||||
"group_owner": "Seskupit podle uživatele",
|
"group_owner": "Seskupit podle uživatele",
|
||||||
|
"group_places_by": "Seskupit místa podle...",
|
||||||
"group_year": "Seskupit podle roku",
|
"group_year": "Seskupit podle roku",
|
||||||
"has_quota": "Má kvótu",
|
"has_quota": "Má kvótu",
|
||||||
"hi_user": "Ahoj {name} ({email})",
|
"hi_user": "Ahoj {name} ({email})",
|
||||||
@@ -800,6 +802,7 @@
|
|||||||
"include_shared_albums": "Včetně sdílených alb",
|
"include_shared_albums": "Včetně sdílených alb",
|
||||||
"include_shared_partner_assets": "Včetně sdílených položek partnera",
|
"include_shared_partner_assets": "Včetně sdílených položek partnera",
|
||||||
"individual_share": "Sdílení jednotlivých položek",
|
"individual_share": "Sdílení jednotlivých položek",
|
||||||
|
"individual_shares": "Sdílení jednotlivých položek",
|
||||||
"info": "Informace",
|
"info": "Informace",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Každý den ve 13:00",
|
"day_at_onepm": "Každý den ve 13:00",
|
||||||
@@ -822,6 +825,7 @@
|
|||||||
"latest_version": "Nejnovější verze",
|
"latest_version": "Nejnovější verze",
|
||||||
"latitude": "Zeměpisná šířka",
|
"latitude": "Zeměpisná šířka",
|
||||||
"leave": "Opustit",
|
"leave": "Opustit",
|
||||||
|
"lens_model": "Model objektivu",
|
||||||
"let_others_respond": "Nechte ostatní reagovat",
|
"let_others_respond": "Nechte ostatní reagovat",
|
||||||
"level": "Úroveň",
|
"level": "Úroveň",
|
||||||
"library": "Knihovna",
|
"library": "Knihovna",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Vyberte polohu",
|
"pick_a_location": "Vyberte polohu",
|
||||||
"place": "Místo",
|
"place": "Místo",
|
||||||
"places": "Místa",
|
"places": "Místa",
|
||||||
|
"places_count": "{count, plural, one {{count, number} místo} few {{count, number} místa} other {{count, number} míst}}",
|
||||||
"play": "Přehrávat",
|
"play": "Přehrávat",
|
||||||
"play_memories": "Přehrát vzpomníky",
|
"play_memories": "Přehrát vzpomníky",
|
||||||
"play_motion_photo": "Přehrát pohybovou fotografii",
|
"play_motion_photo": "Přehrát pohybovou fotografii",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Hledat",
|
"search": "Hledat",
|
||||||
"search_albums": "Vyhledávejte alba",
|
"search_albums": "Vyhledávejte alba",
|
||||||
"search_by_context": "Vyhledávání podle obsahu",
|
"search_by_context": "Vyhledávání podle obsahu",
|
||||||
|
"search_by_description": "Vyhledávat podle popisu",
|
||||||
|
"search_by_description_example": "Pěší turistika v Sapě",
|
||||||
"search_by_filename": "Vyhledávání podle názvu nebo přípony souboru",
|
"search_by_filename": "Vyhledávání podle názvu nebo přípony souboru",
|
||||||
"search_by_filename_example": "např. IMG_1234.JPG nebo PNG",
|
"search_by_filename_example": "např. IMG_1234.JPG nebo PNG",
|
||||||
"search_camera_make": "Vyhledat výrobce fotoaparátu...",
|
"search_camera_make": "Vyhledat výrobce fotoaparátu...",
|
||||||
"search_camera_model": "Vyhledat model fotoaparátu...",
|
"search_camera_model": "Vyhledat model fotoaparátu...",
|
||||||
"search_city": "Vyhledat město...",
|
"search_city": "Vyhledat město...",
|
||||||
"search_country": "Vyhledat zemi...",
|
"search_country": "Vyhledat zemi...",
|
||||||
|
"search_for": "Vyhledat",
|
||||||
"search_for_existing_person": "Vyhledat existující osobu",
|
"search_for_existing_person": "Vyhledat existující osobu",
|
||||||
"search_no_people": "Žádní lidé",
|
"search_no_people": "Žádní lidé",
|
||||||
"search_no_people_named": "Žádní lidé se jménem \"{name}\"",
|
"search_no_people_named": "Žádní lidé se jménem \"{name}\"",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Fotky od {partner}",
|
"shared_from_partner": "Fotky od {partner}",
|
||||||
"shared_link_options": "Možnosti sdíleného odkazu",
|
"shared_link_options": "Možnosti sdíleného odkazu",
|
||||||
"shared_links": "Sdílené odkazy",
|
"shared_links": "Sdílené odkazy",
|
||||||
|
"shared_links_description": "Sdílet fotky a videa pomocí odkazu",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, one {# sdílená fotografie a video.} few {# sdílené fotografie a videa.} other {# sdílených fotografií a videí.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, one {# sdílená fotografie a video.} few {# sdílené fotografie a videa.} other {# sdílených fotografií a videí.}}",
|
||||||
"shared_with_partner": "Sdíleno s {partner}",
|
"shared_with_partner": "Sdíleno s {partner}",
|
||||||
"sharing": "Sdílení",
|
"sharing": "Sdílení",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Zobrazit možnosti osoby",
|
"show_person_options": "Zobrazit možnosti osoby",
|
||||||
"show_progress_bar": "Zobrazit ukazatel průběhu",
|
"show_progress_bar": "Zobrazit ukazatel průběhu",
|
||||||
"show_search_options": "Zobrazit možnosti vyhledávání",
|
"show_search_options": "Zobrazit možnosti vyhledávání",
|
||||||
|
"show_shared_links": "Zobrazit sdílené odkazy",
|
||||||
"show_slideshow_transition": "Zobrazit přechod prezentace",
|
"show_slideshow_transition": "Zobrazit přechod prezentace",
|
||||||
"show_supporter_badge": "Odznak podporovatele",
|
"show_supporter_badge": "Odznak podporovatele",
|
||||||
"show_supporter_badge_description": "Zobrazit odznak podporovatele",
|
"show_supporter_badge_description": "Zobrazit odznak podporovatele",
|
||||||
@@ -1274,6 +1284,7 @@
|
|||||||
"unfavorite": "Zrušit oblíbení",
|
"unfavorite": "Zrušit oblíbení",
|
||||||
"unhide_person": "Zrušit skrytí osoby",
|
"unhide_person": "Zrušit skrytí osoby",
|
||||||
"unknown": "Neznámý",
|
"unknown": "Neznámý",
|
||||||
|
"unknown_country": "Neznámá země",
|
||||||
"unknown_year": "Neznámý rok",
|
"unknown_year": "Neznámý rok",
|
||||||
"unlimited": "Neomezeně",
|
"unlimited": "Neomezeně",
|
||||||
"unlink_motion_video": "Odpojit pohyblivé video",
|
"unlink_motion_video": "Odpojit pohyblivé video",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Мӑшӑр хуш",
|
"add_partner": "Мӑшӑр хуш",
|
||||||
"add_path": "Ҫулне хуш",
|
"add_path": "Ҫулне хуш",
|
||||||
"add_photos": "Сӑнӳкерчӗксем хуш",
|
"add_photos": "Сӑнӳкерчӗксем хуш",
|
||||||
"add_to": "Мӗн те пулин хуш...",
|
"add_to": "Мӗн те пулин хуш…",
|
||||||
"add_to_album": "Альбома хуш",
|
"add_to_album": "Альбома хуш",
|
||||||
"add_to_shared_album": "Пӗрлехи альбома хуш",
|
"add_to_shared_album": "Пӗрлехи альбома хуш",
|
||||||
"add_url": "URL хушӑр",
|
"add_url": "URL хушӑр",
|
||||||
|
|||||||
93
i18n/da.json
93
i18n/da.json
@@ -7,7 +7,7 @@
|
|||||||
"actions": "Handlinger",
|
"actions": "Handlinger",
|
||||||
"active": "Aktive",
|
"active": "Aktive",
|
||||||
"activity": "Aktivitet",
|
"activity": "Aktivitet",
|
||||||
"activity_changed": "Aktivitet er {aktiveret, vælg, sandt {aktiveret} andet {deaktiveret}}",
|
"activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}",
|
||||||
"add": "Tilføj",
|
"add": "Tilføj",
|
||||||
"add_a_description": "Tilføj en beskrivelse",
|
"add_a_description": "Tilføj en beskrivelse",
|
||||||
"add_a_location": "Tilføj en placering",
|
"add_a_location": "Tilføj en placering",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Tilføj partner",
|
"add_partner": "Tilføj partner",
|
||||||
"add_path": "Tilføj sti",
|
"add_path": "Tilføj sti",
|
||||||
"add_photos": "Tilføj billeder",
|
"add_photos": "Tilføj billeder",
|
||||||
"add_to": "Tilføj til...",
|
"add_to": "Tilføj til…",
|
||||||
"add_to_album": "Tilføj til album",
|
"add_to_album": "Tilføj til album",
|
||||||
"add_to_shared_album": "Tilføj til delt album",
|
"add_to_shared_album": "Tilføj til delt album",
|
||||||
"add_url": "Tilføj URL",
|
"add_url": "Tilføj URL",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Nulstil indstillingerne til standard",
|
"reset_settings_to_default": "Nulstil indstillingerne til standard",
|
||||||
"reset_settings_to_recent_saved": "Nulstil indstillinger til de senest gemte indstillinger",
|
"reset_settings_to_recent_saved": "Nulstil indstillinger til de senest gemte indstillinger",
|
||||||
"scanning_library": "Scanner bibliotek",
|
"scanning_library": "Scanner bibliotek",
|
||||||
"search_jobs": "søg opgaver ..",
|
"search_jobs": "Søg opgaver…",
|
||||||
"send_welcome_email": "Send velkomst-email",
|
"send_welcome_email": "Send velkomst-email",
|
||||||
"server_external_domain_settings": "Eksternt domæne",
|
"server_external_domain_settings": "Eksternt domæne",
|
||||||
"server_external_domain_settings_description": "Domæne til offentligt delte links, inklusiv http(s)://",
|
"server_external_domain_settings_description": "Domæne til offentligt delte links, inklusiv http(s)://",
|
||||||
@@ -360,9 +360,9 @@
|
|||||||
"admin_password": "Administratoradgangskode",
|
"admin_password": "Administratoradgangskode",
|
||||||
"administration": "Administration",
|
"administration": "Administration",
|
||||||
"advanced": "Avanceret",
|
"advanced": "Avanceret",
|
||||||
"age_months": "Alder {months, plural, one {# month} other {# months}}",
|
"age_months": "Alder {months, plural, one {# måned} other {# måneder}}",
|
||||||
"age_year_months": "Alder 1 år, {måneder, flertal, en {# måned} flere {# months}}",
|
"age_year_months": "Alder 1 år, {months, plural, one {# måned} other {# måneder}}",
|
||||||
"age_years": "{år, år, andre {Alder #}}",
|
"age_years": "{years, plural, other {Alder #}}",
|
||||||
"album_added": "Album tilføjet",
|
"album_added": "Album tilføjet",
|
||||||
"album_added_notification_setting_description": "Modtag en emailnotifikation når du bliver tilføjet til en delt album",
|
"album_added_notification_setting_description": "Modtag en emailnotifikation når du bliver tilføjet til en delt album",
|
||||||
"album_cover_updated": "Albumcover opdateret",
|
"album_cover_updated": "Albumcover opdateret",
|
||||||
@@ -402,33 +402,33 @@
|
|||||||
"archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
|
"archive_or_unarchive_photo": "Arkivér eller dearkivér billede",
|
||||||
"archive_size": "Arkiv størelse",
|
"archive_size": "Arkiv størelse",
|
||||||
"archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)",
|
"archive_size_description": "Konfigurer arkivstørrelsen for downloads (i GiB)",
|
||||||
"archived_count": "{antal, flertal, andet {Arkiveret #}}",
|
"archived_count": "{count, plural, other {Arkiveret #}}",
|
||||||
"are_these_the_same_person": "Er disse den samme person?",
|
"are_these_the_same_person": "Er disse den samme person?",
|
||||||
"are_you_sure_to_do_this": "Er du sikker på, at du vil gøre det her?",
|
"are_you_sure_to_do_this": "Er du sikker på, at du vil gøre det her?",
|
||||||
"asset_added_to_album": "Tilføjet til album",
|
"asset_added_to_album": "Tilføjet til album",
|
||||||
"asset_adding_to_album": "Tilføjer til album...",
|
"asset_adding_to_album": "Tilføjer til album…",
|
||||||
"asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret",
|
"asset_description_updated": "Mediefilsbeskrivelse er blevet opdateret",
|
||||||
"asset_filename_is_offline": "Mediefil {filename} er offline",
|
"asset_filename_is_offline": "Mediefil {filename} er offline",
|
||||||
"asset_has_unassigned_faces": "Aktivet har ikke-tildelte ansigter",
|
"asset_has_unassigned_faces": "Aktivet har ikke-tildelte ansigter",
|
||||||
"asset_hashing": "Hashing...",
|
"asset_hashing": "Hashing…",
|
||||||
"asset_offline": "Mediefil offline",
|
"asset_offline": "Mediefil offline",
|
||||||
"asset_offline_description": "Denne eksterne mediefil kan ikke længere findes på drevet. Kontakt venligst din Immich-administrator for hjælp.",
|
"asset_offline_description": "Denne eksterne mediefil kan ikke længere findes på drevet. Kontakt venligst din Immich-administrator for hjælp.",
|
||||||
"asset_skipped": "Sprunget over",
|
"asset_skipped": "Sprunget over",
|
||||||
"asset_skipped_in_trash": "I skraldespand",
|
"asset_skipped_in_trash": "I skraldespand",
|
||||||
"asset_uploaded": "Uploaded",
|
"asset_uploaded": "Uploadet",
|
||||||
"asset_uploading": "Uploader...",
|
"asset_uploading": "Uploader…",
|
||||||
"assets": "elementer",
|
"assets": "elementer",
|
||||||
"assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}",
|
"assets_added_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}}",
|
||||||
"assets_added_to_album_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til albummet",
|
"assets_added_to_album_count": "{count, plural, one {# mediefil} other {# mediefiler}} tilføjet til albummet",
|
||||||
"assets_added_to_name_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til {hasName, select, true {<b>{name}</b>} other {nyt album}}",
|
"assets_added_to_name_count": "Tilføjet {count, plural, one {# mediefil} other {# mediefiler}} til {hasName, select, true {<b>{name}</b>} other {nyt album}}",
|
||||||
"assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}",
|
"assets_count": "{count, plural, one {# mediefil} other {# mediefiler}}",
|
||||||
"assets_moved_to_trash_count": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til papirkurven",
|
"assets_moved_to_trash_count": "Flyttede {count, plural, one {# mediefil} other {# mediefiler}} til papirkurven",
|
||||||
"assets_permanently_deleted_count": "Slettet permanent {count, plural, one {# mediefil} other {# mediefiler}}",
|
"assets_permanently_deleted_count": "{count, plural, one {# mediefil} other {# mediefiler}} slettet permanent",
|
||||||
"assets_removed_count": "Fjernede {count, plural, one {# mediefil} other {# mediefiler}}",
|
"assets_removed_count": "Fjernede {count, plural, one {# mediefil} other {# mediefiler}}",
|
||||||
"assets_restore_confirmation": "Er du sikker på, at du vil gendanne alle dine aktiver i papirkurven? Du kan ikke fortryde denne handling! Bemærk, at offline mediefiler ikke kan gendannes på denne måde.",
|
"assets_restore_confirmation": "Er du sikker på, at du vil gendanne alle dine mediafiler i papirkurven? Du kan ikke fortryde denne handling! Bemærk, at offline mediefiler ikke kan gendannes på denne måde.",
|
||||||
"assets_restored_count": "Gendannet {count, plural, one {# mediefil} other {# mediefiler}}",
|
"assets_restored_count": "{count, plural, one {# mediefil} other {# mediefiler}} gendannet",
|
||||||
"assets_trashed_count": "Smidt {count, plural, one {# mediefil} other {# mediefiler}} i papirkurven",
|
"assets_trashed_count": "{count, plural, one {# mediefil} other {# mediefiler}} smidt i papirkurven",
|
||||||
"assets_were_part_of_album_count": "{count, plural, one {Asset was} other {Assets were}} er allerede en del af albummet",
|
"assets_were_part_of_album_count": "mediefil{count, plural, one {mediefil} other {mediefiler}} er allerede en del af albummet",
|
||||||
"authorized_devices": "Tilladte enheder",
|
"authorized_devices": "Tilladte enheder",
|
||||||
"back": "Tilbage",
|
"back": "Tilbage",
|
||||||
"back_close_deselect": "Tilbage, luk eller fravælg",
|
"back_close_deselect": "Tilbage, luk eller fravælg",
|
||||||
@@ -441,7 +441,7 @@
|
|||||||
"build_image": "Byggefil",
|
"build_image": "Byggefil",
|
||||||
"bulk_delete_duplicates_confirmation": "Er du sikker på, at du vil slette alle {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde den største fil i hver gruppe og slette alle dubletter. Denne handling kan ikke fortrydes!",
|
"bulk_delete_duplicates_confirmation": "Er du sikker på, at du vil slette alle {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde den største fil i hver gruppe og slette alle dubletter. Denne handling kan ikke fortrydes!",
|
||||||
"bulk_keep_duplicates_confirmation": "Er du sikker på, at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil løse alle dubletgrupper uden at slette noget.",
|
"bulk_keep_duplicates_confirmation": "Er du sikker på, at du vil beholde {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil løse alle dubletgrupper uden at slette noget.",
|
||||||
"bulk_trash_duplicates_confirmation": "Er du sikker på, at du vil masseslette {count, plural, one {# duplicate asset} other {# duplicate assets}}? Dette vil beholde det største aktiv i hver gruppe og smide alle andre dubletter.",
|
"bulk_trash_duplicates_confirmation": "Er du sikker på, at du vil masseslette {count, plural, one {# duplikeret objekt} other {# duplikerede objekter}}? Dette vil beholde det største objekt i hver gruppe og slette alle andre dubletter.",
|
||||||
"buy": "Køb Immich",
|
"buy": "Køb Immich",
|
||||||
"camera": "Kamera",
|
"camera": "Kamera",
|
||||||
"camera_brand": "Kameramærke",
|
"camera_brand": "Kameramærke",
|
||||||
@@ -595,7 +595,7 @@
|
|||||||
"editor_crop_tool_h2_rotation": "Rotation",
|
"editor_crop_tool_h2_rotation": "Rotation",
|
||||||
"email": "E-mail",
|
"email": "E-mail",
|
||||||
"empty_trash": "Tøm papirkurv",
|
"empty_trash": "Tøm papirkurv",
|
||||||
"empty_trash_confirmation": "Er du sikker på, at du vil tømme papirkurven? Dette vil fjerne alle aktiver i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!",
|
"empty_trash_confirmation": "Er du sikker på, at du vil tømme papirkurven? Dette vil fjerne alle objekter i papirkurven permanent fra Immich.\nDu kan ikke fortryde denne handling!",
|
||||||
"enable": "Aktivér",
|
"enable": "Aktivér",
|
||||||
"enabled": "Aktiveret",
|
"enabled": "Aktiveret",
|
||||||
"end_date": "Slutdato",
|
"end_date": "Slutdato",
|
||||||
@@ -608,7 +608,7 @@
|
|||||||
"cant_apply_changes": "Ændringerne kan ikke anvendes",
|
"cant_apply_changes": "Ændringerne kan ikke anvendes",
|
||||||
"cant_change_activity": "Kan ikke {enabled, select, true {disable} other {enable}} aktivitet",
|
"cant_change_activity": "Kan ikke {enabled, select, true {disable} other {enable}} aktivitet",
|
||||||
"cant_change_asset_favorite": "Kan ikke ændre favorit til aktiv",
|
"cant_change_asset_favorite": "Kan ikke ændre favorit til aktiv",
|
||||||
"cant_change_metadata_assets_count": "Kan ikke ændre metadata for {count, plural, one {# asset} other {# assets}}",
|
"cant_change_metadata_assets_count": "Kan ikke ændre metadata for {count, plural, one {# objekt} other {# objekter}}",
|
||||||
"cant_get_faces": "Kan ikke hente ansigter",
|
"cant_get_faces": "Kan ikke hente ansigter",
|
||||||
"cant_get_number_of_comments": "Kan ikke få antallet af kommentarer",
|
"cant_get_number_of_comments": "Kan ikke få antallet af kommentarer",
|
||||||
"cant_search_people": "Kan ikke søge efter folk",
|
"cant_search_people": "Kan ikke søge efter folk",
|
||||||
@@ -648,7 +648,7 @@
|
|||||||
"unable_to_add_partners": "Ikke i stand til at tilføje partnere",
|
"unable_to_add_partners": "Ikke i stand til at tilføje partnere",
|
||||||
"unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv",
|
"unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv",
|
||||||
"unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter",
|
"unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter",
|
||||||
"unable_to_archive_unarchive": "Ude af stand til at {arkiveret, vælg, sand {arkiv} andet {arkiv}}",
|
"unable_to_archive_unarchive": "Ude af stand til at {archived, select, true {arkivere} other {fjerne fra arkiv}}",
|
||||||
"unable_to_change_album_user_role": "Ikke i stand til at ændre albumbrugerens rolle",
|
"unable_to_change_album_user_role": "Ikke i stand til at ændre albumbrugerens rolle",
|
||||||
"unable_to_change_date": "Ikke i stand til at ændre dato",
|
"unable_to_change_date": "Ikke i stand til at ændre dato",
|
||||||
"unable_to_change_favorite": "Kan ikke ændre favorit for aktiv",
|
"unable_to_change_favorite": "Kan ikke ændre favorit for aktiv",
|
||||||
@@ -689,8 +689,8 @@
|
|||||||
"unable_to_log_out_device": "Enheden kunne ikke logges af",
|
"unable_to_log_out_device": "Enheden kunne ikke logges af",
|
||||||
"unable_to_login_with_oauth": "Kan ikke logge på med OAuth",
|
"unable_to_login_with_oauth": "Kan ikke logge på med OAuth",
|
||||||
"unable_to_play_video": "Ikke i stand til at afspille video",
|
"unable_to_play_video": "Ikke i stand til at afspille video",
|
||||||
"unable_to_reassign_assets_existing_person": "Kan ikke gentildele aktiver til {navn, vælg, null {en eksisterende person} anden {{name}}}",
|
"unable_to_reassign_assets_existing_person": "Kunne ikke tildele mediafiler til {name, select, null {en eksisterende person} other {{name}}}",
|
||||||
"unable_to_reassign_assets_new_person": "Kan ikke omfordele aktiver til en ny person",
|
"unable_to_reassign_assets_new_person": "Kan ikke omfordele objekter til en ny person",
|
||||||
"unable_to_refresh_user": "Ikke i stand til at genopfriske bruger",
|
"unable_to_refresh_user": "Ikke i stand til at genopfriske bruger",
|
||||||
"unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album",
|
"unable_to_remove_album_users": "Ikke i stand til at fjerne brugere fra album",
|
||||||
"unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle",
|
"unable_to_remove_api_key": "Kunne ikke fjerne API-nøgle",
|
||||||
@@ -720,7 +720,7 @@
|
|||||||
"unable_to_unlink_account": "Ikke i stand til at frakoble konto",
|
"unable_to_unlink_account": "Ikke i stand til at frakoble konto",
|
||||||
"unable_to_unlink_motion_video": "Kunne ikke fjerne linket til bevægelsesvideo",
|
"unable_to_unlink_motion_video": "Kunne ikke fjerne linket til bevægelsesvideo",
|
||||||
"unable_to_update_album_cover": "Albumomslaget kunne ikke opdateres",
|
"unable_to_update_album_cover": "Albumomslaget kunne ikke opdateres",
|
||||||
"unable_to_update_album_info": "Albumoplysningerne kunne ikke opdateres",
|
"unable_to_update_album_info": "Albumsoplysningerne kunne ikke opdateres",
|
||||||
"unable_to_update_library": "Ikke i stand til at opdatere bibliotek",
|
"unable_to_update_library": "Ikke i stand til at opdatere bibliotek",
|
||||||
"unable_to_update_location": "Ikke i stand til at opdatere sted",
|
"unable_to_update_location": "Ikke i stand til at opdatere sted",
|
||||||
"unable_to_update_settings": "Ikke i stand til at opdatere indstillinger",
|
"unable_to_update_settings": "Ikke i stand til at opdatere indstillinger",
|
||||||
@@ -766,8 +766,10 @@
|
|||||||
"go_to_folder": "Gå til mappe",
|
"go_to_folder": "Gå til mappe",
|
||||||
"go_to_search": "Gå til søgning",
|
"go_to_search": "Gå til søgning",
|
||||||
"group_albums_by": "Gruppér albummer efter...",
|
"group_albums_by": "Gruppér albummer efter...",
|
||||||
|
"group_country": "Gruppér efter land",
|
||||||
"group_no": "Ingen gruppering",
|
"group_no": "Ingen gruppering",
|
||||||
"group_owner": "Grupper efter ejer",
|
"group_owner": "Grupper efter ejer",
|
||||||
|
"group_places_by": "Gruppér steder efter...",
|
||||||
"group_year": "Grupper efter år",
|
"group_year": "Grupper efter år",
|
||||||
"has_quota": "Har kvote",
|
"has_quota": "Har kvote",
|
||||||
"hi_user": "Hej {name} ({email})",
|
"hi_user": "Hej {name} ({email})",
|
||||||
@@ -800,6 +802,7 @@
|
|||||||
"include_shared_albums": "Inkludér delte albummer",
|
"include_shared_albums": "Inkludér delte albummer",
|
||||||
"include_shared_partner_assets": "Inkludér delte partnermedier",
|
"include_shared_partner_assets": "Inkludér delte partnermedier",
|
||||||
"individual_share": "Individuel andel",
|
"individual_share": "Individuel andel",
|
||||||
|
"individual_shares": "Individuelle delinger",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Hver dag kl. 13",
|
"day_at_onepm": "Hver dag kl. 13",
|
||||||
@@ -809,7 +812,7 @@
|
|||||||
},
|
},
|
||||||
"invite_people": "Inviter personer",
|
"invite_people": "Inviter personer",
|
||||||
"invite_to_album": "Inviter til album",
|
"invite_to_album": "Inviter til album",
|
||||||
"items_count": "{count, plural, one {# genstand} other {# genstande}}",
|
"items_count": "{count, plural, one {# element} other {# elementer}}",
|
||||||
"jobs": "Opgaver",
|
"jobs": "Opgaver",
|
||||||
"keep": "Behold",
|
"keep": "Behold",
|
||||||
"keep_all": "Behold alle",
|
"keep_all": "Behold alle",
|
||||||
@@ -822,13 +825,14 @@
|
|||||||
"latest_version": "Seneste version",
|
"latest_version": "Seneste version",
|
||||||
"latitude": "Breddegrad",
|
"latitude": "Breddegrad",
|
||||||
"leave": "Forlad",
|
"leave": "Forlad",
|
||||||
|
"lens_model": "Objektivmodel",
|
||||||
"let_others_respond": "Lad andre svare",
|
"let_others_respond": "Lad andre svare",
|
||||||
"level": "Niveau",
|
"level": "Niveau",
|
||||||
"library": "Bibliotek",
|
"library": "Bibliotek",
|
||||||
"library_options": "Biblioteksindstillinger",
|
"library_options": "Biblioteksindstillinger",
|
||||||
"light": "Lys",
|
"light": "Lys",
|
||||||
"like_deleted": "Ligesom slettet",
|
"like_deleted": "Ligesom slettet",
|
||||||
"link_motion_video": "Link bevægelses video",
|
"link_motion_video": "Link bevægelsesvideo",
|
||||||
"link_options": "Link-indstillinger",
|
"link_options": "Link-indstillinger",
|
||||||
"link_to_oauth": "Link til OAuth",
|
"link_to_oauth": "Link til OAuth",
|
||||||
"linked_oauth_account": "Tilsluttet OAuth-konto",
|
"linked_oauth_account": "Tilsluttet OAuth-konto",
|
||||||
@@ -864,7 +868,7 @@
|
|||||||
"media_type": "Medietype",
|
"media_type": "Medietype",
|
||||||
"memories": "Minder",
|
"memories": "Minder",
|
||||||
"memories_setting_description": "Administrér hvad du ser i dine minder",
|
"memories_setting_description": "Administrér hvad du ser i dine minder",
|
||||||
"memory": "Hukommelse",
|
"memory": "Minde",
|
||||||
"memory_lane_title": "Minder {title}",
|
"memory_lane_title": "Minder {title}",
|
||||||
"menu": "Menu",
|
"menu": "Menu",
|
||||||
"merge": "Sammenflet",
|
"merge": "Sammenflet",
|
||||||
@@ -872,7 +876,7 @@
|
|||||||
"merge_people_limit": "Du kan kun flette op til 5 ansigter ad gangen",
|
"merge_people_limit": "Du kan kun flette op til 5 ansigter ad gangen",
|
||||||
"merge_people_prompt": "Vil du slå disse mennesker sammen? Denne handling er uigenkaldelig.",
|
"merge_people_prompt": "Vil du slå disse mennesker sammen? Denne handling er uigenkaldelig.",
|
||||||
"merge_people_successfully": "Personer sammenflettet med succes",
|
"merge_people_successfully": "Personer sammenflettet med succes",
|
||||||
"merged_people_count": "Slået sammen {count, plural, one {# person} other {# people}}",
|
"merged_people_count": "{count, plural, one {# person} other {# personer}} lagt sammen",
|
||||||
"minimize": "Minimér",
|
"minimize": "Minimér",
|
||||||
"minute": "Minut",
|
"minute": "Minut",
|
||||||
"missing": "Mangler",
|
"missing": "Mangler",
|
||||||
@@ -923,9 +927,9 @@
|
|||||||
"offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
|
"offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
|
||||||
"ok": "Ok",
|
"ok": "Ok",
|
||||||
"oldest_first": "Ældste først",
|
"oldest_first": "Ældste først",
|
||||||
"onboarding": "Onboarding",
|
"onboarding": "Introduktion",
|
||||||
"onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
|
"onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
|
||||||
"onboarding_theme_description": "Vælg et farvetema til din forekomst. Du kan ændre dette senere i dine indstillinger.",
|
"onboarding_theme_description": "Vælg et farvetema til din instans. Du kan ændre dette senere i dine indstillinger.",
|
||||||
"onboarding_welcome_description": "Lad os få din instans sat op med nogle almindelige indstillinger.",
|
"onboarding_welcome_description": "Lad os få din instans sat op med nogle almindelige indstillinger.",
|
||||||
"onboarding_welcome_user": "Velkommen, {user}",
|
"onboarding_welcome_user": "Velkommen, {user}",
|
||||||
"online": "Online",
|
"online": "Online",
|
||||||
@@ -940,7 +944,7 @@
|
|||||||
"other": "Andet",
|
"other": "Andet",
|
||||||
"other_devices": "Andre enheder",
|
"other_devices": "Andre enheder",
|
||||||
"other_variables": "Andre variable",
|
"other_variables": "Andre variable",
|
||||||
"owned": "Ejet",
|
"owned": "Egne",
|
||||||
"owner": "Ejer",
|
"owner": "Ejer",
|
||||||
"partner": "Partner",
|
"partner": "Partner",
|
||||||
"partner_can_access": "{partner} kan tilgå",
|
"partner_can_access": "{partner} kan tilgå",
|
||||||
@@ -973,7 +977,7 @@
|
|||||||
"permanently_delete_assets_count": "Slet permanent {count, plural, one {asset} other {assets}}",
|
"permanently_delete_assets_count": "Slet permanent {count, plural, one {asset} other {assets}}",
|
||||||
"permanently_delete_assets_prompt": "Er du sikker på, at du permanent vil slette {count, plural, one {dette aktiv?} other {disse <b>#</b> aktiver?}} Dette vil også fjerne {count, plural, one {det fra dets} other {dem fra deres}} album(er).",
|
"permanently_delete_assets_prompt": "Er du sikker på, at du permanent vil slette {count, plural, one {dette aktiv?} other {disse <b>#</b> aktiver?}} Dette vil også fjerne {count, plural, one {det fra dets} other {dem fra deres}} album(er).",
|
||||||
"permanently_deleted_asset": "Permanent slettet medie",
|
"permanently_deleted_asset": "Permanent slettet medie",
|
||||||
"permanently_deleted_assets_count": "Slettet permanent {count, plural, one {# aktiv} other {# aktiver}}",
|
"permanently_deleted_assets_count": "{count, plural, one {# aktiv} other {# aktiver}} permanent slettet",
|
||||||
"person": "Person",
|
"person": "Person",
|
||||||
"person_hidden": "{name}{hidden, select, true { (skjult)} other {}}",
|
"person_hidden": "{name}{hidden, select, true { (skjult)} other {}}",
|
||||||
"photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller også har du ikke nogen bruger at dele med.",
|
"photo_shared_all_users": "Det ser ud til, at du har delt dine billeder med alle brugere, eller også har du ikke nogen bruger at dele med.",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Vælg et sted",
|
"pick_a_location": "Vælg et sted",
|
||||||
"place": "Sted",
|
"place": "Sted",
|
||||||
"places": "Steder",
|
"places": "Steder",
|
||||||
|
"places_count": "{count, plural, one {{count, number} Sted} other {{count, number} Steder}}",
|
||||||
"play": "Afspil",
|
"play": "Afspil",
|
||||||
"play_memories": "Afspil minder",
|
"play_memories": "Afspil minder",
|
||||||
"play_motion_photo": "Afspil bevægelsesbillede",
|
"play_motion_photo": "Afspil bevægelsesbillede",
|
||||||
@@ -1018,11 +1023,11 @@
|
|||||||
"purchase_input_suggestion": "Har du en produktnøgle? Indtast nøglen nedenfor",
|
"purchase_input_suggestion": "Har du en produktnøgle? Indtast nøglen nedenfor",
|
||||||
"purchase_license_subtitle": "Køb Immich for at understøtte den fortsatte udvikling af tjenesten",
|
"purchase_license_subtitle": "Køb Immich for at understøtte den fortsatte udvikling af tjenesten",
|
||||||
"purchase_lifetime_description": "Livsvarigt køb",
|
"purchase_lifetime_description": "Livsvarigt køb",
|
||||||
"purchase_option_title": "KØBEMULIGHEDER",
|
"purchase_option_title": "KØBSMULIGHEDER",
|
||||||
"purchase_panel_info_1": "At bygge Immich tager meget tid og kræfter, og vi har fuldtidsingeniører, der arbejder på det for at gøre det så godt, som vi overhovedet kan. Vores mission er, at open source-software og etisk forretningspraksis bliver en bæredygtig indtægtskilde for udviklere og at skabe et privatlivsrespekterende økosystem med reelle alternativer til udnyttende cloud-tjenester.",
|
"purchase_panel_info_1": "At bygge Immich tager meget tid og kræfter, og vi har fuldtidsingeniører, der arbejder på det for at gøre det så godt, som vi overhovedet kan. Vores mission er, at open source-software og etisk forretningspraksis bliver en bæredygtig indtægtskilde for udviklere og at skabe et privatlivsrespekterende økosystem med reelle alternativer til udnyttende cloud-tjenester.",
|
||||||
"purchase_panel_info_2": "Da vi er forpligtet til ikke at tilføje betalingsvægge, vil dette køb ikke give dig yderligere funktioner i Immich. Vi er afhængige af, at brugere som dig støtter Immichs løbende udvikling.",
|
"purchase_panel_info_2": "Da vi er forpligtet til ikke at tilføje betalingsvægge, vil dette køb ikke give dig yderligere funktioner i Immich. Vi er afhængige af, at brugere som dig støtter Immichs løbende udvikling.",
|
||||||
"purchase_panel_title": "Støt projektet",
|
"purchase_panel_title": "Støt projektet",
|
||||||
"purchase_per_server": "Per server",
|
"purchase_per_server": "Pr. server",
|
||||||
"purchase_per_user": "Per bruger",
|
"purchase_per_user": "Per bruger",
|
||||||
"purchase_remove_product_key": "Fjern produktnøgle",
|
"purchase_remove_product_key": "Fjern produktnøgle",
|
||||||
"purchase_remove_product_key_prompt": "Er du sikker på, at du vil fjerne produktnøglen?",
|
"purchase_remove_product_key_prompt": "Er du sikker på, at du vil fjerne produktnøglen?",
|
||||||
@@ -1039,7 +1044,7 @@
|
|||||||
"reaction_options": "Reaktionsindstillinger",
|
"reaction_options": "Reaktionsindstillinger",
|
||||||
"read_changelog": "Læs ændringslog",
|
"read_changelog": "Læs ændringslog",
|
||||||
"reassign": "Gentildel",
|
"reassign": "Gentildel",
|
||||||
"reassigned_assets_to_existing_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til {name, select, null {en eksisterende person} other {{navne}}}",
|
"reassigned_assets_to_existing_person": "{count, plural, one {# mediefil} other {# mediefiler}} er blevet gentildelt til {name, select, null {en eksisterende person} other {{name}}}",
|
||||||
"reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person",
|
"reassigned_assets_to_new_person": "Gentildelt {count, plural, one {# aktiv} other {# aktiver}} til en ny person",
|
||||||
"reassing_hint": "Tildel valgte aktiver til en eksisterende person",
|
"reassing_hint": "Tildel valgte aktiver til en eksisterende person",
|
||||||
"recent": "For nylig",
|
"recent": "For nylig",
|
||||||
@@ -1084,7 +1089,7 @@
|
|||||||
"reset_people_visibility": "Nulstil personsynlighed",
|
"reset_people_visibility": "Nulstil personsynlighed",
|
||||||
"reset_to_default": "Nulstil til standard",
|
"reset_to_default": "Nulstil til standard",
|
||||||
"resolve_duplicates": "Løs dubletter",
|
"resolve_duplicates": "Løs dubletter",
|
||||||
"resolved_all_duplicates": "Løste alle dubletter",
|
"resolved_all_duplicates": "Alle dubletter løst",
|
||||||
"restore": "Gendan",
|
"restore": "Gendan",
|
||||||
"restore_all": "Gendan alle",
|
"restore_all": "Gendan alle",
|
||||||
"restore_user": "Gendan bruger",
|
"restore_user": "Gendan bruger",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Søg",
|
"search": "Søg",
|
||||||
"search_albums": "Søg i albummer",
|
"search_albums": "Søg i albummer",
|
||||||
"search_by_context": "Søg efter kontekst",
|
"search_by_context": "Søg efter kontekst",
|
||||||
|
"search_by_description": "Søg efter beskrivelse",
|
||||||
|
"search_by_description_example": "Vandredag i Paris",
|
||||||
"search_by_filename": "Søg efter filnavn eller filtypenavn",
|
"search_by_filename": "Søg efter filnavn eller filtypenavn",
|
||||||
"search_by_filename_example": "dvs. IMG_1234.JPG eller PNG",
|
"search_by_filename_example": "dvs. IMG_1234.JPG eller PNG",
|
||||||
"search_camera_make": "Søg efter kameraproducent...",
|
"search_camera_make": "Søg efter kameraproducent...",
|
||||||
"search_camera_model": "Søg efter kameramodel...",
|
"search_camera_model": "Søg efter kameramodel...",
|
||||||
"search_city": "Søg efter by...",
|
"search_city": "Søg efter by...",
|
||||||
"search_country": "Søg efter land...",
|
"search_country": "Søg efter land...",
|
||||||
|
"search_for": "Søg efter",
|
||||||
"search_for_existing_person": "Søg efter eksisterende person",
|
"search_for_existing_person": "Søg efter eksisterende person",
|
||||||
"search_no_people": "Ingen personer",
|
"search_no_people": "Ingen personer",
|
||||||
"search_no_people_named": "Ingen personer med navnet \"{name}\"",
|
"search_no_people_named": "Ingen personer med navnet \"{name}\"",
|
||||||
@@ -1141,7 +1149,7 @@
|
|||||||
"select_photos": "Vælg billeder",
|
"select_photos": "Vælg billeder",
|
||||||
"select_trash_all": "Vælg smid alle ud",
|
"select_trash_all": "Vælg smid alle ud",
|
||||||
"selected": "Valgt",
|
"selected": "Valgt",
|
||||||
"selected_count": "{count, plural, other {# valgt}}",
|
"selected_count": "{count, plural, one {# valgt} other {# valgte}}",
|
||||||
"send_message": "Send besked",
|
"send_message": "Send besked",
|
||||||
"send_welcome_email": "Send velkomstemail",
|
"send_welcome_email": "Send velkomstemail",
|
||||||
"server_offline": "Server Offline",
|
"server_offline": "Server Offline",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Billeder fra {partner}",
|
"shared_from_partner": "Billeder fra {partner}",
|
||||||
"shared_link_options": "Muligheder for delt link",
|
"shared_link_options": "Muligheder for delt link",
|
||||||
"shared_links": "Delte links",
|
"shared_links": "Delte links",
|
||||||
|
"shared_links_description": "Del billeder og videoer med et link",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# delte billeder & videoer.}}",
|
||||||
"shared_with_partner": "Delt med {partner}",
|
"shared_with_partner": "Delt med {partner}",
|
||||||
"sharing": "Delte",
|
"sharing": "Delte",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Vis personindstillinger",
|
"show_person_options": "Vis personindstillinger",
|
||||||
"show_progress_bar": "Vis statuslinje",
|
"show_progress_bar": "Vis statuslinje",
|
||||||
"show_search_options": "Vis søgeindstillinger",
|
"show_search_options": "Vis søgeindstillinger",
|
||||||
|
"show_shared_links": "Vis delte links",
|
||||||
"show_slideshow_transition": "Vis overgang til diasshow",
|
"show_slideshow_transition": "Vis overgang til diasshow",
|
||||||
"show_supporter_badge": "Supportermærke",
|
"show_supporter_badge": "Supportermærke",
|
||||||
"show_supporter_badge_description": "Vis et supportermærke",
|
"show_supporter_badge_description": "Vis et supportermærke",
|
||||||
@@ -1264,9 +1274,9 @@
|
|||||||
"total_usage": "Samlet forbrug",
|
"total_usage": "Samlet forbrug",
|
||||||
"trash": "Papirkurv",
|
"trash": "Papirkurv",
|
||||||
"trash_all": "Smid alle ud",
|
"trash_all": "Smid alle ud",
|
||||||
"trash_count": "Skrald {count, number}",
|
"trash_count": "Slet {count, number}",
|
||||||
"trash_delete_asset": "Papirkurv/slet aktiv",
|
"trash_delete_asset": "Papirkurv/slet aktiv",
|
||||||
"trash_no_results_message": "Udsmidte billeder og videoer vil kunne findes her.",
|
"trash_no_results_message": "Billeder og videoer markeret til sletning vil blive vist her.",
|
||||||
"trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.",
|
"trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.",
|
||||||
"type": "Type",
|
"type": "Type",
|
||||||
"unarchive": "Afakivér",
|
"unarchive": "Afakivér",
|
||||||
@@ -1274,6 +1284,7 @@
|
|||||||
"unfavorite": "Fjern favorit",
|
"unfavorite": "Fjern favorit",
|
||||||
"unhide_person": "Hold op med at gemme person væk",
|
"unhide_person": "Hold op med at gemme person væk",
|
||||||
"unknown": "Ukendt",
|
"unknown": "Ukendt",
|
||||||
|
"unknown_country": "Ukendt land",
|
||||||
"unknown_year": "Ukendt år",
|
"unknown_year": "Ukendt år",
|
||||||
"unlimited": "Ubegrænset",
|
"unlimited": "Ubegrænset",
|
||||||
"unlink_motion_video": "Fjern link til bevægelsesvideo",
|
"unlink_motion_video": "Fjern link til bevægelsesvideo",
|
||||||
@@ -1287,7 +1298,7 @@
|
|||||||
"unselect_all_duplicates": "Fjern markeringen af alle dubletter",
|
"unselect_all_duplicates": "Fjern markeringen af alle dubletter",
|
||||||
"unstack": "Fjern fra stak",
|
"unstack": "Fjern fra stak",
|
||||||
"unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}",
|
"unstacked_assets_count": "Ikke-stablet {count, plural, one {# aktiv} other {# aktiver}}",
|
||||||
"untracked_files": "Usporede filer",
|
"untracked_files": "Ikke overvågede filer",
|
||||||
"untracked_files_decription": "Disse filer bliver ikke sporet af applikationen. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller efterladt på grund af en fejl",
|
"untracked_files_decription": "Disse filer bliver ikke sporet af applikationen. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller efterladt på grund af en fejl",
|
||||||
"up_next": "Næste",
|
"up_next": "Næste",
|
||||||
"updated_password": "Opdaterede adgangskode",
|
"updated_password": "Opdaterede adgangskode",
|
||||||
@@ -1298,7 +1309,7 @@
|
|||||||
"upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}",
|
"upload_skipped_duplicates": "Sprang over {count, plural, one {# duplet aktiv} other {# duplikerede aktiver}}",
|
||||||
"upload_status_duplicates": "Dubletter",
|
"upload_status_duplicates": "Dubletter",
|
||||||
"upload_status_errors": "Fejl",
|
"upload_status_errors": "Fejl",
|
||||||
"upload_status_uploaded": "Uploaded",
|
"upload_status_uploaded": "Uploadet",
|
||||||
"upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.",
|
"upload_success": "Upload gennemført. Opdater siden for at se nye uploadaktiver.",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
"usage": "Forbrug",
|
"usage": "Forbrug",
|
||||||
|
|||||||
37
i18n/de.json
37
i18n/de.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Partner hinzufügen",
|
"add_partner": "Partner hinzufügen",
|
||||||
"add_path": "Pfad hinzufügen",
|
"add_path": "Pfad hinzufügen",
|
||||||
"add_photos": "Fotos hinzufügen",
|
"add_photos": "Fotos hinzufügen",
|
||||||
"add_to": "Hinzufügen zu ...",
|
"add_to": "Hinzufügen zu …",
|
||||||
"add_to_album": "Zu Album hinzufügen",
|
"add_to_album": "Zu Album hinzufügen",
|
||||||
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
|
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
|
||||||
"add_url": "URL hinzufügen",
|
"add_url": "URL hinzufügen",
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
"add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.",
|
"add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.",
|
||||||
"asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.",
|
"asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.",
|
||||||
"authentication_settings": "Authentifizierungseinstellungen",
|
"authentication_settings": "Authentifizierungseinstellungen",
|
||||||
"authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten",
|
"authentication_settings_description": "Passwort-, OAuth- und sonstige Authentifizierungseinstellungen verwalten",
|
||||||
"authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
|
"authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
|
||||||
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
|
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
|
||||||
"background_task_job": "Hintergrundaufgaben",
|
"background_task_job": "Hintergrundaufgaben",
|
||||||
@@ -187,7 +187,7 @@
|
|||||||
"oauth_issuer_url": "Aussteller-URL",
|
"oauth_issuer_url": "Aussteller-URL",
|
||||||
"oauth_mobile_redirect_uri": "Mobile Umleitungs-URI",
|
"oauth_mobile_redirect_uri": "Mobile Umleitungs-URI",
|
||||||
"oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI überschreiben",
|
"oauth_mobile_redirect_uri_override": "Mobile Umleitungs-URI überschreiben",
|
||||||
"oauth_mobile_redirect_uri_override_description": "Einschalten, wenn der OAuth-Provider keine mobile URI wie '{callback}' erlaubt",
|
"oauth_mobile_redirect_uri_override_description": "Einschalten, wenn der OAuth-Anbieter keine mobile URI wie '{callback}' erlaubt",
|
||||||
"oauth_profile_signing_algorithm": "Algorithmus zur Profilsignierung",
|
"oauth_profile_signing_algorithm": "Algorithmus zur Profilsignierung",
|
||||||
"oauth_profile_signing_algorithm_description": "Dieser Algorithmus wird für die Signatur des Benutzerprofils verwendet.",
|
"oauth_profile_signing_algorithm_description": "Dieser Algorithmus wird für die Signatur des Benutzerprofils verwendet.",
|
||||||
"oauth_scope": "Umfang",
|
"oauth_scope": "Umfang",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Einstellungen auf Standard zurücksetzen",
|
"reset_settings_to_default": "Einstellungen auf Standard zurücksetzen",
|
||||||
"reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen",
|
"reset_settings_to_recent_saved": "Einstellungen auf die zuletzt gespeicherten Einstellungen zurücksetzen",
|
||||||
"scanning_library": "Bibliothek scannen",
|
"scanning_library": "Bibliothek scannen",
|
||||||
"search_jobs": "Aufgaben suchen...",
|
"search_jobs": "Suchaufgaben…",
|
||||||
"send_welcome_email": "Begrüssungsmail senden",
|
"send_welcome_email": "Begrüssungsmail senden",
|
||||||
"server_external_domain_settings": "Externe Domain",
|
"server_external_domain_settings": "Externe Domain",
|
||||||
"server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://",
|
"server_external_domain_settings_description": "Domäne für öffentlich freigegebene Links, einschließlich http(s)://",
|
||||||
@@ -290,7 +290,7 @@
|
|||||||
"transcoding_constant_rate_factor_description": "Videoqualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
"transcoding_constant_rate_factor_description": "Videoqualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
||||||
"transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen",
|
"transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen",
|
||||||
"transcoding_encoding_options": "Kodierungsoptionen",
|
"transcoding_encoding_options": "Kodierungsoptionen",
|
||||||
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für kodierte Videos",
|
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für die kodierten Videos",
|
||||||
"transcoding_hardware_acceleration": "Hardware-Beschleunigung",
|
"transcoding_hardware_acceleration": "Hardware-Beschleunigung",
|
||||||
"transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität",
|
"transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität",
|
||||||
"transcoding_hardware_decoding": "Hardware-Dekodierung",
|
"transcoding_hardware_decoding": "Hardware-Dekodierung",
|
||||||
@@ -313,7 +313,7 @@
|
|||||||
"transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.",
|
"transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.",
|
||||||
"transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format",
|
"transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format",
|
||||||
"transcoding_settings": "Einstellungen für die Videotranskodierung",
|
"transcoding_settings": "Einstellungen für die Videotranskodierung",
|
||||||
"transcoding_settings_description": "Verwalten welche Videos transkodiert werden und wie diese verarbeitet werden",
|
"transcoding_settings_description": "Verwalten welche Videos transkodiert und wie diese verarbeitet werden",
|
||||||
"transcoding_target_resolution": "Ziel-Auflösung",
|
"transcoding_target_resolution": "Ziel-Auflösung",
|
||||||
"transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.",
|
"transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.",
|
||||||
"transcoding_temporal_aq": "Temporäre AQ",
|
"transcoding_temporal_aq": "Temporäre AQ",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Ist das dieselbe Person?",
|
"are_these_the_same_person": "Ist das dieselbe Person?",
|
||||||
"are_you_sure_to_do_this": "Bist du sicher, dass du das tun willst?",
|
"are_you_sure_to_do_this": "Bist du sicher, dass du das tun willst?",
|
||||||
"asset_added_to_album": "Zum Album hinzugefügt",
|
"asset_added_to_album": "Zum Album hinzugefügt",
|
||||||
"asset_adding_to_album": "Hinzufügen zum Album...",
|
"asset_adding_to_album": "Hinzufügen zum Album…",
|
||||||
"asset_description_updated": "Die Beschreibung der Datei wurde aktualisiert",
|
"asset_description_updated": "Die Beschreibung der Datei wurde aktualisiert",
|
||||||
"asset_filename_is_offline": "Datei {filename} ist offline",
|
"asset_filename_is_offline": "Datei {filename} ist offline",
|
||||||
"asset_has_unassigned_faces": "Datei hat nicht zugewiesene Gesichter",
|
"asset_has_unassigned_faces": "Datei hat nicht zugewiesene Gesichter",
|
||||||
"asset_hashing": "Berechnung des Hashwerts...",
|
"asset_hashing": "Berechne Prüfsumme…",
|
||||||
"asset_offline": "Datei offline",
|
"asset_offline": "Datei offline",
|
||||||
"asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.",
|
"asset_offline_description": "Diese externe Datei ist nicht mehr auf dem Datenträger vorhanden. Bitte wende dich an deinen Immich-Administrator, um Hilfe zu erhalten.",
|
||||||
"asset_skipped": "Übersprungen",
|
"asset_skipped": "Übersprungen",
|
||||||
"asset_skipped_in_trash": "Im Papierkorb",
|
"asset_skipped_in_trash": "Im Papierkorb",
|
||||||
"asset_uploaded": "Hochgeladen",
|
"asset_uploaded": "Hochgeladen",
|
||||||
"asset_uploading": "Hochladen...",
|
"asset_uploading": "Hochladen…",
|
||||||
"assets": "Dateien",
|
"assets": "Dateien",
|
||||||
"assets_added_count": "{count, plural, one {# Datei} other {# Dateien}} hinzugefügt",
|
"assets_added_count": "{count, plural, one {# Datei} other {# Dateien}} hinzugefügt",
|
||||||
"assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefügt",
|
"assets_added_to_album_count": "{count, plural, one {# Datei} other {# Dateien}} zum Album hinzugefügt",
|
||||||
@@ -687,7 +687,7 @@
|
|||||||
"unable_to_load_liked_status": "Gewünschter Status kann nicht geladen werden",
|
"unable_to_load_liked_status": "Gewünschter Status kann nicht geladen werden",
|
||||||
"unable_to_log_out_all_devices": "Konnte nicht von allen Geräten abmelden",
|
"unable_to_log_out_all_devices": "Konnte nicht von allen Geräten abmelden",
|
||||||
"unable_to_log_out_device": "Konnte nicht vom Gerät abmelden",
|
"unable_to_log_out_device": "Konnte nicht vom Gerät abmelden",
|
||||||
"unable_to_login_with_oauth": "Konnte nicht mit OAuth anmelden",
|
"unable_to_login_with_oauth": "Anmeldung mit OAuth nicht möglich",
|
||||||
"unable_to_play_video": "Das Video kann nicht wiedergegeben werden",
|
"unable_to_play_video": "Das Video kann nicht wiedergegeben werden",
|
||||||
"unable_to_reassign_assets_existing_person": "Kann Dateien nicht {name, select, null {einer vorhandenen Person} other {{name}}} zuweisen",
|
"unable_to_reassign_assets_existing_person": "Kann Dateien nicht {name, select, null {einer vorhandenen Person} other {{name}}} zuweisen",
|
||||||
"unable_to_reassign_assets_new_person": "Dateien konnten nicht einer neuen Person zugeordnet werden",
|
"unable_to_reassign_assets_new_person": "Dateien konnten nicht einer neuen Person zugeordnet werden",
|
||||||
@@ -766,8 +766,10 @@
|
|||||||
"go_to_folder": "Gehe zu Ordner",
|
"go_to_folder": "Gehe zu Ordner",
|
||||||
"go_to_search": "Zur Suche gehen",
|
"go_to_search": "Zur Suche gehen",
|
||||||
"group_albums_by": "Alben gruppieren nach...",
|
"group_albums_by": "Alben gruppieren nach...",
|
||||||
|
"group_country": "Nach Land gruppieren",
|
||||||
"group_no": "Keine Gruppierung",
|
"group_no": "Keine Gruppierung",
|
||||||
"group_owner": "Gruppierung nach Besitzer",
|
"group_owner": "Gruppierung nach Besitzer",
|
||||||
|
"group_places_by": "Orte gruppieren nach...",
|
||||||
"group_year": "Gruppierung nach Jahr",
|
"group_year": "Gruppierung nach Jahr",
|
||||||
"has_quota": "Kontingent",
|
"has_quota": "Kontingent",
|
||||||
"hi_user": "Hallo {name} ({email})",
|
"hi_user": "Hallo {name} ({email})",
|
||||||
@@ -800,6 +802,7 @@
|
|||||||
"include_shared_albums": "Freigegebene Alben einbeziehen",
|
"include_shared_albums": "Freigegebene Alben einbeziehen",
|
||||||
"include_shared_partner_assets": "Geteilte Partner-Dateien mit einbeziehen",
|
"include_shared_partner_assets": "Geteilte Partner-Dateien mit einbeziehen",
|
||||||
"individual_share": "Individuelle Freigabe",
|
"individual_share": "Individuelle Freigabe",
|
||||||
|
"individual_shares": "Individuelles Teilen",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Täglich um 13:00 Uhr",
|
"day_at_onepm": "Täglich um 13:00 Uhr",
|
||||||
@@ -822,6 +825,7 @@
|
|||||||
"latest_version": "Aktuellste Version",
|
"latest_version": "Aktuellste Version",
|
||||||
"latitude": "Breitengrad",
|
"latitude": "Breitengrad",
|
||||||
"leave": "Verlassen",
|
"leave": "Verlassen",
|
||||||
|
"lens_model": "Objektivmodell",
|
||||||
"let_others_respond": "Antworten zulassen",
|
"let_others_respond": "Antworten zulassen",
|
||||||
"level": "Level",
|
"level": "Level",
|
||||||
"library": "Bibliothek",
|
"library": "Bibliothek",
|
||||||
@@ -830,7 +834,7 @@
|
|||||||
"like_deleted": "Like gelöscht",
|
"like_deleted": "Like gelöscht",
|
||||||
"link_motion_video": "Bewegungsvideo verknüpfen",
|
"link_motion_video": "Bewegungsvideo verknüpfen",
|
||||||
"link_options": "Link-Optionen",
|
"link_options": "Link-Optionen",
|
||||||
"link_to_oauth": "Link zu OAuth",
|
"link_to_oauth": "Mit OAuth verknüpfen",
|
||||||
"linked_oauth_account": "Verknüpftes OAuth-Konto",
|
"linked_oauth_account": "Verknüpftes OAuth-Konto",
|
||||||
"list": "Liste",
|
"list": "Liste",
|
||||||
"loading": "Laden",
|
"loading": "Laden",
|
||||||
@@ -855,7 +859,7 @@
|
|||||||
"manage_your_account": "Dein Konto verwalten",
|
"manage_your_account": "Dein Konto verwalten",
|
||||||
"manage_your_api_keys": "Deine API-Schlüssel verwalten",
|
"manage_your_api_keys": "Deine API-Schlüssel verwalten",
|
||||||
"manage_your_devices": "Deine eingeloggten Geräte verwalten",
|
"manage_your_devices": "Deine eingeloggten Geräte verwalten",
|
||||||
"manage_your_oauth_connection": "Deine OAuth-Verbindung verwalten",
|
"manage_your_oauth_connection": "Deine OAuth-Verknüpfung verwalten",
|
||||||
"map": "Karte",
|
"map": "Karte",
|
||||||
"map_marker_for_images": "Kartenmarkierung für Bilder, die in {city}, {country} aufgenommen wurden",
|
"map_marker_for_images": "Kartenmarkierung für Bilder, die in {city}, {country} aufgenommen wurden",
|
||||||
"map_marker_with_image": "Kartenmarkierung mit Bild",
|
"map_marker_with_image": "Kartenmarkierung mit Bild",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Wähle einen Ort",
|
"pick_a_location": "Wähle einen Ort",
|
||||||
"place": "Ort",
|
"place": "Ort",
|
||||||
"places": "Orte",
|
"places": "Orte",
|
||||||
|
"places_count": "{count, plural, one {{count, number} Ort} other {{count, number} Orte}}",
|
||||||
"play": "Abspielen",
|
"play": "Abspielen",
|
||||||
"play_memories": "Erinnerungen abspielen",
|
"play_memories": "Erinnerungen abspielen",
|
||||||
"play_motion_photo": "Bewegte Bilder abspielen",
|
"play_motion_photo": "Bewegte Bilder abspielen",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Suche",
|
"search": "Suche",
|
||||||
"search_albums": "Album suchen",
|
"search_albums": "Album suchen",
|
||||||
"search_by_context": "Suche nach Kontext",
|
"search_by_context": "Suche nach Kontext",
|
||||||
|
"search_by_description": "Nach Beschreibung suchen",
|
||||||
|
"search_by_description_example": "Wandern in Sapa",
|
||||||
"search_by_filename": "Suche nach Dateiname oder -erweiterung",
|
"search_by_filename": "Suche nach Dateiname oder -erweiterung",
|
||||||
"search_by_filename_example": "z.B. IMG_1234.JPG oder PNG",
|
"search_by_filename_example": "z.B. IMG_1234.JPG oder PNG",
|
||||||
"search_camera_make": "Suche nach Kameramarke...",
|
"search_camera_make": "Suche nach Kameramarke...",
|
||||||
"search_camera_model": "Suche nach Kameramodell...",
|
"search_camera_model": "Suche nach Kameramodell...",
|
||||||
"search_city": "Suche nach Stadt...",
|
"search_city": "Suche nach Stadt...",
|
||||||
"search_country": "Suche nach Land...",
|
"search_country": "Suche nach Land...",
|
||||||
|
"search_for": "Suche nach",
|
||||||
"search_for_existing_person": "Suche nach vorhandener Person",
|
"search_for_existing_person": "Suche nach vorhandener Person",
|
||||||
"search_no_people": "Keine Personen",
|
"search_no_people": "Keine Personen",
|
||||||
"search_no_people_named": "Keine Person mit dem Namen \"{name}\"",
|
"search_no_people_named": "Keine Person mit dem Namen \"{name}\"",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Fotos von {partner}",
|
"shared_from_partner": "Fotos von {partner}",
|
||||||
"shared_link_options": "Optionen für geteilten Link",
|
"shared_link_options": "Optionen für geteilten Link",
|
||||||
"shared_links": "Geteilte Links",
|
"shared_links": "Geteilte Links",
|
||||||
|
"shared_links_description": "Teile Fotos und Videos mit einem Link",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, one {# geteiltes Foto oder Video.} other {# geteilte Fotos & Videos.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, one {# geteiltes Foto oder Video.} other {# geteilte Fotos & Videos.}}",
|
||||||
"shared_with_partner": "Geteilt mit {partner}",
|
"shared_with_partner": "Geteilt mit {partner}",
|
||||||
"sharing": "Geteiltes",
|
"sharing": "Geteiltes",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Personen-Optionen anzeigen",
|
"show_person_options": "Personen-Optionen anzeigen",
|
||||||
"show_progress_bar": "Fortschrittsbalken anzeigen",
|
"show_progress_bar": "Fortschrittsbalken anzeigen",
|
||||||
"show_search_options": "Suchoptionen anzeigen",
|
"show_search_options": "Suchoptionen anzeigen",
|
||||||
|
"show_shared_links": "Zeige geteilte Links",
|
||||||
"show_slideshow_transition": "Slideshow-Übergang anzeigen",
|
"show_slideshow_transition": "Slideshow-Übergang anzeigen",
|
||||||
"show_supporter_badge": "Unterstützerabzeichen",
|
"show_supporter_badge": "Unterstützerabzeichen",
|
||||||
"show_supporter_badge_description": "Zeige Unterstützerabzeichen",
|
"show_supporter_badge_description": "Zeige Unterstützerabzeichen",
|
||||||
@@ -1274,11 +1284,12 @@
|
|||||||
"unfavorite": "Entfavorisieren",
|
"unfavorite": "Entfavorisieren",
|
||||||
"unhide_person": "Person einblenden",
|
"unhide_person": "Person einblenden",
|
||||||
"unknown": "Unbekannt",
|
"unknown": "Unbekannt",
|
||||||
|
"unknown_country": "Unbekanntes Land",
|
||||||
"unknown_year": "Unbekanntes Jahr",
|
"unknown_year": "Unbekanntes Jahr",
|
||||||
"unlimited": "Unlimitiert",
|
"unlimited": "Unlimitiert",
|
||||||
"unlink_motion_video": "Verknüpfung zum Bewegungsvideo aufheben",
|
"unlink_motion_video": "Verknüpfung zum Bewegungsvideo aufheben",
|
||||||
"unlink_oauth": "OAuth entfernen",
|
"unlink_oauth": "OAuth entfernen",
|
||||||
"unlinked_oauth_account": "Nicht verknüpftes OAuth-Konto",
|
"unlinked_oauth_account": "OAuth-Konto entfernt",
|
||||||
"unnamed_album": "Unbenanntes Album",
|
"unnamed_album": "Unbenanntes Album",
|
||||||
"unnamed_album_delete_confirmation": "Bist du sicher, dass du dieses Album löschen willst?",
|
"unnamed_album_delete_confirmation": "Bist du sicher, dass du dieses Album löschen willst?",
|
||||||
"unnamed_share": "Unbenannte Freigabe",
|
"unnamed_share": "Unbenannte Freigabe",
|
||||||
|
|||||||
33
i18n/el.json
33
i18n/el.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Προσθήκη συνεργάτη",
|
"add_partner": "Προσθήκη συνεργάτη",
|
||||||
"add_path": "Προσθήκη διαδρομής",
|
"add_path": "Προσθήκη διαδρομής",
|
||||||
"add_photos": "Προσθήκη φωτογραφιών",
|
"add_photos": "Προσθήκη φωτογραφιών",
|
||||||
"add_to": "Προσθήκη σε...",
|
"add_to": "Προσθήκη σε…",
|
||||||
"add_to_album": "Προσθήκη σε άλμπουμ",
|
"add_to_album": "Προσθήκη σε άλμπουμ",
|
||||||
"add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ",
|
"add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ",
|
||||||
"add_url": "Προσθήκη Συνδέσμου",
|
"add_url": "Προσθήκη Συνδέσμου",
|
||||||
@@ -114,24 +114,24 @@
|
|||||||
"machine_learning_facial_recognition": "Αναγνώριση Προσώπου",
|
"machine_learning_facial_recognition": "Αναγνώριση Προσώπου",
|
||||||
"machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων που υπάρχουν σε εικόνες",
|
"machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων που υπάρχουν σε εικόνες",
|
||||||
"machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου",
|
"machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου",
|
||||||
"machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.",
|
"machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να επανεκτελέσετε την εργασία \"Ανίχνευση Προσώπου\" για όλες τις εικόνες μετά την αλλαγή μοντέλου.",
|
||||||
"machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου",
|
"machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου",
|
||||||
"machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Περιήγησης.",
|
"machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Περιήγησης.",
|
||||||
"machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης",
|
"machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης",
|
||||||
"machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
|
"machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
|
||||||
"machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης",
|
"machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης",
|
||||||
"machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτή μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.",
|
"machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτής της τιμής μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.",
|
||||||
"machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης",
|
"machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης",
|
||||||
"machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
|
"machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.",
|
||||||
"machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα",
|
"machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα",
|
||||||
"machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.",
|
"machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.",
|
||||||
"machine_learning_settings": "Ρυθμίσεις Μηχανικής Εκμάθησης",
|
"machine_learning_settings": "Ρυθμίσεις Μηχανικής Μάθησης",
|
||||||
"machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής εκμάθησης",
|
"machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής μάθησης",
|
||||||
"machine_learning_smart_search": "Έξυπνη Αναζήτηση",
|
"machine_learning_smart_search": "Έξυπνη Αναζήτηση",
|
||||||
"machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP",
|
"machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP",
|
||||||
"machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης",
|
"machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης",
|
||||||
"machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.",
|
"machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.",
|
||||||
"machine_learning_url_description": "Η διεύθυνση URL του διακομιστή μηχανικής εκμάθησης. Αν παρέχονται περισσότερες από μία διευθύνσεις URL, τότε, κάθε διακομιστής θα προσπαθήσει να συνδεθεί διαδοχικά, από την πρώτη μέχρι την τελευταία, έως ότου απαντήσει επιτυχώς.",
|
"machine_learning_url_description": "Η διεύθυνση URL του διακομιστή μηχανικής μάθησης. Αν παρέχονται περισσότερες από μία διευθύνσεις, τότε θα γίνει προσπάθεια σύνδεσης σε κάθε μια διαδοχικά από την πρώτη μέχρι την τελευταία, έως ότου κάποια να είναι επιτυχής.",
|
||||||
"manage_concurrency": "Διαχείριση ταυτόχρονη εκτέλεσης",
|
"manage_concurrency": "Διαχείριση ταυτόχρονη εκτέλεσης",
|
||||||
"manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής",
|
"manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής",
|
||||||
"map_dark_style": "Σκούρο Θέμα",
|
"map_dark_style": "Σκούρο Θέμα",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Επαναφορά προεπιλεγμένων ρυθμίσεων",
|
"reset_settings_to_default": "Επαναφορά προεπιλεγμένων ρυθμίσεων",
|
||||||
"reset_settings_to_recent_saved": "Επαναφορά ρυθμίσεων στις πρόσφατα αποθηκευμένες ρυθμίσεις",
|
"reset_settings_to_recent_saved": "Επαναφορά ρυθμίσεων στις πρόσφατα αποθηκευμένες ρυθμίσεις",
|
||||||
"scanning_library": "Σάρωση βιβλιοθήκης",
|
"scanning_library": "Σάρωση βιβλιοθήκης",
|
||||||
"search_jobs": "Αναζήτηση εργασιών...",
|
"search_jobs": "Αναζήτηση εργασιών…",
|
||||||
"send_welcome_email": "Αποστολή email καλωσορίσματος",
|
"send_welcome_email": "Αποστολή email καλωσορίσματος",
|
||||||
"server_external_domain_settings": "Εξωτερική διεύθυνση τομέα",
|
"server_external_domain_settings": "Εξωτερική διεύθυνση τομέα",
|
||||||
"server_external_domain_settings_description": "Διεύθυνση τομέα για δημόσιους κοινούς συνδέσμους, περιλαμβανομένου του http(s)://",
|
"server_external_domain_settings_description": "Διεύθυνση τομέα για δημόσιους κοινούς συνδέσμους, περιλαμβανομένου του http(s)://",
|
||||||
@@ -232,7 +232,7 @@
|
|||||||
"sidecar_job": "Μεταδεδομένα συνοδευτικού αρχείου",
|
"sidecar_job": "Μεταδεδομένα συνοδευτικού αρχείου",
|
||||||
"sidecar_job_description": "Ανακάλυψη ή συγχρονισμός των μεταδεδομένων του συνοδευτικού αρχείου από το σύστημα αρχείων",
|
"sidecar_job_description": "Ανακάλυψη ή συγχρονισμός των μεταδεδομένων του συνοδευτικού αρχείου από το σύστημα αρχείων",
|
||||||
"slideshow_duration_description": "Αριθμός δευτερολέπτων για την εμφάνιση κάθε εικόνας",
|
"slideshow_duration_description": "Αριθμός δευτερολέπτων για την εμφάνιση κάθε εικόνας",
|
||||||
"smart_search_job_description": "Εκτέλεση της μηχανικής εκμάθησης, σε αρχεία, για την υποστήριξη της έξυπνης αναζήτησης",
|
"smart_search_job_description": "Εκτέλεση της μηχανικής μάθησης, σε αρχεία, για την υποστήριξη της έξυπνης αναζήτησης",
|
||||||
"storage_template_date_time_description": "Η χρονική σήμανση της δημιουργίας του αρχείου, χρησιμοποιείται για τις πληροφορίες ημερομηνίας και ώρας",
|
"storage_template_date_time_description": "Η χρονική σήμανση της δημιουργίας του αρχείου, χρησιμοποιείται για τις πληροφορίες ημερομηνίας και ώρας",
|
||||||
"storage_template_date_time_sample": "Χρόνος δείγματος {date}",
|
"storage_template_date_time_sample": "Χρόνος δείγματος {date}",
|
||||||
"storage_template_enable_description": "Ενεργοποίηση του μηχανισμού των προτύπων αποθήκευσης",
|
"storage_template_enable_description": "Ενεργοποίηση του μηχανισμού των προτύπων αποθήκευσης",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Είναι το ίδιο άτομο;",
|
"are_these_the_same_person": "Είναι το ίδιο άτομο;",
|
||||||
"are_you_sure_to_do_this": "Είστε σίγουροι ότι θέλετε να το κάνετε αυτό;",
|
"are_you_sure_to_do_this": "Είστε σίγουροι ότι θέλετε να το κάνετε αυτό;",
|
||||||
"asset_added_to_album": "Προστέθηκε στο άλμπουμ",
|
"asset_added_to_album": "Προστέθηκε στο άλμπουμ",
|
||||||
"asset_adding_to_album": "Προστίθεται στο άλμπουμ...",
|
"asset_adding_to_album": "Προστίθεται στο άλμπουμ…",
|
||||||
"asset_description_updated": "Η περιγραφή του αντικειμένου έχει ενημερωθεί",
|
"asset_description_updated": "Η περιγραφή του αντικειμένου έχει ενημερωθεί",
|
||||||
"asset_filename_is_offline": "Το αντικείμενο {filename} είναι εκτός σύνδεσης",
|
"asset_filename_is_offline": "Το αντικείμενο {filename} είναι εκτός σύνδεσης",
|
||||||
"asset_has_unassigned_faces": "Το αντικείμενο έχει μη ανατεθειμένα πρόσωπα",
|
"asset_has_unassigned_faces": "Το αντικείμενο έχει μη ανατεθειμένα πρόσωπα",
|
||||||
"asset_hashing": "Δημιουργία κατακερματισμού...",
|
"asset_hashing": "Δημιουργία κατακερματισμού…",
|
||||||
"asset_offline": "Αντικείμενο εκτός σύνδεσης",
|
"asset_offline": "Αντικείμενο εκτός σύνδεσης",
|
||||||
"asset_offline_description": "Αυτό το εξωτερικό αντικείμενο δεν βρέθηκε πλέον στον δίσκο. Παρακαλώ επικοινωνήστε με τον διαχειριστή του Immich για βοήθεια.",
|
"asset_offline_description": "Αυτό το εξωτερικό αντικείμενο δεν βρέθηκε πλέον στον δίσκο. Παρακαλώ επικοινωνήστε με τον διαχειριστή του Immich για βοήθεια.",
|
||||||
"asset_skipped": "Παραλείφθηκε",
|
"asset_skipped": "Παραλείφθηκε",
|
||||||
"asset_skipped_in_trash": "Στον κάδο απορριμμάτων",
|
"asset_skipped_in_trash": "Στον κάδο απορριμμάτων",
|
||||||
"asset_uploaded": "Ανεβάστηκε",
|
"asset_uploaded": "Ανεβάστηκε",
|
||||||
"asset_uploading": "Ανεβάζεται...",
|
"asset_uploading": "Ανεβάζεται…",
|
||||||
"assets": "Αντικείμενα",
|
"assets": "Αντικείμενα",
|
||||||
"assets_added_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}}",
|
"assets_added_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}}",
|
||||||
"assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ",
|
"assets_added_to_album_count": "Προστέθηκε {count, plural, one {# αρχείο} other {# αρχεία}} στο άλμπουμ",
|
||||||
@@ -766,8 +766,10 @@
|
|||||||
"go_to_folder": "Μετάβαση στο φάκελο",
|
"go_to_folder": "Μετάβαση στο φάκελο",
|
||||||
"go_to_search": "Πηγαίνετε στην αναζήτηση",
|
"go_to_search": "Πηγαίνετε στην αναζήτηση",
|
||||||
"group_albums_by": "Ομαδοποίηση άλμπουμ κατά...",
|
"group_albums_by": "Ομαδοποίηση άλμπουμ κατά...",
|
||||||
|
"group_country": "Ομαδοποίηση κατά χώρα",
|
||||||
"group_no": "Καμία ομοδοποίηση",
|
"group_no": "Καμία ομοδοποίηση",
|
||||||
"group_owner": "Ομαδοποίηση κατά ιδιοκτήτη",
|
"group_owner": "Ομαδοποίηση κατά ιδιοκτήτη",
|
||||||
|
"group_places_by": "Ομοδοποίηση τοποθεσιών κατά...",
|
||||||
"group_year": "Ομαδοποίηση κατά έτος",
|
"group_year": "Ομαδοποίηση κατά έτος",
|
||||||
"has_quota": "Έχει ποσόστωση",
|
"has_quota": "Έχει ποσόστωση",
|
||||||
"hi_user": "Γειά σου {name} {email}",
|
"hi_user": "Γειά σου {name} {email}",
|
||||||
@@ -800,6 +802,7 @@
|
|||||||
"include_shared_albums": "Συμπερίληψη διαμοιρασμένων άλμπουμ",
|
"include_shared_albums": "Συμπερίληψη διαμοιρασμένων άλμπουμ",
|
||||||
"include_shared_partner_assets": "Συμπερίληψη των στοιχείων των συνεργατών που έχουν κοινοποιηθεί",
|
"include_shared_partner_assets": "Συμπερίληψη των στοιχείων των συνεργατών που έχουν κοινοποιηθεί",
|
||||||
"individual_share": "Μεμονωμένος διαμοιρασμός",
|
"individual_share": "Μεμονωμένος διαμοιρασμός",
|
||||||
|
"individual_shares": "Μεμονωμένες κοινοποιήσεις",
|
||||||
"info": "Πληροφορίες",
|
"info": "Πληροφορίες",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Κάθε μέρα στη 1μμ",
|
"day_at_onepm": "Κάθε μέρα στη 1μμ",
|
||||||
@@ -822,6 +825,7 @@
|
|||||||
"latest_version": "Τελευταία Έκδοση",
|
"latest_version": "Τελευταία Έκδοση",
|
||||||
"latitude": "Γεωγραφικό πλάτος",
|
"latitude": "Γεωγραφικό πλάτος",
|
||||||
"leave": "Εγκατάλειψη",
|
"leave": "Εγκατάλειψη",
|
||||||
|
"lens_model": "Μοντέλο φακού",
|
||||||
"let_others_respond": "Επέτρεψε σε άλλους να απαντήσουν",
|
"let_others_respond": "Επέτρεψε σε άλλους να απαντήσουν",
|
||||||
"level": "Επίπεδο",
|
"level": "Επίπεδο",
|
||||||
"library": "Βιβλιοθήκη",
|
"library": "Βιβλιοθήκη",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Επιλέξτε μια τοποθεσία",
|
"pick_a_location": "Επιλέξτε μια τοποθεσία",
|
||||||
"place": "Τοποθεσία",
|
"place": "Τοποθεσία",
|
||||||
"places": "Τοποθεσίες",
|
"places": "Τοποθεσίες",
|
||||||
|
"places_count": "{count, plural, one {{count} Τοποθεσία} other {{count} Τοποθεσίες}}",
|
||||||
"play": "Αναπαραγωγή",
|
"play": "Αναπαραγωγή",
|
||||||
"play_memories": "Αναπαραγωγή αναμνήσεων",
|
"play_memories": "Αναπαραγωγή αναμνήσεων",
|
||||||
"play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας",
|
"play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Αναζήτηση",
|
"search": "Αναζήτηση",
|
||||||
"search_albums": "Αναζήτηση άλμπουμ",
|
"search_albums": "Αναζήτηση άλμπουμ",
|
||||||
"search_by_context": "Αναζήτηση με βάση το πλαίσιο",
|
"search_by_context": "Αναζήτηση με βάση το πλαίσιο",
|
||||||
|
"search_by_description": "Αναζήτηση με βάση την περιγραφή",
|
||||||
|
"search_by_description_example": "Ημερήσια πεζοπορία στο Πάπιγκο",
|
||||||
"search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου",
|
"search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου",
|
||||||
"search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG",
|
"search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG",
|
||||||
"search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...",
|
"search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...",
|
||||||
"search_camera_model": "Αναζήτηση μοντέλου κάμερας...",
|
"search_camera_model": "Αναζήτηση μοντέλου κάμερας...",
|
||||||
"search_city": "Αναζήτηση πόλης...",
|
"search_city": "Αναζήτηση πόλης...",
|
||||||
"search_country": "Αναζήτηση χώρας...",
|
"search_country": "Αναζήτηση χώρας...",
|
||||||
|
"search_for": "Αναζήτηση για",
|
||||||
"search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου",
|
"search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου",
|
||||||
"search_no_people": "Κανένα άτομο",
|
"search_no_people": "Κανένα άτομο",
|
||||||
"search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"",
|
"search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Φωτογραφίες από {partner}",
|
"shared_from_partner": "Φωτογραφίες από {partner}",
|
||||||
"shared_link_options": "Επιλογές κοινόχρηστου συνδέσμου",
|
"shared_link_options": "Επιλογές κοινόχρηστου συνδέσμου",
|
||||||
"shared_links": "Κοινόχρηστοι σύνδεσμοι",
|
"shared_links": "Κοινόχρηστοι σύνδεσμοι",
|
||||||
|
"shared_links_description": "Μοιραστείτε φωτογραφίες και βίντεο με σύνδεσμο",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}",
|
||||||
"shared_with_partner": "Σε κοινή χρήση με {partner}",
|
"shared_with_partner": "Σε κοινή χρήση με {partner}",
|
||||||
"sharing": "Κοινοποίηση",
|
"sharing": "Κοινοποίηση",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Εμφάνιση επιλογών ατόμου",
|
"show_person_options": "Εμφάνιση επιλογών ατόμου",
|
||||||
"show_progress_bar": "Εμφάνιση γραμμής προόδου",
|
"show_progress_bar": "Εμφάνιση γραμμής προόδου",
|
||||||
"show_search_options": "Εμφάνιση επιλογών αναζήτησης",
|
"show_search_options": "Εμφάνιση επιλογών αναζήτησης",
|
||||||
|
"show_shared_links": "Εμφάνιση κοινών συνδέσμων",
|
||||||
"show_slideshow_transition": "Εμφάνιση μετάβασης παρουσίασης",
|
"show_slideshow_transition": "Εμφάνιση μετάβασης παρουσίασης",
|
||||||
"show_supporter_badge": "Σήμα υποστηρικτή",
|
"show_supporter_badge": "Σήμα υποστηρικτή",
|
||||||
"show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή",
|
"show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή",
|
||||||
@@ -1274,6 +1284,7 @@
|
|||||||
"unfavorite": "Αποεπιλογή από τα αγαπημένα",
|
"unfavorite": "Αποεπιλογή από τα αγαπημένα",
|
||||||
"unhide_person": "Αναίρεση απόκρυψης ατόμου",
|
"unhide_person": "Αναίρεση απόκρυψης ατόμου",
|
||||||
"unknown": "Άγνωστο",
|
"unknown": "Άγνωστο",
|
||||||
|
"unknown_country": "Άγνωστη Χώρα",
|
||||||
"unknown_year": "Άγνωστο Έτος",
|
"unknown_year": "Άγνωστο Έτος",
|
||||||
"unlimited": "Απεριόριστο",
|
"unlimited": "Απεριόριστο",
|
||||||
"unlink_motion_video": "Αποσυνδέστε το βίντεο κίνησης",
|
"unlink_motion_video": "Αποσυνδέστε το βίντεο κίνησης",
|
||||||
|
|||||||
48
i18n/en.json
48
i18n/en.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Add partner",
|
"add_partner": "Add partner",
|
||||||
"add_path": "Add path",
|
"add_path": "Add path",
|
||||||
"add_photos": "Add photos",
|
"add_photos": "Add photos",
|
||||||
"add_to": "Add to...",
|
"add_to": "Add to…",
|
||||||
"add_to_album": "Add to album",
|
"add_to_album": "Add to album",
|
||||||
"add_to_shared_album": "Add to shared album",
|
"add_to_shared_album": "Add to shared album",
|
||||||
"add_url": "Add URL",
|
"add_url": "Add URL",
|
||||||
@@ -96,7 +96,7 @@
|
|||||||
"library_scanning_enable_description": "Enable periodic library scanning",
|
"library_scanning_enable_description": "Enable periodic library scanning",
|
||||||
"library_settings": "External Library",
|
"library_settings": "External Library",
|
||||||
"library_settings_description": "Manage external library settings",
|
"library_settings_description": "Manage external library settings",
|
||||||
"library_tasks_description": "Perform library tasks",
|
"library_tasks_description": "Scan external libraries for new and/or changed assets",
|
||||||
"library_watching_enable_description": "Watch external libraries for file changes",
|
"library_watching_enable_description": "Watch external libraries for file changes",
|
||||||
"library_watching_settings": "Library watching (EXPERIMENTAL)",
|
"library_watching_settings": "Library watching (EXPERIMENTAL)",
|
||||||
"library_watching_settings_description": "Automatically watch for changed files",
|
"library_watching_settings_description": "Automatically watch for changed files",
|
||||||
@@ -131,7 +131,7 @@
|
|||||||
"machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
|
"machine_learning_smart_search_description": "Search for images semantically using CLIP embeddings",
|
||||||
"machine_learning_smart_search_enabled": "Enable smart search",
|
"machine_learning_smart_search_enabled": "Enable smart search",
|
||||||
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
"machine_learning_smart_search_enabled_description": "If disabled, images will not be encoded for smart search.",
|
||||||
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last.",
|
"machine_learning_url_description": "The URL of the machine learning server. If more than one URL is provided, each server will be attempted one-at-a-time until one responds successfully, in order from first to last. Servers that don't respond will be temporarily ignored until they come back online.",
|
||||||
"manage_concurrency": "Manage Concurrency",
|
"manage_concurrency": "Manage Concurrency",
|
||||||
"manage_log_settings": "Manage log settings",
|
"manage_log_settings": "Manage log settings",
|
||||||
"map_dark_style": "Dark style",
|
"map_dark_style": "Dark style",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Reset settings to default",
|
"reset_settings_to_default": "Reset settings to default",
|
||||||
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
|
"reset_settings_to_recent_saved": "Reset settings to the recent saved settings",
|
||||||
"scanning_library": "Scanning library",
|
"scanning_library": "Scanning library",
|
||||||
"search_jobs": "Search jobs...",
|
"search_jobs": "Search jobs…",
|
||||||
"send_welcome_email": "Send welcome email",
|
"send_welcome_email": "Send welcome email",
|
||||||
"server_external_domain_settings": "External domain",
|
"server_external_domain_settings": "External domain",
|
||||||
"server_external_domain_settings_description": "Domain for public shared links, including http(s)://",
|
"server_external_domain_settings_description": "Domain for public shared links, including http(s)://",
|
||||||
@@ -336,6 +336,7 @@
|
|||||||
"untracked_files": "Untracked Files",
|
"untracked_files": "Untracked Files",
|
||||||
"untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
|
"untracked_files_description": "These files are not tracked by the application. They can be the results of failed moves, interrupted uploads, or left behind due to a bug",
|
||||||
"user_cleanup_job": "User cleanup",
|
"user_cleanup_job": "User cleanup",
|
||||||
|
"cleanup": "Cleanup",
|
||||||
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
"user_delete_delay": "<b>{user}</b>'s account and assets will be scheduled for permanent deletion in {delay, plural, one {# day} other {# days}}.",
|
||||||
"user_delete_delay_settings": "Delete delay",
|
"user_delete_delay_settings": "Delete delay",
|
||||||
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
|
"user_delete_delay_settings_description": "Number of days after removal to permanently delete a user's account and assets. The user deletion job runs at midnight to check for users that are ready for deletion. Changes to this setting will be evaluated at the next execution.",
|
||||||
@@ -352,6 +353,8 @@
|
|||||||
"version_check_enabled_description": "Enable version check",
|
"version_check_enabled_description": "Enable version check",
|
||||||
"version_check_implications": "The version check feature relies on periodic communication with github.com",
|
"version_check_implications": "The version check feature relies on periodic communication with github.com",
|
||||||
"version_check_settings": "Version Check",
|
"version_check_settings": "Version Check",
|
||||||
|
"memory_cleanup_job": "Memory cleanup",
|
||||||
|
"memory_generate_job": "Memory generation",
|
||||||
"version_check_settings_description": "Enable/disable the new version notification",
|
"version_check_settings_description": "Enable/disable the new version notification",
|
||||||
"video_conversion_job": "Transcode videos",
|
"video_conversion_job": "Transcode videos",
|
||||||
"video_conversion_job_description": "Transcode videos for wider compatibility with browsers and devices"
|
"video_conversion_job_description": "Transcode videos for wider compatibility with browsers and devices"
|
||||||
@@ -391,6 +394,7 @@
|
|||||||
"allow_edits": "Allow edits",
|
"allow_edits": "Allow edits",
|
||||||
"allow_public_user_to_download": "Allow public user to download",
|
"allow_public_user_to_download": "Allow public user to download",
|
||||||
"allow_public_user_to_upload": "Allow public user to upload",
|
"allow_public_user_to_upload": "Allow public user to upload",
|
||||||
|
"alt_text_qr_code": "QR code image",
|
||||||
"anti_clockwise": "Anti-clockwise",
|
"anti_clockwise": "Anti-clockwise",
|
||||||
"api_key": "API Key",
|
"api_key": "API Key",
|
||||||
"api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.",
|
"api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.",
|
||||||
@@ -406,17 +410,17 @@
|
|||||||
"are_these_the_same_person": "Are these the same person?",
|
"are_these_the_same_person": "Are these the same person?",
|
||||||
"are_you_sure_to_do_this": "Are you sure you want to do this?",
|
"are_you_sure_to_do_this": "Are you sure you want to do this?",
|
||||||
"asset_added_to_album": "Added to album",
|
"asset_added_to_album": "Added to album",
|
||||||
"asset_adding_to_album": "Adding to album...",
|
"asset_adding_to_album": "Adding to album…",
|
||||||
"asset_description_updated": "Asset description has been updated",
|
"asset_description_updated": "Asset description has been updated",
|
||||||
"asset_filename_is_offline": "Asset {filename} is offline",
|
"asset_filename_is_offline": "Asset {filename} is offline",
|
||||||
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
"asset_has_unassigned_faces": "Asset has unassigned faces",
|
||||||
"asset_hashing": "Hashing...",
|
"asset_hashing": "Hashing…",
|
||||||
"asset_offline": "Asset Offline",
|
"asset_offline": "Asset Offline",
|
||||||
"asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.",
|
"asset_offline_description": "This external asset is no longer found on disk. Please contact your Immich administrator for help.",
|
||||||
"asset_skipped": "Skipped",
|
"asset_skipped": "Skipped",
|
||||||
"asset_skipped_in_trash": "In trash",
|
"asset_skipped_in_trash": "In trash",
|
||||||
"asset_uploaded": "Uploaded",
|
"asset_uploaded": "Uploaded",
|
||||||
"asset_uploading": "Uploading...",
|
"asset_uploading": "Uploading…",
|
||||||
"assets": "Assets",
|
"assets": "Assets",
|
||||||
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
"assets_added_count": "Added {count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
"assets_added_to_album_count": "Added {count, plural, one {# asset} other {# assets}} to the album",
|
||||||
@@ -481,6 +485,7 @@
|
|||||||
"comments_are_disabled": "Comments are disabled",
|
"comments_are_disabled": "Comments are disabled",
|
||||||
"confirm": "Confirm",
|
"confirm": "Confirm",
|
||||||
"confirm_admin_password": "Confirm Admin Password",
|
"confirm_admin_password": "Confirm Admin Password",
|
||||||
|
"confirm_delete_face": "Are you sure you want to delete {name} face from the asset?",
|
||||||
"confirm_delete_shared_link": "Are you sure you want to delete this shared link?",
|
"confirm_delete_shared_link": "Are you sure you want to delete this shared link?",
|
||||||
"confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?",
|
"confirm_keep_this_delete_others": "All other assets in the stack will be deleted except for this asset. Are you sure you want to continue?",
|
||||||
"confirm_password": "Confirm password",
|
"confirm_password": "Confirm password",
|
||||||
@@ -523,16 +528,17 @@
|
|||||||
"date_range": "Date range",
|
"date_range": "Date range",
|
||||||
"day": "Day",
|
"day": "Day",
|
||||||
"deduplicate_all": "Deduplicate All",
|
"deduplicate_all": "Deduplicate All",
|
||||||
"deduplication_info": "Deduplication Info",
|
|
||||||
"deduplication_info_description": "To automatically preselect assets and remove duplicates in bulk, we look at:",
|
|
||||||
"deduplication_criteria_1": "Image size in bytes",
|
"deduplication_criteria_1": "Image size in bytes",
|
||||||
"deduplication_criteria_2": "Count of EXIF data",
|
"deduplication_criteria_2": "Count of EXIF data",
|
||||||
|
"deduplication_info": "Deduplication Info",
|
||||||
|
"deduplication_info_description": "To automatically preselect assets and remove duplicates in bulk, we look at:",
|
||||||
"default_locale": "Default Locale",
|
"default_locale": "Default Locale",
|
||||||
"default_locale_description": "Format dates and numbers based on your browser locale",
|
"default_locale_description": "Format dates and numbers based on your browser locale",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"delete_album": "Delete album",
|
"delete_album": "Delete album",
|
||||||
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
"delete_api_key_prompt": "Are you sure you want to delete this API key?",
|
||||||
"delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?",
|
"delete_duplicates_confirmation": "Are you sure you want to permanently delete these duplicates?",
|
||||||
|
"delete_face": "Delete face",
|
||||||
"delete_key": "Delete key",
|
"delete_key": "Delete key",
|
||||||
"delete_library": "Delete Library",
|
"delete_library": "Delete Library",
|
||||||
"delete_link": "Delete link",
|
"delete_link": "Delete link",
|
||||||
@@ -600,6 +606,7 @@
|
|||||||
"enabled": "Enabled",
|
"enabled": "Enabled",
|
||||||
"end_date": "End date",
|
"end_date": "End date",
|
||||||
"error": "Error",
|
"error": "Error",
|
||||||
|
"error_delete_face": "Error deleting face from asset",
|
||||||
"error_loading_image": "Error loading image",
|
"error_loading_image": "Error loading image",
|
||||||
"error_title": "Error - Something went wrong",
|
"error_title": "Error - Something went wrong",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -763,11 +770,13 @@
|
|||||||
"get_help": "Get Help",
|
"get_help": "Get Help",
|
||||||
"getting_started": "Getting Started",
|
"getting_started": "Getting Started",
|
||||||
"go_back": "Go back",
|
"go_back": "Go back",
|
||||||
"go_to_search": "Go to search",
|
|
||||||
"go_to_folder": "Go to folder",
|
"go_to_folder": "Go to folder",
|
||||||
|
"go_to_search": "Go to search",
|
||||||
"group_albums_by": "Group albums by...",
|
"group_albums_by": "Group albums by...",
|
||||||
|
"group_country": "Group by country",
|
||||||
"group_no": "No grouping",
|
"group_no": "No grouping",
|
||||||
"group_owner": "Group by owner",
|
"group_owner": "Group by owner",
|
||||||
|
"group_places_by": "Group places by...",
|
||||||
"group_year": "Group by year",
|
"group_year": "Group by year",
|
||||||
"has_quota": "Has quota",
|
"has_quota": "Has quota",
|
||||||
"hi_user": "Hi {name} ({email})",
|
"hi_user": "Hi {name} ({email})",
|
||||||
@@ -800,6 +809,7 @@
|
|||||||
"include_shared_albums": "Include shared albums",
|
"include_shared_albums": "Include shared albums",
|
||||||
"include_shared_partner_assets": "Include shared partner assets",
|
"include_shared_partner_assets": "Include shared partner assets",
|
||||||
"individual_share": "Individual share",
|
"individual_share": "Individual share",
|
||||||
|
"individual_shares": "Individual shares",
|
||||||
"info": "Info",
|
"info": "Info",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Every day at 1pm",
|
"day_at_onepm": "Every day at 1pm",
|
||||||
@@ -881,6 +891,7 @@
|
|||||||
"month": "Month",
|
"month": "Month",
|
||||||
"more": "More",
|
"more": "More",
|
||||||
"moved_to_trash": "Moved to trash",
|
"moved_to_trash": "Moved to trash",
|
||||||
|
"mute_memories": "Mute Memories",
|
||||||
"my_albums": "My albums",
|
"my_albums": "My albums",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_or_nickname": "Name or nickname",
|
"name_or_nickname": "Name or nickname",
|
||||||
@@ -985,6 +996,7 @@
|
|||||||
"pick_a_location": "Pick a location",
|
"pick_a_location": "Pick a location",
|
||||||
"place": "Place",
|
"place": "Place",
|
||||||
"places": "Places",
|
"places": "Places",
|
||||||
|
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||||
"play": "Play",
|
"play": "Play",
|
||||||
"play_memories": "Play memories",
|
"play_memories": "Play memories",
|
||||||
"play_motion_photo": "Play Motion Photo",
|
"play_motion_photo": "Play Motion Photo",
|
||||||
@@ -1069,6 +1081,8 @@
|
|||||||
"remove_url": "Remove URL",
|
"remove_url": "Remove URL",
|
||||||
"remove_user": "Remove user",
|
"remove_user": "Remove user",
|
||||||
"removed_api_key": "Removed API Key: {name}",
|
"removed_api_key": "Removed API Key: {name}",
|
||||||
|
"removed_memory": "Removed memory",
|
||||||
|
"removed_photo_from_memory": "Removed photo from memory",
|
||||||
"removed_from_archive": "Removed from archive",
|
"removed_from_archive": "Removed from archive",
|
||||||
"removed_from_favorites": "Removed from favorites",
|
"removed_from_favorites": "Removed from favorites",
|
||||||
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
"removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites",
|
||||||
@@ -1103,11 +1117,14 @@
|
|||||||
"say_something": "Say something",
|
"say_something": "Say something",
|
||||||
"scan_all_libraries": "Scan All Libraries",
|
"scan_all_libraries": "Scan All Libraries",
|
||||||
"scan_library": "Scan",
|
"scan_library": "Scan",
|
||||||
|
"rescan": "Rescan",
|
||||||
"scan_settings": "Scan Settings",
|
"scan_settings": "Scan Settings",
|
||||||
"scanning_for_album": "Scanning for album...",
|
"scanning_for_album": "Scanning for album...",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_albums": "Search albums",
|
"search_albums": "Search albums",
|
||||||
"search_by_context": "Search by context",
|
"search_by_context": "Search by context",
|
||||||
|
"search_by_description": "Search by description",
|
||||||
|
"search_by_description_example": "Hiking day in Sapa",
|
||||||
"search_by_filename": "Search by file name or extension",
|
"search_by_filename": "Search by file name or extension",
|
||||||
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
||||||
"search_camera_make": "Search camera make...",
|
"search_camera_make": "Search camera make...",
|
||||||
@@ -1127,6 +1144,7 @@
|
|||||||
"search_timezone": "Search timezone...",
|
"search_timezone": "Search timezone...",
|
||||||
"search_type": "Search type",
|
"search_type": "Search type",
|
||||||
"search_your_photos": "Search your photos",
|
"search_your_photos": "Search your photos",
|
||||||
|
"search_rating": "Search by rating...",
|
||||||
"searching_locales": "Searching locales...",
|
"searching_locales": "Searching locales...",
|
||||||
"second": "Second",
|
"second": "Second",
|
||||||
"see_all_people": "See all people",
|
"see_all_people": "See all people",
|
||||||
@@ -1152,8 +1170,8 @@
|
|||||||
"server_version": "Server Version",
|
"server_version": "Server Version",
|
||||||
"set": "Set",
|
"set": "Set",
|
||||||
"set_as_album_cover": "Set as album cover",
|
"set_as_album_cover": "Set as album cover",
|
||||||
"set_as_profile_picture": "Set as profile picture",
|
|
||||||
"set_as_featured_photo": "Set as featured photo",
|
"set_as_featured_photo": "Set as featured photo",
|
||||||
|
"set_as_profile_picture": "Set as profile picture",
|
||||||
"set_date_of_birth": "Set date of birth",
|
"set_date_of_birth": "Set date of birth",
|
||||||
"set_profile_picture": "Set profile picture",
|
"set_profile_picture": "Set profile picture",
|
||||||
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
"set_slideshow_to_fullscreen": "Set Slideshow to fullscreen",
|
||||||
@@ -1167,6 +1185,7 @@
|
|||||||
"shared_from_partner": "Photos from {partner}",
|
"shared_from_partner": "Photos from {partner}",
|
||||||
"shared_link_options": "Shared link options",
|
"shared_link_options": "Shared link options",
|
||||||
"shared_links": "Shared links",
|
"shared_links": "Shared links",
|
||||||
|
"shared_links_description": "Share photos and videos with a link",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
||||||
"shared_with_partner": "Shared with {partner}",
|
"shared_with_partner": "Shared with {partner}",
|
||||||
"sharing": "Sharing",
|
"sharing": "Sharing",
|
||||||
@@ -1189,6 +1208,7 @@
|
|||||||
"show_person_options": "Show person options",
|
"show_person_options": "Show person options",
|
||||||
"show_progress_bar": "Show Progress Bar",
|
"show_progress_bar": "Show Progress Bar",
|
||||||
"show_search_options": "Show search options",
|
"show_search_options": "Show search options",
|
||||||
|
"show_shared_links": "Show shared links",
|
||||||
"show_slideshow_transition": "Show slideshow transition",
|
"show_slideshow_transition": "Show slideshow transition",
|
||||||
"show_supporter_badge": "Supporter badge",
|
"show_supporter_badge": "Supporter badge",
|
||||||
"show_supporter_badge_description": "Show a supporter badge",
|
"show_supporter_badge_description": "Show a supporter badge",
|
||||||
@@ -1242,6 +1262,7 @@
|
|||||||
"tag_created": "Created tag: {tag}",
|
"tag_created": "Created tag: {tag}",
|
||||||
"tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
|
"tag_feature_description": "Browsing photos and videos grouped by logical tag topics",
|
||||||
"tag_not_found_question": "Cannot find a tag? <link>Create a new tag.</link>",
|
"tag_not_found_question": "Cannot find a tag? <link>Create a new tag.</link>",
|
||||||
|
"tag_people": "Tag People",
|
||||||
"tag_updated": "Updated tag: {tag}",
|
"tag_updated": "Updated tag: {tag}",
|
||||||
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
|
"tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}",
|
||||||
"tags": "Tags",
|
"tags": "Tags",
|
||||||
@@ -1276,6 +1297,7 @@
|
|||||||
"unfavorite": "Unfavorite",
|
"unfavorite": "Unfavorite",
|
||||||
"unhide_person": "Unhide person",
|
"unhide_person": "Unhide person",
|
||||||
"unknown": "Unknown",
|
"unknown": "Unknown",
|
||||||
|
"unknown_country": "Unknown Country",
|
||||||
"unknown_year": "Unknown Year",
|
"unknown_year": "Unknown Year",
|
||||||
"unlimited": "Unlimited",
|
"unlimited": "Unlimited",
|
||||||
"unlink_motion_video": "Unlink motion video",
|
"unlink_motion_video": "Unlink motion video",
|
||||||
@@ -1284,6 +1306,7 @@
|
|||||||
"unnamed_album": "Unnamed Album",
|
"unnamed_album": "Unnamed Album",
|
||||||
"unnamed_album_delete_confirmation": "Are you sure you want to delete this album?",
|
"unnamed_album_delete_confirmation": "Are you sure you want to delete this album?",
|
||||||
"unnamed_share": "Unnamed Share",
|
"unnamed_share": "Unnamed Share",
|
||||||
|
"unmute_memories": "Unmute Memories",
|
||||||
"unsaved_change": "Unsaved change",
|
"unsaved_change": "Unsaved change",
|
||||||
"unselect_all": "Unselect all",
|
"unselect_all": "Unselect all",
|
||||||
"unselect_all_duplicates": "Unselect all duplicates",
|
"unselect_all_duplicates": "Unselect all duplicates",
|
||||||
@@ -1334,6 +1357,7 @@
|
|||||||
"view_all": "View All",
|
"view_all": "View All",
|
||||||
"view_all_users": "View all users",
|
"view_all_users": "View all users",
|
||||||
"view_in_timeline": "View in timeline",
|
"view_in_timeline": "View in timeline",
|
||||||
|
"view_link": "View link",
|
||||||
"view_links": "View links",
|
"view_links": "View links",
|
||||||
"view_name": "View",
|
"view_name": "View",
|
||||||
"view_next_asset": "View next asset",
|
"view_next_asset": "View next asset",
|
||||||
@@ -1350,4 +1374,4 @@
|
|||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||||
"zoom_image": "Zoom Image"
|
"zoom_image": "Zoom Image"
|
||||||
}
|
}
|
||||||
|
|||||||
25
i18n/es.json
25
i18n/es.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Agregar compañero",
|
"add_partner": "Agregar compañero",
|
||||||
"add_path": "Agregar carpeta",
|
"add_path": "Agregar carpeta",
|
||||||
"add_photos": "Agregar fotos",
|
"add_photos": "Agregar fotos",
|
||||||
"add_to": "Agregar a...",
|
"add_to": "Agregar a…",
|
||||||
"add_to_album": "Incluir en álbum",
|
"add_to_album": "Incluir en álbum",
|
||||||
"add_to_shared_album": "Incluir en álbum compartido",
|
"add_to_shared_album": "Incluir en álbum compartido",
|
||||||
"add_url": "Añadir URL",
|
"add_url": "Añadir URL",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Restablecer la configuración predeterminada",
|
"reset_settings_to_default": "Restablecer la configuración predeterminada",
|
||||||
"reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente",
|
"reset_settings_to_recent_saved": "Restablecer la configuración a la configuración guardada recientemente",
|
||||||
"scanning_library": "Escaneando la biblioteca",
|
"scanning_library": "Escaneando la biblioteca",
|
||||||
"search_jobs": "Buscar trabajo...",
|
"search_jobs": "Buscar trabajos…",
|
||||||
"send_welcome_email": "Enviar correo de bienvenida",
|
"send_welcome_email": "Enviar correo de bienvenida",
|
||||||
"server_external_domain_settings": "Dominio externo",
|
"server_external_domain_settings": "Dominio externo",
|
||||||
"server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://",
|
"server_external_domain_settings_description": "Dominio para enlaces públicos compartidos, incluidos http(s)://",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "¿Son la misma persona?",
|
"are_these_the_same_person": "¿Son la misma persona?",
|
||||||
"are_you_sure_to_do_this": "¿Estas seguro de que quieres hacer esto?",
|
"are_you_sure_to_do_this": "¿Estas seguro de que quieres hacer esto?",
|
||||||
"asset_added_to_album": "Añadido al álbum",
|
"asset_added_to_album": "Añadido al álbum",
|
||||||
"asset_adding_to_album": "Añadiendo al álbum...",
|
"asset_adding_to_album": "Añadiendo al álbum…",
|
||||||
"asset_description_updated": "La descripción del elemento ha sido actualizada",
|
"asset_description_updated": "La descripción del elemento ha sido actualizada",
|
||||||
"asset_filename_is_offline": "El archivo {filename} está offline",
|
"asset_filename_is_offline": "El archivo {filename} está offline",
|
||||||
"asset_has_unassigned_faces": "El archivo no tiene rostros asignados",
|
"asset_has_unassigned_faces": "El archivo no tiene rostros asignados",
|
||||||
"asset_hashing": "Hashing...",
|
"asset_hashing": "Hashing…",
|
||||||
"asset_offline": "Archivos sin conexión",
|
"asset_offline": "Archivos sin conexión",
|
||||||
"asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.",
|
"asset_offline_description": "Este activo externo ya no se encuentra en el disco. Por favor, póngase en contacto con su administrador de Immich para obtener ayuda.",
|
||||||
"asset_skipped": "Omitido",
|
"asset_skipped": "Omitido",
|
||||||
"asset_skipped_in_trash": "En la papelera",
|
"asset_skipped_in_trash": "En la papelera",
|
||||||
"asset_uploaded": "Subido",
|
"asset_uploaded": "Subido",
|
||||||
"asset_uploading": "Subiendo...",
|
"asset_uploading": "Subiendo…",
|
||||||
"assets": "elementos",
|
"assets": "elementos",
|
||||||
"assets_added_count": "Añadido {count, plural, one {# asset} other {# assets}}",
|
"assets_added_count": "Añadido {count, plural, one {# asset} other {# assets}}",
|
||||||
"assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum",
|
"assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum",
|
||||||
@@ -438,7 +438,7 @@
|
|||||||
"blurred_background": "Fondo borroso",
|
"blurred_background": "Fondo borroso",
|
||||||
"bugs_and_feature_requests": "Errores y solicitudes de funciones",
|
"bugs_and_feature_requests": "Errores y solicitudes de funciones",
|
||||||
"build": "Compilación",
|
"build": "Compilación",
|
||||||
"build_image": "Imagen",
|
"build_image": "Construir imagen",
|
||||||
"bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
|
"bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# elemento duplicado} other {# elementos duplicados}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!",
|
||||||
"bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.",
|
"bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.",
|
||||||
"bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.",
|
"bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.",
|
||||||
@@ -766,8 +766,10 @@
|
|||||||
"go_to_folder": "Ir al directorio",
|
"go_to_folder": "Ir al directorio",
|
||||||
"go_to_search": "Ir a búsqueda",
|
"go_to_search": "Ir a búsqueda",
|
||||||
"group_albums_by": "Agrupar albums por...",
|
"group_albums_by": "Agrupar albums por...",
|
||||||
|
"group_country": "Agrupar por país",
|
||||||
"group_no": "Sin agrupación",
|
"group_no": "Sin agrupación",
|
||||||
"group_owner": "Agrupar por propietario",
|
"group_owner": "Agrupar por propietario",
|
||||||
|
"group_places_by": "Agrupar lugares por...",
|
||||||
"group_year": "Agrupar por año",
|
"group_year": "Agrupar por año",
|
||||||
"has_quota": "Su cuota",
|
"has_quota": "Su cuota",
|
||||||
"hi_user": "Hola {name} ({email})",
|
"hi_user": "Hola {name} ({email})",
|
||||||
@@ -800,6 +802,7 @@
|
|||||||
"include_shared_albums": "Incluir álbumes compartidos",
|
"include_shared_albums": "Incluir álbumes compartidos",
|
||||||
"include_shared_partner_assets": "Incluir archivos compartidos de invitados",
|
"include_shared_partner_assets": "Incluir archivos compartidos de invitados",
|
||||||
"individual_share": "Compartir individualmente",
|
"individual_share": "Compartir individualmente",
|
||||||
|
"individual_shares": "Acciones individuales",
|
||||||
"info": "Información",
|
"info": "Información",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Todos los días a las 1pm",
|
"day_at_onepm": "Todos los días a las 1pm",
|
||||||
@@ -822,6 +825,7 @@
|
|||||||
"latest_version": "Última versión",
|
"latest_version": "Última versión",
|
||||||
"latitude": "Latitud",
|
"latitude": "Latitud",
|
||||||
"leave": "Abandonar",
|
"leave": "Abandonar",
|
||||||
|
"lens_model": "Modelo de objetivo",
|
||||||
"let_others_respond": "Permitir que otros respondan",
|
"let_others_respond": "Permitir que otros respondan",
|
||||||
"level": "Nivel",
|
"level": "Nivel",
|
||||||
"library": "Biblioteca",
|
"library": "Biblioteca",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Elige una ubicación",
|
"pick_a_location": "Elige una ubicación",
|
||||||
"place": "Lugar",
|
"place": "Lugar",
|
||||||
"places": "Lugares",
|
"places": "Lugares",
|
||||||
|
"places_count": "{count, plural, one {{count, number} Lugar} other {{count, number} Lugares}}",
|
||||||
"play": "Reproducir",
|
"play": "Reproducir",
|
||||||
"play_memories": "Reproducir recuerdos",
|
"play_memories": "Reproducir recuerdos",
|
||||||
"play_motion_photo": "Reproducir foto en movimiento",
|
"play_motion_photo": "Reproducir foto en movimiento",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Buscar",
|
"search": "Buscar",
|
||||||
"search_albums": "Buscar álbums",
|
"search_albums": "Buscar álbums",
|
||||||
"search_by_context": "Buscar por contexto",
|
"search_by_context": "Buscar por contexto",
|
||||||
|
"search_by_description": "Buscar por descripción",
|
||||||
|
"search_by_description_example": "Día de senderismo en Sapa",
|
||||||
"search_by_filename": "Buscar por nombre de archivo o extensión",
|
"search_by_filename": "Buscar por nombre de archivo o extensión",
|
||||||
"search_by_filename_example": "es decir IMG_1234.JPG o PNG",
|
"search_by_filename_example": "es decir IMG_1234.JPG o PNG",
|
||||||
"search_camera_make": "Buscar fabricante de cámara...",
|
"search_camera_make": "Buscar fabricante de cámara...",
|
||||||
"search_camera_model": "Buscar modelo de cámara...",
|
"search_camera_model": "Buscar modelo de cámara...",
|
||||||
"search_city": "Buscar ciudad...",
|
"search_city": "Buscar ciudad...",
|
||||||
"search_country": "Buscar país...",
|
"search_country": "Buscar país...",
|
||||||
|
"search_for": "Buscar",
|
||||||
"search_for_existing_person": "Buscar persona existente",
|
"search_for_existing_person": "Buscar persona existente",
|
||||||
"search_no_people": "Ninguna persona",
|
"search_no_people": "Ninguna persona",
|
||||||
"search_no_people_named": "Ninguna persona llamada \"{name}\"",
|
"search_no_people_named": "Ninguna persona llamada \"{name}\"",
|
||||||
@@ -1139,7 +1147,7 @@
|
|||||||
"select_library_owner": "Seleccionar propietario de la biblioteca",
|
"select_library_owner": "Seleccionar propietario de la biblioteca",
|
||||||
"select_new_face": "Seleccionar nueva cara",
|
"select_new_face": "Seleccionar nueva cara",
|
||||||
"select_photos": "Seleccionar Fotos",
|
"select_photos": "Seleccionar Fotos",
|
||||||
"select_trash_all": "Descartar todo",
|
"select_trash_all": "Seleccionar eliminar todo",
|
||||||
"selected": "Seleccionado",
|
"selected": "Seleccionado",
|
||||||
"selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}",
|
"selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}",
|
||||||
"send_message": "Enviar mensaje",
|
"send_message": "Enviar mensaje",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Fotos de {partner}",
|
"shared_from_partner": "Fotos de {partner}",
|
||||||
"shared_link_options": "Opciones de enlaces compartidos",
|
"shared_link_options": "Opciones de enlaces compartidos",
|
||||||
"shared_links": "Enlaces compartidos",
|
"shared_links": "Enlaces compartidos",
|
||||||
|
"shared_links_description": "Comparte fotos y vídeos con un enlace",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos y vídeos compartidos.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos y vídeos compartidos.}}",
|
||||||
"shared_with_partner": "Compartido con {partner}",
|
"shared_with_partner": "Compartido con {partner}",
|
||||||
"sharing": "Compartido",
|
"sharing": "Compartido",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Mostrar opciones de la persona",
|
"show_person_options": "Mostrar opciones de la persona",
|
||||||
"show_progress_bar": "Mostrar barra de progreso",
|
"show_progress_bar": "Mostrar barra de progreso",
|
||||||
"show_search_options": "Mostrar opciones de búsqueda",
|
"show_search_options": "Mostrar opciones de búsqueda",
|
||||||
|
"show_shared_links": "Mostrar enlaces compartidos",
|
||||||
"show_slideshow_transition": "Mostrar la transición de las diapositivas",
|
"show_slideshow_transition": "Mostrar la transición de las diapositivas",
|
||||||
"show_supporter_badge": "Insignia de colaborador",
|
"show_supporter_badge": "Insignia de colaborador",
|
||||||
"show_supporter_badge_description": "Mostrar una insignia de colaborador",
|
"show_supporter_badge_description": "Mostrar una insignia de colaborador",
|
||||||
@@ -1274,6 +1284,7 @@
|
|||||||
"unfavorite": "Retirar favorito",
|
"unfavorite": "Retirar favorito",
|
||||||
"unhide_person": "Mostrar persona",
|
"unhide_person": "Mostrar persona",
|
||||||
"unknown": "Desconocido",
|
"unknown": "Desconocido",
|
||||||
|
"unknown_country": "País desconocido",
|
||||||
"unknown_year": "Año desconocido",
|
"unknown_year": "Año desconocido",
|
||||||
"unlimited": "Ilimitado",
|
"unlimited": "Ilimitado",
|
||||||
"unlink_motion_video": "Desvincular vídeo en movimiento",
|
"unlink_motion_video": "Desvincular vídeo en movimiento",
|
||||||
|
|||||||
30
i18n/et.json
30
i18n/et.json
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Lisa partner",
|
"add_partner": "Lisa partner",
|
||||||
"add_path": "Lisa tee",
|
"add_path": "Lisa tee",
|
||||||
"add_photos": "Lisa fotosid",
|
"add_photos": "Lisa fotosid",
|
||||||
"add_to": "Lisa kohta...",
|
"add_to": "Lisa kohta…",
|
||||||
"add_to_album": "Lisa albumisse",
|
"add_to_album": "Lisa albumisse",
|
||||||
"add_to_shared_album": "Lisa jagatud albumisse",
|
"add_to_shared_album": "Lisa jagatud albumisse",
|
||||||
"add_url": "Lisa URL",
|
"add_url": "Lisa URL",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Lähtesta seaded",
|
"reset_settings_to_default": "Lähtesta seaded",
|
||||||
"reset_settings_to_recent_saved": "Taasta hiljuti salvestatud seaded",
|
"reset_settings_to_recent_saved": "Taasta hiljuti salvestatud seaded",
|
||||||
"scanning_library": "Kogu skaneerimine",
|
"scanning_library": "Kogu skaneerimine",
|
||||||
"search_jobs": "Otsi töödet...",
|
"search_jobs": "Otsi töödet…",
|
||||||
"send_welcome_email": "Saada tervituskiri",
|
"send_welcome_email": "Saada tervituskiri",
|
||||||
"server_external_domain_settings": "Väline domeen",
|
"server_external_domain_settings": "Väline domeen",
|
||||||
"server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://",
|
"server_external_domain_settings_description": "Domeen avalikult jagatud linkide jaoks, k.a. http(s)://",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Kas need on sama isik?",
|
"are_these_the_same_person": "Kas need on sama isik?",
|
||||||
"are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?",
|
"are_you_sure_to_do_this": "Kas oled kindel, et soovid seda teha?",
|
||||||
"asset_added_to_album": "Lisatud albumisse",
|
"asset_added_to_album": "Lisatud albumisse",
|
||||||
"asset_adding_to_album": "Albumisse lisamine...",
|
"asset_adding_to_album": "Albumisse lisamine…",
|
||||||
"asset_description_updated": "Üksuse kirjeldus on muudetud",
|
"asset_description_updated": "Üksuse kirjeldus on muudetud",
|
||||||
"asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav",
|
"asset_filename_is_offline": "Üksus {filename} ei ole kättesaadav",
|
||||||
"asset_has_unassigned_faces": "Üksusel on seostamata nägusid",
|
"asset_has_unassigned_faces": "Üksusel on seostamata nägusid",
|
||||||
"asset_hashing": "Räsimine...",
|
"asset_hashing": "Räsimine…",
|
||||||
"asset_offline": "Üksus pole kättesaadav",
|
"asset_offline": "Üksus pole kättesaadav",
|
||||||
"asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.",
|
"asset_offline_description": "Seda välise kogu üksust ei leitud kettalt. Abi saamiseks palun võta ühendust oma Immich'i administraatoriga.",
|
||||||
"asset_skipped": "Vahele jäetud",
|
"asset_skipped": "Vahele jäetud",
|
||||||
"asset_skipped_in_trash": "Prügikastis",
|
"asset_skipped_in_trash": "Prügikastis",
|
||||||
"asset_uploaded": "Üleslaaditud",
|
"asset_uploaded": "Üleslaaditud",
|
||||||
"asset_uploading": "Üleslaadimine...",
|
"asset_uploading": "Üleslaadimine…",
|
||||||
"assets": "Üksused",
|
"assets": "Üksused",
|
||||||
"assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud",
|
"assets_added_count": "{count, plural, one {# üksus} other {# üksust}} lisatud",
|
||||||
"assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud",
|
"assets_added_to_album_count": "{count, plural, one {# üksus} other {# üksust}} albumisse lisatud",
|
||||||
@@ -763,8 +763,10 @@
|
|||||||
"go_to_folder": "Mine kausta",
|
"go_to_folder": "Mine kausta",
|
||||||
"go_to_search": "Otsingusse",
|
"go_to_search": "Otsingusse",
|
||||||
"group_albums_by": "Grupeeri albumid...",
|
"group_albums_by": "Grupeeri albumid...",
|
||||||
|
"group_country": "Grupeeri riigi kaupa",
|
||||||
"group_no": "Ära grupeeri",
|
"group_no": "Ära grupeeri",
|
||||||
"group_owner": "Grupeeri omaniku kaupa",
|
"group_owner": "Grupeeri omaniku kaupa",
|
||||||
|
"group_places_by": "Grupeeri kohad...",
|
||||||
"group_year": "Grupeeri aasta kaupa",
|
"group_year": "Grupeeri aasta kaupa",
|
||||||
"has_quota": "On kvoot",
|
"has_quota": "On kvoot",
|
||||||
"hi_user": "Tere {name} ({email})",
|
"hi_user": "Tere {name} ({email})",
|
||||||
@@ -819,6 +821,7 @@
|
|||||||
"latest_version": "Uusim versioon",
|
"latest_version": "Uusim versioon",
|
||||||
"latitude": "Laiuskraad",
|
"latitude": "Laiuskraad",
|
||||||
"leave": "Lahku",
|
"leave": "Lahku",
|
||||||
|
"lens_model": "Läätse mudel",
|
||||||
"let_others_respond": "Luba teistel vastata",
|
"let_others_respond": "Luba teistel vastata",
|
||||||
"level": "Tase",
|
"level": "Tase",
|
||||||
"library": "Kogu",
|
"library": "Kogu",
|
||||||
@@ -861,6 +864,7 @@
|
|||||||
"memories": "Mälestused",
|
"memories": "Mälestused",
|
||||||
"memories_setting_description": "Halda, mida sa oma mälestustes näed",
|
"memories_setting_description": "Halda, mida sa oma mälestustes näed",
|
||||||
"memory": "Mälestus",
|
"memory": "Mälestus",
|
||||||
|
"memory_lane_title": "Mälestus {title}",
|
||||||
"menu": "Menüü",
|
"menu": "Menüü",
|
||||||
"merge": "Ühenda",
|
"merge": "Ühenda",
|
||||||
"merge_people": "Ühenda isikud",
|
"merge_people": "Ühenda isikud",
|
||||||
@@ -979,6 +983,7 @@
|
|||||||
"pick_a_location": "Vali asukoht",
|
"pick_a_location": "Vali asukoht",
|
||||||
"place": "Asukoht",
|
"place": "Asukoht",
|
||||||
"places": "Kohad",
|
"places": "Kohad",
|
||||||
|
"places_count": "{count, plural, one {{count, number} koht} other {{count, number} kohta}}",
|
||||||
"play": "Esita",
|
"play": "Esita",
|
||||||
"play_memories": "Esita mälestused",
|
"play_memories": "Esita mälestused",
|
||||||
"play_motion_photo": "Esita liikuv foto",
|
"play_motion_photo": "Esita liikuv foto",
|
||||||
@@ -994,6 +999,7 @@
|
|||||||
"profile_image_of_user": "Kasutaja {user} profiilipilt",
|
"profile_image_of_user": "Kasutaja {user} profiilipilt",
|
||||||
"profile_picture_set": "Profiilipilt määratud.",
|
"profile_picture_set": "Profiilipilt määratud.",
|
||||||
"public_album": "Avalik album",
|
"public_album": "Avalik album",
|
||||||
|
"public_share": "Avalik jagamine",
|
||||||
"purchase_account_info": "Toetaja",
|
"purchase_account_info": "Toetaja",
|
||||||
"purchase_activated_subtitle": "Aitäh, et toetad Immich'it ja avatud lähtekoodiga tarkvara",
|
"purchase_activated_subtitle": "Aitäh, et toetad Immich'it ja avatud lähtekoodiga tarkvara",
|
||||||
"purchase_activated_time": "Aktiveeritud {date, date}",
|
"purchase_activated_time": "Aktiveeritud {date, date}",
|
||||||
@@ -1032,6 +1038,7 @@
|
|||||||
"rating_description": "Kuva infopaneelis EXIF hinnangut",
|
"rating_description": "Kuva infopaneelis EXIF hinnangut",
|
||||||
"reaction_options": "Reaktsiooni valikud",
|
"reaction_options": "Reaktsiooni valikud",
|
||||||
"read_changelog": "Vaata muudatuste ülevaadet",
|
"read_changelog": "Vaata muudatuste ülevaadet",
|
||||||
|
"reassign": "Määra uuesti",
|
||||||
"reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}",
|
"reassigned_assets_to_existing_person": "{count, plural, one {# üksus} other {# üksust}} seostatud {name, select, null {olemasoleva isikuga} other {isikuga {name}}}",
|
||||||
"reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga",
|
"reassigned_assets_to_new_person": "{count, plural, one {# üksus} other {# üksust}} seostatud uue isikuga",
|
||||||
"reassing_hint": "Seosta valitud üksused olemasoleva isikuga",
|
"reassing_hint": "Seosta valitud üksused olemasoleva isikuga",
|
||||||
@@ -1066,6 +1073,7 @@
|
|||||||
"removed_from_favorites_count": "{count, plural, other {# eemaldatud}} lemmikutest",
|
"removed_from_favorites_count": "{count, plural, other {# eemaldatud}} lemmikutest",
|
||||||
"removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}",
|
"removed_tagged_assets": "Silt eemaldatud {count, plural, one {# üksuselt} other {# üksuselt}}",
|
||||||
"rename": "Nimeta ümber",
|
"rename": "Nimeta ümber",
|
||||||
|
"repair": "Parandus",
|
||||||
"repair_no_results_message": "Mittejälgitavad ja puuduvad failid kuvatakse siin",
|
"repair_no_results_message": "Mittejälgitavad ja puuduvad failid kuvatakse siin",
|
||||||
"replace_with_upload": "Asenda üleslaadimisega",
|
"replace_with_upload": "Asenda üleslaadimisega",
|
||||||
"repository": "Koodihoidla",
|
"repository": "Koodihoidla",
|
||||||
@@ -1099,12 +1107,15 @@
|
|||||||
"search": "Otsi",
|
"search": "Otsi",
|
||||||
"search_albums": "Otsi albumeid",
|
"search_albums": "Otsi albumeid",
|
||||||
"search_by_context": "Otsi konteksti alusel",
|
"search_by_context": "Otsi konteksti alusel",
|
||||||
|
"search_by_description": "Otsi kirjelduse alusel",
|
||||||
|
"search_by_description_example": "Matkapäev Sapas",
|
||||||
"search_by_filename": "Otsi failinime või -laiendi järgi",
|
"search_by_filename": "Otsi failinime või -laiendi järgi",
|
||||||
"search_by_filename_example": "st. IMG_1234.JPG või PNG",
|
"search_by_filename_example": "st. IMG_1234.JPG või PNG",
|
||||||
"search_camera_make": "Otsi kaamera marki...",
|
"search_camera_make": "Otsi kaamera marki...",
|
||||||
"search_camera_model": "Otsi kaamera mudelit...",
|
"search_camera_model": "Otsi kaamera mudelit...",
|
||||||
"search_city": "Otsi linna...",
|
"search_city": "Otsi linna...",
|
||||||
"search_country": "Otsi riiki...",
|
"search_country": "Otsi riiki...",
|
||||||
|
"search_for": "Otsi",
|
||||||
"search_for_existing_person": "Otsi olemasolevat isikut",
|
"search_for_existing_person": "Otsi olemasolevat isikut",
|
||||||
"search_no_people": "Isikuid ei ole",
|
"search_no_people": "Isikuid ei ole",
|
||||||
"search_no_people_named": "Ei ole isikuid nimega \"{name}\"",
|
"search_no_people_named": "Ei ole isikuid nimega \"{name}\"",
|
||||||
@@ -1127,9 +1138,11 @@
|
|||||||
"select_face": "Vali nägu",
|
"select_face": "Vali nägu",
|
||||||
"select_featured_photo": "Vali esiletõstetud foto",
|
"select_featured_photo": "Vali esiletõstetud foto",
|
||||||
"select_from_computer": "Vali arvutist",
|
"select_from_computer": "Vali arvutist",
|
||||||
|
"select_keep_all": "Vali jäta kõik alles",
|
||||||
"select_library_owner": "Vali kogu omanik",
|
"select_library_owner": "Vali kogu omanik",
|
||||||
"select_new_face": "Vali uus nägu",
|
"select_new_face": "Vali uus nägu",
|
||||||
"select_photos": "Vali fotod",
|
"select_photos": "Vali fotod",
|
||||||
|
"select_trash_all": "Vali kõik prügikasti",
|
||||||
"selected": "Valitud",
|
"selected": "Valitud",
|
||||||
"selected_count": "{count, plural, other {# valitud}}",
|
"selected_count": "{count, plural, other {# valitud}}",
|
||||||
"send_message": "Saada sõnum",
|
"send_message": "Saada sõnum",
|
||||||
@@ -1155,6 +1168,7 @@
|
|||||||
"shared_from_partner": "Fotod partnerilt {partner}",
|
"shared_from_partner": "Fotod partnerilt {partner}",
|
||||||
"shared_link_options": "Jagatud lingi valikud",
|
"shared_link_options": "Jagatud lingi valikud",
|
||||||
"shared_links": "Jagatud lingid",
|
"shared_links": "Jagatud lingid",
|
||||||
|
"shared_links_description": "Jaga fotosid ja videosid lingiga",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# jagatud fotot ja videot.}}",
|
||||||
"shared_with_partner": "Jagatud partneriga {partner}",
|
"shared_with_partner": "Jagatud partneriga {partner}",
|
||||||
"sharing": "Jagamine",
|
"sharing": "Jagamine",
|
||||||
@@ -1177,6 +1191,7 @@
|
|||||||
"show_person_options": "Näita isiku valikuid",
|
"show_person_options": "Näita isiku valikuid",
|
||||||
"show_progress_bar": "Kuva edenemisriba",
|
"show_progress_bar": "Kuva edenemisriba",
|
||||||
"show_search_options": "Kuva otsingu valikud",
|
"show_search_options": "Kuva otsingu valikud",
|
||||||
|
"show_shared_links": "Näita jagatud linke",
|
||||||
"show_slideshow_transition": "Kuva slaidiesitluse üleminekud",
|
"show_slideshow_transition": "Kuva slaidiesitluse üleminekud",
|
||||||
"show_supporter_badge": "Toetaja märk",
|
"show_supporter_badge": "Toetaja märk",
|
||||||
"show_supporter_badge_description": "Kuva toetaja märki",
|
"show_supporter_badge_description": "Kuva toetaja märki",
|
||||||
@@ -1217,6 +1232,7 @@
|
|||||||
"storage": "Talletusruum",
|
"storage": "Talletusruum",
|
||||||
"storage_label": "Talletussilt",
|
"storage_label": "Talletussilt",
|
||||||
"storage_usage": "{used}/{available} kasutatud",
|
"storage_usage": "{used}/{available} kasutatud",
|
||||||
|
"submit": "Saada",
|
||||||
"suggestions": "Soovitused",
|
"suggestions": "Soovitused",
|
||||||
"sunrise_on_the_beach": "Päikesetõus rannal",
|
"sunrise_on_the_beach": "Päikesetõus rannal",
|
||||||
"support": "Tugi",
|
"support": "Tugi",
|
||||||
@@ -1245,6 +1261,7 @@
|
|||||||
"to_change_password": "Muuda parool",
|
"to_change_password": "Muuda parool",
|
||||||
"to_favorite": "Lemmik",
|
"to_favorite": "Lemmik",
|
||||||
"to_login": "Logi sisse",
|
"to_login": "Logi sisse",
|
||||||
|
"to_parent": "Tase üles",
|
||||||
"to_trash": "Prügikasti",
|
"to_trash": "Prügikasti",
|
||||||
"toggle_settings": "Kuva/peida seaded",
|
"toggle_settings": "Kuva/peida seaded",
|
||||||
"toggle_theme": "Lülita tume teema",
|
"toggle_theme": "Lülita tume teema",
|
||||||
@@ -1262,6 +1279,7 @@
|
|||||||
"unfavorite": "Eemalda lemmikutest",
|
"unfavorite": "Eemalda lemmikutest",
|
||||||
"unhide_person": "Ära peida isikut",
|
"unhide_person": "Ära peida isikut",
|
||||||
"unknown": "Teadmata",
|
"unknown": "Teadmata",
|
||||||
|
"unknown_country": "Tundmatu riik",
|
||||||
"unknown_year": "Teadmata aasta",
|
"unknown_year": "Teadmata aasta",
|
||||||
"unlimited": "Piiramatu",
|
"unlimited": "Piiramatu",
|
||||||
"unlink_oauth": "Eemalda OAuth ühendus",
|
"unlink_oauth": "Eemalda OAuth ühendus",
|
||||||
@@ -1269,6 +1287,8 @@
|
|||||||
"unnamed_album": "Nimetu album",
|
"unnamed_album": "Nimetu album",
|
||||||
"unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?",
|
"unnamed_album_delete_confirmation": "Kas oled kindel, et soovid selle albumi kustutada?",
|
||||||
"unsaved_change": "Salvestamata muudatus",
|
"unsaved_change": "Salvestamata muudatus",
|
||||||
|
"unselect_all": "Ära vali ühtegi",
|
||||||
|
"unselect_all_duplicates": "Ära vali duplikaate",
|
||||||
"unstack": "Eralda",
|
"unstack": "Eralda",
|
||||||
"unstacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} eraldatud",
|
"unstacked_assets_count": "{count, plural, one {# üksus} other {# üksust}} eraldatud",
|
||||||
"untracked_files": "Mittejälgitavad failid",
|
"untracked_files": "Mittejälgitavad failid",
|
||||||
|
|||||||
876
i18n/fa.json
876
i18n/fa.json
File diff suppressed because it is too large
Load Diff
@@ -20,7 +20,7 @@
|
|||||||
"add_partner": "Lisää kumppani",
|
"add_partner": "Lisää kumppani",
|
||||||
"add_path": "Lisää polku",
|
"add_path": "Lisää polku",
|
||||||
"add_photos": "Lisää kuvia",
|
"add_photos": "Lisää kuvia",
|
||||||
"add_to": "Lisää...",
|
"add_to": "Lisää…",
|
||||||
"add_to_album": "Lisää albumiin",
|
"add_to_album": "Lisää albumiin",
|
||||||
"add_to_shared_album": "Lisää jaettuun albumiin",
|
"add_to_shared_album": "Lisää jaettuun albumiin",
|
||||||
"add_url": "Lisää URL",
|
"add_url": "Lisää URL",
|
||||||
@@ -540,7 +540,7 @@
|
|||||||
"delete_shared_link": "Poista jaettu linkki",
|
"delete_shared_link": "Poista jaettu linkki",
|
||||||
"delete_tag": "Poista tunniste",
|
"delete_tag": "Poista tunniste",
|
||||||
"delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?",
|
"delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?",
|
||||||
"delete_user": "Poista käyttäjä pysyvästi",
|
"delete_user": "Poista käyttäjä",
|
||||||
"deleted_shared_link": "Jaettu linkki poistettu",
|
"deleted_shared_link": "Jaettu linkki poistettu",
|
||||||
"deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit",
|
"deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit",
|
||||||
"description": "Kuvaus",
|
"description": "Kuvaus",
|
||||||
|
|||||||
27
i18n/fr.json
27
i18n/fr.json
@@ -59,7 +59,7 @@
|
|||||||
"external_library_management": "Gestion de la bibliothèque externe",
|
"external_library_management": "Gestion de la bibliothèque externe",
|
||||||
"face_detection": "Détection des visages",
|
"face_detection": "Détection des visages",
|
||||||
"face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les médias en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été pris en compte. Lorsque la détection est terminée, tous les visages détectés sont ensuite mis en file d'attente pour la reconnaissance faciale.",
|
"face_detection_description": "Détection des visages dans les médias à l'aide de l'apprentissage automatique. Pour les vidéos, seule la miniature est prise en compte. « Actualiser » (re)traite tous les médias. « Réinitialiser » retraite tous les médias en repartant de zéro. « Manquant » met en file d'attente les médias qui n'ont pas encore été pris en compte. Lorsque la détection est terminée, tous les visages détectés sont ensuite mis en file d'attente pour la reconnaissance faciale.",
|
||||||
"facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Rafraichir » (re)regroupe tous les visages. « Manquant » met en file d'attente les visages auxquels aucune personne n'a été attribuée.",
|
"facial_recognition_job_description": "Regrouper les visages détectés en personnes. Cette étape est exécutée une fois la détection des visages terminée. « Réinitialiser » (re)regroupe tous les visages. « Manquant » met en file d'attente les visages auxquels aucune personne n'a été attribuée.",
|
||||||
"failed_job_command": "La commande {command} a échoué pour la tâche : {job}",
|
"failed_job_command": "La commande {command} a échoué pour la tâche : {job}",
|
||||||
"force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.",
|
"force_delete_user_warning": "ATTENTION : Cette opération entraîne la suppression immédiate de l'utilisateur et de tous ses médias. Cette opération ne peut être annulée et les fichiers ne peuvent être récupérés.",
|
||||||
"forcing_refresh_library_files": "Forcer le rafraîchissement de tous les fichiers de la bibliothèque",
|
"forcing_refresh_library_files": "Forcer le rafraîchissement de tous les fichiers de la bibliothèque",
|
||||||
@@ -219,7 +219,7 @@
|
|||||||
"reset_settings_to_default": "Réinitialiser les paramètres par défaut",
|
"reset_settings_to_default": "Réinitialiser les paramètres par défaut",
|
||||||
"reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés",
|
"reset_settings_to_recent_saved": "Paramètres réinitialisés avec les derniers paramètres enregistrés",
|
||||||
"scanning_library": "Analyse de la bibliothèque",
|
"scanning_library": "Analyse de la bibliothèque",
|
||||||
"search_jobs": "Recherche des tâches ...",
|
"search_jobs": "Recherche des tâches…",
|
||||||
"send_welcome_email": "Envoyer un courriel de bienvenue",
|
"send_welcome_email": "Envoyer un courriel de bienvenue",
|
||||||
"server_external_domain_settings": "Domaine externe",
|
"server_external_domain_settings": "Domaine externe",
|
||||||
"server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://",
|
"server_external_domain_settings_description": "Nom de domaine pour les liens partagés publics, y compris http(s)://",
|
||||||
@@ -406,17 +406,17 @@
|
|||||||
"are_these_the_same_person": "Est-ce la même personne ?",
|
"are_these_the_same_person": "Est-ce la même personne ?",
|
||||||
"are_you_sure_to_do_this": "Êtes-vous sûr de vouloir faire ceci ?",
|
"are_you_sure_to_do_this": "Êtes-vous sûr de vouloir faire ceci ?",
|
||||||
"asset_added_to_album": "Ajouté à l'album",
|
"asset_added_to_album": "Ajouté à l'album",
|
||||||
"asset_adding_to_album": "Ajout à l'album...",
|
"asset_adding_to_album": "Ajout à l'album…",
|
||||||
"asset_description_updated": "La description du média a été mise à jour",
|
"asset_description_updated": "La description du média a été mise à jour",
|
||||||
"asset_filename_is_offline": "Le média {filename} est hors ligne",
|
"asset_filename_is_offline": "Le média {filename} est hors ligne",
|
||||||
"asset_has_unassigned_faces": "Le média a des visages non attribués",
|
"asset_has_unassigned_faces": "Le média a des visages non attribués",
|
||||||
"asset_hashing": "Hachage...",
|
"asset_hashing": "Hachage…",
|
||||||
"asset_offline": "Média hors ligne",
|
"asset_offline": "Média hors ligne",
|
||||||
"asset_offline_description": "Ce média externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.",
|
"asset_offline_description": "Ce média externe n'est plus accessible sur le disque. Veuillez contacter votre administrateur Immich pour obtenir de l'aide.",
|
||||||
"asset_skipped": "Sauté",
|
"asset_skipped": "Sauté",
|
||||||
"asset_skipped_in_trash": "À la corbeille",
|
"asset_skipped_in_trash": "À la corbeille",
|
||||||
"asset_uploaded": "Envoyé",
|
"asset_uploaded": "Envoyé",
|
||||||
"asset_uploading": "Envoi...",
|
"asset_uploading": "Téléversement…",
|
||||||
"assets": "Médias",
|
"assets": "Médias",
|
||||||
"assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}",
|
"assets_added_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}}",
|
||||||
"assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album",
|
"assets_added_to_album_count": "{count, plural, one {# média ajouté} other {# médias ajoutés}} à l'album",
|
||||||
@@ -766,9 +766,11 @@
|
|||||||
"go_to_folder": "Dossier",
|
"go_to_folder": "Dossier",
|
||||||
"go_to_search": "Faire une recherche",
|
"go_to_search": "Faire une recherche",
|
||||||
"group_albums_by": "Grouper les albums par...",
|
"group_albums_by": "Grouper les albums par...",
|
||||||
|
"group_country": "Grouper par pays",
|
||||||
"group_no": "Pas de groupe",
|
"group_no": "Pas de groupe",
|
||||||
"group_owner": "Groupe par propriétaire",
|
"group_owner": "Grouper par propriétaire",
|
||||||
"group_year": "Groupe par année",
|
"group_places_by": "Grouper les lieux par...",
|
||||||
|
"group_year": "Grouper par année",
|
||||||
"has_quota": "Quota",
|
"has_quota": "Quota",
|
||||||
"hi_user": "Bonjour {name} ({email})",
|
"hi_user": "Bonjour {name} ({email})",
|
||||||
"hide_all_people": "Cacher toutes les personnes",
|
"hide_all_people": "Cacher toutes les personnes",
|
||||||
@@ -799,7 +801,8 @@
|
|||||||
"include_archived": "Inclure les archives",
|
"include_archived": "Inclure les archives",
|
||||||
"include_shared_albums": "Inclure les albums partagés",
|
"include_shared_albums": "Inclure les albums partagés",
|
||||||
"include_shared_partner_assets": "Inclure les médias partagés du partenaire",
|
"include_shared_partner_assets": "Inclure les médias partagés du partenaire",
|
||||||
"individual_share": "Partage individuel",
|
"individual_share": "Partage d'un média unique",
|
||||||
|
"individual_shares": "Partages d'un média unique",
|
||||||
"info": "Information",
|
"info": "Information",
|
||||||
"interval": {
|
"interval": {
|
||||||
"day_at_onepm": "Tous les jours à 13h",
|
"day_at_onepm": "Tous les jours à 13h",
|
||||||
@@ -822,6 +825,7 @@
|
|||||||
"latest_version": "Dernière version",
|
"latest_version": "Dernière version",
|
||||||
"latitude": "Latitude",
|
"latitude": "Latitude",
|
||||||
"leave": "Quitter",
|
"leave": "Quitter",
|
||||||
|
"lens_model": "Modèle d'objectif",
|
||||||
"let_others_respond": "Laisser les autres réagir",
|
"let_others_respond": "Laisser les autres réagir",
|
||||||
"level": "Niveau",
|
"level": "Niveau",
|
||||||
"library": "Bibliothèque",
|
"library": "Bibliothèque",
|
||||||
@@ -984,6 +988,7 @@
|
|||||||
"pick_a_location": "Choisissez un lieu",
|
"pick_a_location": "Choisissez un lieu",
|
||||||
"place": "Lieu",
|
"place": "Lieu",
|
||||||
"places": "Lieux",
|
"places": "Lieux",
|
||||||
|
"places_count": "{count, plural, one {{count, number} Lieu} other {{count, number} Lieux}}",
|
||||||
"play": "Jouer",
|
"play": "Jouer",
|
||||||
"play_memories": "Lancer les souvenirs",
|
"play_memories": "Lancer les souvenirs",
|
||||||
"play_motion_photo": "Jouer la photo animée",
|
"play_motion_photo": "Jouer la photo animée",
|
||||||
@@ -1107,12 +1112,15 @@
|
|||||||
"search": "Recherche",
|
"search": "Recherche",
|
||||||
"search_albums": "Rechercher des albums",
|
"search_albums": "Rechercher des albums",
|
||||||
"search_by_context": "Rechercher par contexte",
|
"search_by_context": "Rechercher par contexte",
|
||||||
|
"search_by_description": "Recherche par description",
|
||||||
|
"search_by_description_example": "Randonnée à Sapa",
|
||||||
"search_by_filename": "Rechercher par nom du fichier ou extension",
|
"search_by_filename": "Rechercher par nom du fichier ou extension",
|
||||||
"search_by_filename_example": "Exemple : IMG_1234.JPG ou PNG",
|
"search_by_filename_example": "Exemple : IMG_1234.JPG ou PNG",
|
||||||
"search_camera_make": "Rechercher par marque d'appareil photo...",
|
"search_camera_make": "Rechercher par marque d'appareil photo...",
|
||||||
"search_camera_model": "Rechercher par modèle d'appareil photo...",
|
"search_camera_model": "Rechercher par modèle d'appareil photo...",
|
||||||
"search_city": "Rechercher par ville...",
|
"search_city": "Rechercher par ville...",
|
||||||
"search_country": "Rechercher par pays...",
|
"search_country": "Rechercher par pays...",
|
||||||
|
"search_for": "Chercher",
|
||||||
"search_for_existing_person": "Rechercher une personne existante",
|
"search_for_existing_person": "Rechercher une personne existante",
|
||||||
"search_no_people": "Aucune personne",
|
"search_no_people": "Aucune personne",
|
||||||
"search_no_people_named": "Aucune personne nommée « {name} »",
|
"search_no_people_named": "Aucune personne nommée « {name} »",
|
||||||
@@ -1165,6 +1173,7 @@
|
|||||||
"shared_from_partner": "Photos de {partner}",
|
"shared_from_partner": "Photos de {partner}",
|
||||||
"shared_link_options": "Options de lien partagé",
|
"shared_link_options": "Options de lien partagé",
|
||||||
"shared_links": "Liens partagés",
|
"shared_links": "Liens partagés",
|
||||||
|
"shared_links_description": "Partager les photos et vidéos via un lien",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# photos et vidéos partagées.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# photos et vidéos partagées.}}",
|
||||||
"shared_with_partner": "Partagé avec {partner}",
|
"shared_with_partner": "Partagé avec {partner}",
|
||||||
"sharing": "Partage",
|
"sharing": "Partage",
|
||||||
@@ -1187,6 +1196,7 @@
|
|||||||
"show_person_options": "Afficher les options de personnes",
|
"show_person_options": "Afficher les options de personnes",
|
||||||
"show_progress_bar": "Afficher la barre de progression",
|
"show_progress_bar": "Afficher la barre de progression",
|
||||||
"show_search_options": "Afficher les options de recherche",
|
"show_search_options": "Afficher les options de recherche",
|
||||||
|
"show_shared_links": "Afficher les liens partagés",
|
||||||
"show_slideshow_transition": "Afficher la transition du diaporama",
|
"show_slideshow_transition": "Afficher la transition du diaporama",
|
||||||
"show_supporter_badge": "Badge de contributeur",
|
"show_supporter_badge": "Badge de contributeur",
|
||||||
"show_supporter_badge_description": "Afficher le badge de contributeur",
|
"show_supporter_badge_description": "Afficher le badge de contributeur",
|
||||||
@@ -1274,6 +1284,7 @@
|
|||||||
"unfavorite": "Enlever des favoris",
|
"unfavorite": "Enlever des favoris",
|
||||||
"unhide_person": "Afficher la personne",
|
"unhide_person": "Afficher la personne",
|
||||||
"unknown": "Inconnu",
|
"unknown": "Inconnu",
|
||||||
|
"unknown_country": "Pays non connu",
|
||||||
"unknown_year": "Année inconnue",
|
"unknown_year": "Année inconnue",
|
||||||
"unlimited": "Illimité",
|
"unlimited": "Illimité",
|
||||||
"unlink_motion_video": "Détacher la photo animée",
|
"unlink_motion_video": "Détacher la photo animée",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user