mirror of
https://github.com/immich-app/immich.git
synced 2026-01-23 01:49:01 -08:00
Compare commits
300 Commits
v1.124.2
...
feat/share
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8b0684ee9c | ||
|
|
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 | ||
|
|
071b271484 | ||
|
|
50a2f6193f | ||
|
|
907fed1081 | ||
|
|
49a16045bd | ||
|
|
a47aa86392 | ||
|
|
f32c5d97cd | ||
|
|
afc6e91c66 | ||
|
|
1311189fab | ||
|
|
fa3b5a4c8f | ||
|
|
d3446f3092 | ||
|
|
b31414af8f | ||
|
|
cf99dcb279 | ||
|
|
dc56ed5d45 | ||
|
|
d1d26c60d6 | ||
|
|
66849d0d45 | ||
|
|
30b8864d2d | ||
|
|
78464a4ba3 | ||
|
|
1f19a65d1a | ||
|
|
ca3619658b | ||
|
|
c7a1f2944f | ||
|
|
7b71c145c8 | ||
|
|
49a6961ec6 | ||
|
|
7b882b35e5 | ||
|
|
443aad5794 | ||
|
|
8d6cbb51e2 | ||
|
|
c8abe9a2fd | ||
|
|
58a75d59bd | ||
|
|
36058b9b59 | ||
|
|
8440f146e2 | ||
|
|
3da17da7b4 | ||
|
|
ccf6d71c3c | ||
|
|
5171630b98 | ||
|
|
9a27a99cab | ||
|
|
332a865ce6 | ||
|
|
0c152366ec | ||
|
|
c35fd6cbdb | ||
|
|
58d5cc1e4b | ||
|
|
9a1068c867 | ||
|
|
1745f48f3d | ||
|
|
b0cdd8f475 | ||
|
|
318dd32363 | ||
|
|
887267b133 | ||
|
|
1d0d4fc281 | ||
|
|
345791c0e6 | ||
|
|
07698f8a40 | ||
|
|
6fdb8f83f0 | ||
|
|
a0b2c69b99 | ||
|
|
70809c1465 | ||
|
|
97ec3b147c | ||
|
|
d249b63c99 | ||
|
|
0f803a4f5e | ||
|
|
8eac82c5a3 | ||
|
|
3d13da7f11 | ||
|
|
430d0b86ee | ||
|
|
f40fdce658 | ||
|
|
097183b31d | ||
|
|
d5a9294eeb | ||
|
|
c5582fc8d9 | ||
|
|
6993726d50 | ||
|
|
c821458e6c | ||
|
|
efbc0cb192 | ||
|
|
fd99bd05cf | ||
|
|
3a2bf91889 | ||
|
|
378bd3c993 | ||
|
|
89f40b311c | ||
|
|
6ce1533117 | ||
|
|
0ce62d8efd | ||
|
|
e151248b16 | ||
|
|
a2207f2eef | ||
|
|
81568dbda3 | ||
|
|
a60da1ccab | ||
|
|
2d2966caa0 | ||
|
|
7d087371b5 | ||
|
|
93e2545275 | ||
|
|
43b3181f45 | ||
|
|
2903ad8156 | ||
|
|
c5476a99b1 | ||
|
|
5d2e421800 | ||
|
|
b9000d8770 | ||
|
|
073fccb517 | ||
|
|
3e11b90851 | ||
|
|
19e2504583 | ||
|
|
4279cd6e1e | ||
|
|
f70ee3f350 | ||
|
|
9e1651ef66 | ||
|
|
a35af2b242 | ||
|
|
fc99c5f530 | ||
|
|
e978b8c685 | ||
|
|
3b06220219 | ||
|
|
dc53e2a9b9 | ||
|
|
28b08ed417 | ||
|
|
b74f013b53 | ||
|
|
79726acc72 | ||
|
|
36eef9807b | ||
|
|
3da750117f | ||
|
|
a6c8eb57f1 | ||
|
|
efe4396e54 | ||
|
|
c4a8fdf0f3 | ||
|
|
abf5b0afe1 | ||
|
|
77d4eb8787 | ||
|
|
e7abfe3067 | ||
|
|
be1187bc46 | ||
|
|
fef36e6a37 | ||
|
|
a39fbcb8ac | ||
|
|
ca75bba3b0 | ||
|
|
f3dbbfa16d | ||
|
|
8b4390c247 | ||
|
|
581d32269d | ||
|
|
2b76112014 | ||
|
|
2301affd7e | ||
|
|
2f9a66e961 | ||
|
|
0b8cfc6b82 | ||
|
|
cab201270c | ||
|
|
beb31cebed | ||
|
|
e51091b6e5 | ||
|
|
cc6a8b0c74 | ||
|
|
930f979960 | ||
|
|
3030e74fc3 | ||
|
|
f9db60f25b | ||
|
|
7d50d3032b | ||
|
|
1fb2b3f899 | ||
|
|
2e12c46980 | ||
|
|
1489d69f81 |
2
.devcontainer/.gitignore
vendored
Normal file
2
.devcontainer/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
.env
|
||||
library
|
||||
@@ -1,2 +1,16 @@
|
||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:9791f4aa527774bc370c6bd2f6705ce5a686f1e6f204badd8dfaacce28c631ae
|
||||
FROM ${BASEIMAGE}
|
||||
|
||||
# Flutter SDK
|
||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||
ENV FLUTTER_CHANNEL="stable"
|
||||
ENV FLUTTER_VERSION="3.24.5"
|
||||
ENV FLUTTER_HOME=/flutter
|
||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
||||
|
||||
# Flutter SDK
|
||||
RUN mkdir -p ${FLUTTER_HOME} \
|
||||
&& curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \
|
||||
&& tar -xf flutter.tar.xz --strip-components=1 -C ${FLUTTER_HOME} \
|
||||
&& rm flutter.tar.xz \
|
||||
&& chown -R 1000:1000 ${FLUTTER_HOME}
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
{
|
||||
"name": "Immich devcontainers",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile",
|
||||
"args": {
|
||||
"BASEIMAGE": "mcr.microsoft.com/devcontainers/typescript-node:22"
|
||||
}
|
||||
},
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [],
|
||||
"postCreateCommand": "make install-all",
|
||||
"remoteUser": "node"
|
||||
"name": "Immich",
|
||||
"service": "immich-devcontainer",
|
||||
"dockerComposeFile": [
|
||||
"docker-compose.yml",
|
||||
"../docker/docker-compose.dev.yml"
|
||||
],
|
||||
"customizations": {
|
||||
"vscode": {
|
||||
"extensions": [
|
||||
"Dart-Code.dart-code",
|
||||
"Dart-Code.flutter",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"dcmdev.dcm-vscode-extension",
|
||||
"esbenp.prettier-vscode",
|
||||
"svelte.svelte-vscode"
|
||||
]
|
||||
}
|
||||
},
|
||||
"forwardPorts": [],
|
||||
"initializeCommand": "bash .devcontainer/scripts/initializeCommand.sh",
|
||||
"onCreateCommand": "bash .devcontainer/scripts/onCreateCommand.sh",
|
||||
"overrideCommand": true,
|
||||
"workspaceFolder": "/immich",
|
||||
"remoteUser": "node"
|
||||
}
|
||||
|
||||
|
||||
8
.devcontainer/docker-compose.yml
Normal file
8
.devcontainer/docker-compose.yml
Normal file
@@ -0,0 +1,8 @@
|
||||
services:
|
||||
immich-devcontainer:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
extra_hosts:
|
||||
- 'host.docker.internal:host-gateway'
|
||||
volumes:
|
||||
- ..:/immich:cached
|
||||
6
.devcontainer/scripts/initializeCommand.sh
Normal file
6
.devcontainer/scripts/initializeCommand.sh
Normal file
@@ -0,0 +1,6 @@
|
||||
#!/bin/bash
|
||||
|
||||
# If .env file does not exist, create it by copying example.env from the docker folder
|
||||
if [ ! -f ".devcontainer/.env" ]; then
|
||||
cp docker/example.env .devcontainer/.env
|
||||
fi
|
||||
25
.devcontainer/scripts/onCreateCommand.sh
Normal file
25
.devcontainer/scripts/onCreateCommand.sh
Normal file
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Enable multiarch for arm64 if necessary
|
||||
if [ "$(dpkg --print-architecture)" = "arm64" ]; then
|
||||
sudo dpkg --add-architecture amd64 && \
|
||||
sudo apt-get update && \
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
qemu-user-static \
|
||||
libc6:amd64 \
|
||||
libstdc++6:amd64 \
|
||||
libgcc1:amd64
|
||||
fi
|
||||
|
||||
# Install DCM
|
||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
||||
sudo echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
||||
|
||||
sudo apt-get update
|
||||
sudo apt-get install dcm
|
||||
|
||||
dart --disable-analytics
|
||||
|
||||
# Install immich
|
||||
cd /immich || exit
|
||||
make install-all
|
||||
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
|
||||
|
||||
@@ -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: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
workflow:
|
||||
- '.github/workflows/build-mobile.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build-sign-android:
|
||||
name: Build and sign Android
|
||||
|
||||
6
.github/workflows/cli.yml
vendored
6
.github/workflows/cli.yml
vendored
@@ -56,10 +56,10 @@ jobs:
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
uses: docker/setup-qemu-action@v3.4.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
@@ -88,7 +88,7 @@ jobs:
|
||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
file: cli/Dockerfile
|
||||
platforms: linux/amd64,linux/arm64
|
||||
|
||||
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 }}"
|
||||
344
.github/workflows/docker.yml
vendored
344
.github/workflows/docker.yml
vendored
@@ -36,10 +36,12 @@ jobs:
|
||||
- 'i18n/**'
|
||||
machine-learning:
|
||||
- 'machine-learning/**'
|
||||
workflow:
|
||||
- '.github/workflows/docker.yml'
|
||||
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
retag_ml:
|
||||
name: Re-Tag ML
|
||||
@@ -61,8 +63,10 @@ jobs:
|
||||
REGISTRY_NAME="ghcr.io"
|
||||
REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
|
||||
TAG_OLD=main${{ matrix.suffix }}
|
||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
retag_server:
|
||||
name: Re-Tag Server
|
||||
@@ -84,107 +88,100 @@ jobs:
|
||||
REGISTRY_NAME="ghcr.io"
|
||||
REPOSITORY=${{ github.repository_owner }}/immich-server
|
||||
TAG_OLD=main${{ matrix.suffix }}
|
||||
TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
TAG_PR=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
|
||||
TAG_COMMIT=commit-${{ github.event_name != 'pull_request' && github.sha || github.event.pull_request.head.sha }}${{ matrix.suffix }}
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_PR $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_COMMIT $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
|
||||
|
||||
build_and_push_ml:
|
||||
name: Build and Push ML
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
env:
|
||||
image: immich-machine-learning
|
||||
context: machine-learning
|
||||
file: machine-learning/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||
strategy:
|
||||
# Prevent a failure in one image from stopping the other builds
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platforms: linux/amd64,linux/arm64
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: cpu
|
||||
|
||||
- platforms: linux/amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
device: cpu
|
||||
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: cuda
|
||||
suffix: -cuda
|
||||
|
||||
- platforms: linux/amd64
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
device: openvino
|
||||
suffix: -openvino
|
||||
|
||||
- platforms: linux/arm64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
device: armnn
|
||||
suffix: -armnn
|
||||
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
uses: docker/setup-buildx-action@v3.9.0
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
- name: Generate cache key suffix
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Generate cache target
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
@@ -192,100 +189,245 @@ jobs:
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_ml:
|
||||
name: Merge & Push ML
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
env:
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||
DOCKER_REPO: altran1502/immich-machine-learning
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- device: cpu
|
||||
- device: cuda
|
||||
suffix: -cuda
|
||||
- device: openvino
|
||||
suffix: -openvino
|
||||
- device: armnn
|
||||
suffix: -armnn
|
||||
needs:
|
||||
- build_and_push_ml
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: ml-digests-${{ matrix.device }}-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=${{ env.GHCR_REPO }}
|
||||
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with long commit sha hash
|
||||
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
build_and_push_server:
|
||||
name: Build and Push Server
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: ${{ matrix.runner }}
|
||||
needs: pre-job
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||
env:
|
||||
image: immich-server
|
||||
context: .
|
||||
file: server/Dockerfile
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- platforms: linux/amd64,linux/arm64
|
||||
device: cpu
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
steps:
|
||||
- name: Prepare
|
||||
run: |
|
||||
platform=${{ matrix.platform }}
|
||||
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.2.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.8.0
|
||||
|
||||
- name: Login to Docker Hub
|
||||
# Only push to Docker Hub when making a release
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
# Skip when PR from a fork
|
||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Generate cache key suffix
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||
else
|
||||
echo "CACHE_KEY_SUFFIX=$(echo ${{ github.ref_name }} | sed 's/[^a-zA-Z0-9]/-/g')" >> $GITHUB_ENV
|
||||
fi
|
||||
|
||||
- name: Generate cache target
|
||||
id: cache-target
|
||||
run: |
|
||||
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
id: build
|
||||
uses: docker/build-push-action@v6.13.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
cache-from: |
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
|
||||
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||
build-args: |
|
||||
DEVICE=cpu
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
|
||||
- name: Export digest
|
||||
run: |
|
||||
mkdir -p ${{ runner.temp }}/digests
|
||||
digest="${{ steps.build.outputs.digest }}"
|
||||
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||
|
||||
- name: Upload digest
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: server-digests-${{ env.PLATFORM_PAIR }}
|
||||
path: ${{ runner.temp }}/digests/*
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
merge_server:
|
||||
name: Merge & Push Server
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||
env:
|
||||
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||
DOCKER_REPO: altran1502/immich-server
|
||||
needs:
|
||||
- build_and_push_server
|
||||
steps:
|
||||
- name: Download digests
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
path: ${{ runner.temp }}/digests
|
||||
pattern: server-digests-*
|
||||
merge-multiple: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
if: ${{ github.event_name == 'release' }}
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Generate docker image tags
|
||||
id: metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
flavor: |
|
||||
# Disable latest tag
|
||||
latest=false
|
||||
images: |
|
||||
name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
|
||||
name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
|
||||
name=${{ env.GHCR_REPO }}
|
||||
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||
tags: |
|
||||
# Tag with branch name
|
||||
type=ref,event=branch,suffix=${{ matrix.suffix }}
|
||||
# Tag with pr-number
|
||||
type=ref,event=pr,suffix=${{ matrix.suffix }}
|
||||
# Tag with long commit sha hash
|
||||
type=sha,format=long,prefix=commit-,suffix=${{ matrix.suffix }}
|
||||
# Tag with git tag on release
|
||||
type=ref,event=tag,suffix=${{ matrix.suffix }}
|
||||
type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
|
||||
|
||||
- name: Determine build cache output
|
||||
id: cache-target
|
||||
- name: Create manifest list and push
|
||||
working-directory: ${{ runner.temp }}/digests
|
||||
run: |
|
||||
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||
# Essentially just ignore the cache output (PR can't write to registry cache)
|
||||
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Build and push image
|
||||
uses: docker/build-push-action@v6.10.0
|
||||
with:
|
||||
context: ${{ env.context }}
|
||||
file: ${{ env.file }}
|
||||
platforms: ${{ matrix.platforms }}
|
||||
# Skip pushing when PR from a fork
|
||||
push: ${{ !github.event.pull_request.head.repo.fork }}
|
||||
cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
|
||||
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||
tags: ${{ steps.metadata.outputs.tags }}
|
||||
labels: ${{ steps.metadata.outputs.labels }}
|
||||
build-args: |
|
||||
DEVICE=${{ matrix.device }}
|
||||
BUILD_ID=${{ github.run_id }}
|
||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||
docker buildx imagetools create $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||
$(printf '${{ env.GHCR_REPO }}@sha256:%s ' *)
|
||||
|
||||
success-check-server:
|
||||
name: Docker Build & Push Server Success
|
||||
needs: [build_and_push_server, retag_server]
|
||||
needs: [merge_server, retag_server]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
@@ -298,7 +440,7 @@ jobs:
|
||||
|
||||
success-check-ml:
|
||||
name: Docker Build & Push ML Success
|
||||
needs: [build_and_push_ml, retag_ml]
|
||||
needs: [merge_ml, retag_ml]
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
|
||||
6
.github/workflows/docs-build.yml
vendored
6
.github/workflows/docs-build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
||||
pre-job:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
@@ -25,9 +25,11 @@ jobs:
|
||||
filters: |
|
||||
docs:
|
||||
- 'docs/**'
|
||||
workflow:
|
||||
- '.github/workflows/docs-build.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' || github.ref_name == 'main' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
build:
|
||||
name: Docs Build
|
||||
|
||||
9
.github/workflows/prepare-release.yml
vendored
9
.github/workflows/prepare-release.yml
vendored
@@ -68,10 +68,17 @@ jobs:
|
||||
needs: build_mobile
|
||||
|
||||
steps:
|
||||
- name: Generate a token
|
||||
id: generate-token
|
||||
uses: actions/create-github-app-token@v1
|
||||
with:
|
||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
token: ${{ secrets.ORG_RELEASE_TOKEN }}
|
||||
token: ${{ steps.generate-token.outputs.token }}
|
||||
|
||||
- name: Download APK
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
17
.github/workflows/preview-comment.yaml
vendored
Normal file
17
.github/workflows/preview-comment.yaml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Preview comment
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [labeled]
|
||||
|
||||
jobs:
|
||||
comment-status:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.event.label.name == 'preview' }}
|
||||
permissions:
|
||||
pull-requests: write
|
||||
steps:
|
||||
- uses: mshick/add-pr-comment@v2
|
||||
with:
|
||||
message-id: "preview-status"
|
||||
message: "Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/"
|
||||
4
.github/workflows/static_analysis.yml
vendored
4
.github/workflows/static_analysis.yml
vendored
@@ -23,9 +23,11 @@ jobs:
|
||||
filters: |
|
||||
mobile:
|
||||
- 'mobile/**'
|
||||
workflow:
|
||||
- '.github/workflows/static_analysis.yml'
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
mobile-dart-analyze:
|
||||
name: Run Dart Code Analysis
|
||||
|
||||
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
@@ -43,10 +43,12 @@ jobs:
|
||||
- 'mobile/**'
|
||||
machine-learning:
|
||||
- 'machine-learning/**'
|
||||
workflow:
|
||||
- '.github/workflows/test.yml'
|
||||
|
||||
- name: Check if we should force jobs to run
|
||||
id: should_force
|
||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
run: echo "should_force=${{ steps.found_paths.outputs.workflow == 'true' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
server-unit-tests:
|
||||
name: Test & Lint Server
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.12.0
|
||||
22.14.0
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM node:22.12.0-alpine3.20@sha256:96cc8323e25c8cc6ddcb8b965e135cfd57846e8003ec0d7bcec16c5fd5f6d39f AS core
|
||||
FROM node:22.13.1-alpine3.20@sha256:c52e20859a92b3eccbd3a36c5e1a90adc20617d8d421d65e8a622e87b5dac963 AS core
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
|
||||
1130
cli/package-lock.json
generated
1130
cli/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@immich/cli",
|
||||
"version": "2.2.40",
|
||||
"version": "2.2.50",
|
||||
"description": "Command Line Interface (CLI) for Immich",
|
||||
"type": "module",
|
||||
"exports": "./dist/index.js",
|
||||
@@ -20,15 +20,15 @@
|
||||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node": "^22.13.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"byte-size": "^9.0.0",
|
||||
"cli-progress": "^3.12.0",
|
||||
"commander": "^12.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^56.0.1",
|
||||
"globals": "^15.9.0",
|
||||
@@ -36,9 +36,9 @@
|
||||
"prettier": "^3.2.5",
|
||||
"prettier-plugin-organize-imports": "^4.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^6.0.0",
|
||||
"vite-tsconfig-paths": "^5.0.0",
|
||||
"vitest": "^2.0.5",
|
||||
"vitest": "^3.0.0",
|
||||
"vitest-fetch-mock": "^0.4.0",
|
||||
"yaml": "^2.3.1"
|
||||
},
|
||||
@@ -67,6 +67,6 @@
|
||||
"lodash-es": "^4.17.21"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.12.0"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ interface Test {
|
||||
test: string;
|
||||
options: Omit<CrawlOptions, 'extensions'>;
|
||||
files: Record<string, boolean>;
|
||||
skipOnWin32?: boolean;
|
||||
}
|
||||
|
||||
const cwd = process.cwd();
|
||||
@@ -48,6 +49,18 @@ const tests: Test[] = [
|
||||
'/photos/image.jpg': true,
|
||||
},
|
||||
},
|
||||
{
|
||||
test: 'should crawl folders with quotes',
|
||||
options: {
|
||||
pathsToCrawl: ["/photo's/", '/photo"s/', '/photo`s/'],
|
||||
},
|
||||
files: {
|
||||
"/photo's/image1.jpg": true,
|
||||
'/photo"s/image2.jpg': true,
|
||||
'/photo`s/image3.jpg': true,
|
||||
},
|
||||
skipOnWin32: true, // single quote interferes with mockfs root on Windows
|
||||
},
|
||||
{
|
||||
test: 'should crawl a single file',
|
||||
options: {
|
||||
@@ -270,8 +283,12 @@ describe('crawl', () => {
|
||||
});
|
||||
|
||||
describe('crawl', () => {
|
||||
for (const { test, options, files } of tests) {
|
||||
it(test, async () => {
|
||||
for (const { test: name, options, files, skipOnWin32 } of tests) {
|
||||
if (process.platform === 'win32' && skipOnWin32) {
|
||||
test.skip(name);
|
||||
continue;
|
||||
}
|
||||
it(name, async () => {
|
||||
// The file contents is the same as the path.
|
||||
mockfs(Object.fromEntries(Object.keys(files).map((file) => [file, file])));
|
||||
|
||||
|
||||
@@ -146,7 +146,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||
}
|
||||
|
||||
const searchPatterns = patterns.map((pattern) => {
|
||||
let escapedPattern = pattern;
|
||||
let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]');
|
||||
if (recursive) {
|
||||
escapedPattern = escapedPattern + '/**';
|
||||
}
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.48.0"
|
||||
constraints = "4.48.0"
|
||||
version = "4.52.0"
|
||||
constraints = "4.52.0"
|
||||
hashes = [
|
||||
"h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
|
||||
"h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
|
||||
"h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
|
||||
"h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
|
||||
"h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
|
||||
"h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
|
||||
"h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
|
||||
"h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
|
||||
"h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
|
||||
"h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
|
||||
"h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
|
||||
"h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
|
||||
"h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
|
||||
"h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
|
||||
"zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
|
||||
"zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
|
||||
"zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
|
||||
"zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
|
||||
"zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
|
||||
"zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
|
||||
"zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
|
||||
"zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
|
||||
"zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
|
||||
"zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
|
||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
|
||||
"zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
|
||||
"zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
|
||||
"zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
|
||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.48.0"
|
||||
version = "4.52.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,37 +2,37 @@
|
||||
# Manual edits may be lost in future updates.
|
||||
|
||||
provider "registry.opentofu.org/cloudflare/cloudflare" {
|
||||
version = "4.48.0"
|
||||
constraints = "4.48.0"
|
||||
version = "4.52.0"
|
||||
constraints = "4.52.0"
|
||||
hashes = [
|
||||
"h1:0IKUOR32xEI1suS5QCOjfxjQ2mRd058btXk8hVnaOJ4=",
|
||||
"h1:3YG6vu/bFPcYOeLdSUZhiAWiWKaFlOAR34z2o8cbE9k=",
|
||||
"h1:FvGy06/i9AMtVkSIUnCrXNv5xF6jqBqMH8oPVLyeeAg=",
|
||||
"h1:GXH7nIF0ocMqebbA41+fSGIYfM+VAM/PvTe7fJr8UrQ=",
|
||||
"h1:H0ll0ph4404vFE868W3qJ3zhOyy4jbXrOMtdkViEZsU=",
|
||||
"h1:SX42e3k73IcFcrQlZ2e/Veqt2tvCMy6fwlo5yNUktCE=",
|
||||
"h1:Uu/gjBc99GefdPdSrlBwU75DWU0ZcwGcrd3ZFyTeL0s=",
|
||||
"h1:VZw0uN41PWRmNlhg7Ze0Eh7cdoklX1oZbfNAXNYnU1I=",
|
||||
"h1:cMdV7ql6PsFa4qtb0EoZSctvTaTqV7yplBSDwcLRCLc=",
|
||||
"h1:ePGvSurmlqOCkD761vkhRmz7bsK36/EnIvx2Xy8TdXo=",
|
||||
"h1:fOYufF+1bzw2N3aHLpkLB6E8VbZ4ysXDODYQOlwhwd4=",
|
||||
"h1:qe8RbnWq0T4xhqjn9QcbO6YW5YDx47P+eJ0NUMIfwCc=",
|
||||
"h1:tRD2av6PafHDP/b9jDQsG5/aX+lHeKxpbIEHYYLBVUc=",
|
||||
"h1:zyl6Gvx/CFpwYW8pFFDesfO8Lxv+a6CopyAsIMhp54s=",
|
||||
"zh:04c0a49c2b23140b2f21cfd0d52f9798d70d3bdae3831613e156aabe519bbc6c",
|
||||
"zh:185f21b4834ba63e8df1f84aa34639d8a7e126429a4007bb5f9ad82f2602a997",
|
||||
"zh:234724f52cb4c0c3f7313d3b2697caef26d921d134f26ae14801e7afac522f7b",
|
||||
"zh:38a56fcd1b3e40706af995611c977816543b53f1e55fe2720944aae2b6828fcb",
|
||||
"zh:419938f5430fc78eff933470aefbf94a460a478f867cf7761a3dea177b4eb153",
|
||||
"zh:4b46d92bfde1deab7de7ba1a6bbf4ba7c711e4fd925341ddf09d4cc28dae03d8",
|
||||
"zh:537acd4a31c752f1bae305ba7190f60b71ad1a459f22d464f3f914336c9e919f",
|
||||
"zh:5ff36b005aad07697dd0b30d4f0c35dbcdc30dc52b41722552060792fa87ce04",
|
||||
"zh:635c5ee419daea098060f794d9d7d999275301181e49562c4e4c08f043076937",
|
||||
"zh:859277c330d61f91abe9e799389467ca11b77131bf34bedbef52f8da68b2bb49",
|
||||
"h1:2BEJyXJtYC4B4nda/WCYUmuJYDaYk88F8t1pwPzr0iQ=",
|
||||
"h1:4IASk5SESeWKQ7JU0+M7KApuF5mZyklvwMXPBabim3c=",
|
||||
"h1:5ImZxxALSnWfH/4EXw/wFirSmk5Tr0ACmcysy51AafE=",
|
||||
"h1:6TJ3dxLSin4ZKBJLsZDn95H2ZYnGm8S7GGHvvXuuMQU=",
|
||||
"h1:IzTUjg9kQ4N3qizP9CjYLeHwjsuGgtxwXvfUQWyOLcA=",
|
||||
"h1:NTaOQfYINA0YTG/V1/9+SYtgX1it63+cBugj4WK4FWc=",
|
||||
"h1:PXH48LuJn329sCfMXprdMDk51EZaWFyajVvS03qhQLs=",
|
||||
"h1:Pi5M+GeoMSN2eJ6QnIeXjBf19O+rby/74CfB2ocpv20=",
|
||||
"h1:ShXZ2ZjBvm3thfoPPzPT8+OhyismnydQVkUAfI8X12w=",
|
||||
"h1:WQ9hu0Wge2msBbODfottCSKgu8oKUrw4Opz+fDPVVHk=",
|
||||
"h1:Z5yXML2DE0uH9UU+M0ut9JMQAORcwVZz1CxBHzeBmao=",
|
||||
"h1:jqI2qKknpleS3JDSplyGYHMu0u9K/tor1ZOjFwDgEMk=",
|
||||
"h1:kgfutDh14Q5nw4eg6qGFamFxIiY8Ae0FPKRBLDOzpcI=",
|
||||
"h1:zCAO7GZmfYhWb+i6TfqlqhMeDyPZWGio2IzEzAh3YTs=",
|
||||
"zh:19be1a91c982b902c42aba47766860dfa5dc151eed1e95fd39ca642229381ef0",
|
||||
"zh:1de451c4d1ecf7efbe67b6dace3426ba810711afdd644b0f1b870364c8ae91f8",
|
||||
"zh:352b4a2120173298622e669258744554339d959ac3a95607b117a48ee4a83238",
|
||||
"zh:3c6f1346d9154afbd2d558fabb4b0150fc8d559aa961254144fe1bc17fe6032f",
|
||||
"zh:4c4c92d53fb535b1e0eff26f222bbd627b97d3b4c891ec9c321268676d06152f",
|
||||
"zh:53276f68006c9ceb7cdb10a6ccf91a5c1eadd1407a28edb5741e84e88d7e29e8",
|
||||
"zh:7925a97773948171a63d4f65bb81ee92fd6d07a447e36012977313293a5435c9",
|
||||
"zh:7dfb0a4496cfe032437386d0a2cd9229a1956e9c30bd920923c141b0f0440060",
|
||||
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
|
||||
"zh:927dfdb8d9aef37ead03fceaa29e87ba076a3dd24e19b6cefdbb0efe9987ff8c",
|
||||
"zh:bbf2226f07f6b1e721877328e69ded4b64f9c196634d2e2429e3cfabbe41e532",
|
||||
"zh:daeed873d6f38604232b46ee4a5830c85d195b967f8dbcafe2fcffa98daf9c5f",
|
||||
"zh:f8f2fc4646c1ba44085612fa7f4dbb7cbcead43b4e661f2b98ddfb4f68afc758",
|
||||
"zh:8d4aa79f0a414bb4163d771063c70cd991c8fac6c766e685bac2ee12903c5bd6",
|
||||
"zh:a67540c13565616a7e7e51ee9366e88b0dc60046e1d75c72680e150bd02725bb",
|
||||
"zh:a936383a4767f5393f38f622e92bf2d0c03fe04b69c284951f27345766c7b31b",
|
||||
"zh:d4887d73c466ff036eecf50ad6404ba38fd82ea4855296b1846d244b0f13c380",
|
||||
"zh:e9093c8bd5b6cd99c81666e315197791781b8f93afa14fc2e0f732d1bb2a44b7",
|
||||
"zh:efd3b3f1ec59a37f635aa1d4efcf178734c2fcf8ddb0d56ea690bec342da8672",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ terraform {
|
||||
required_providers {
|
||||
cloudflare = {
|
||||
source = "cloudflare/cloudflare"
|
||||
version = "4.48.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/troubleshooting
|
||||
|
||||
@@ -71,6 +80,7 @@ services:
|
||||
- ../web:/usr/src/app
|
||||
- ../i18n:/usr/src/i18n
|
||||
- ../open-api/:/usr/src/open-api/
|
||||
# - ../../ui:/usr/ui
|
||||
- /usr/src/app/node_modules
|
||||
ulimits:
|
||||
nofile:
|
||||
@@ -106,7 +116,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
#
|
||||
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
|
||||
name: immich-prod
|
||||
|
||||
services:
|
||||
@@ -47,7 +56,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
@@ -91,7 +100,7 @@ services:
|
||||
container_name: immich_prometheus
|
||||
ports:
|
||||
- 9090:9090
|
||||
image: prom/prometheus@sha256:565ee86501224ebbb98fc10b332fa54440b100469924003359edf49cbce374bd
|
||||
image: prom/prometheus@sha256:5888c188cf09e3f7eebc97369c3b2ce713e844cdbd88ccf36f5047c958aea120
|
||||
volumes:
|
||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||
- prometheus-data:/prometheus
|
||||
@@ -103,7 +112,7 @@ services:
|
||||
command: ['./run.sh', '-disable-reporting']
|
||||
ports:
|
||||
- 3000:3000
|
||||
image: grafana/grafana:11.4.0-ubuntu@sha256:afccec22ba0e4815cca1d2bf3836e414322390dc78d77f1851976ffa8d61051c
|
||||
image: grafana/grafana:11.5.1-ubuntu@sha256:9a4ab78cec1a2ec7d1ca5dfd5aacec6412706a1bc9e971fc7184e2f6696a63f5
|
||||
volumes:
|
||||
- grafana-data:/var/lib/grafana
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#
|
||||
# WARNING: Make sure to use the docker-compose.yml of the current release:
|
||||
# WARNING: To install Immich, follow our guide: https://immich.app/docs/install/docker-compose
|
||||
#
|
||||
# Make sure to use the docker-compose.yml of the current release:
|
||||
#
|
||||
# https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||
#
|
||||
# The compose file on main may not be compatible with the latest release.
|
||||
#
|
||||
|
||||
name: immich
|
||||
|
||||
@@ -48,7 +49,7 @@ services:
|
||||
|
||||
redis:
|
||||
container_name: immich_redis
|
||||
image: docker.io/redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
|
||||
image: docker.io/redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
healthcheck:
|
||||
test: redis-cli ping || exit 1
|
||||
restart: always
|
||||
|
||||
@@ -48,6 +48,7 @@ services:
|
||||
vaapi-wsl: # use this for VAAPI if you're running Immich in WSL2
|
||||
devices:
|
||||
- /dev/dri:/dev/dri
|
||||
- /dev/dxg:/dev/dxg
|
||||
volumes:
|
||||
- /usr/lib/wsl:/usr/lib/wsl
|
||||
environment:
|
||||
|
||||
@@ -1 +1 @@
|
||||
22.12.0
|
||||
22.14.0
|
||||
|
||||
@@ -53,10 +53,18 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca
|
||||
- Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich.
|
||||
- Use the Immich app more often.
|
||||
|
||||
### Why are features not working with a self-signed cert or mTLS?
|
||||
### Why are features in the mobile app not working with a self-signed certificate, Basic Auth, custom headers, or mutual TLS?
|
||||
|
||||
Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background).
|
||||
We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/).
|
||||
These network features are experimental. They often do not work with video playback, asset upload or download, and other features.
|
||||
Many of these limitations are tracked in [#15230](https://github.com/immich-app/immich/issues/15230).
|
||||
Instead of these experimental features, we recommend using the URL switching feature, a VPN, or a [free trusted SSL certificate](https://letsencrypt.org/) for your domain.
|
||||
|
||||
We are not actively developing these features and will not be able to provide support, but welcome contributions to improve them.
|
||||
Please discuss any large PRs with our dev team to ensure your time is not wasted.
|
||||
|
||||
### Why isn't the mobile app updated yet?
|
||||
|
||||
The app stores can take a few days to approve new builds of the app. If you're impatient, android APKs can be downloaded from the GitHub releases.
|
||||
|
||||
---
|
||||
|
||||
@@ -156,6 +164,35 @@ For example, say you have existing transcodes with the policy "Videos higher tha
|
||||
|
||||
No. Our design principle is that the original assets should always be untouched.
|
||||
|
||||
### How can I mount a CIFS/Samba volume within Docker?
|
||||
|
||||
If you aren't able to or prefer not to mount Samba on the host (such as Windows environment), you can mount the volume within Docker.
|
||||
Below is an example in the `docker-compose.yml`.
|
||||
|
||||
Change your username, password, local IP, and share name, and see below where the line `- originals:/usr/src/app/originals`,
|
||||
corrolates to the section where the volume `originals` was created. You can call this whatever you like, and map it to the docker container as you like.
|
||||
For example you could change `originals:` to `Photos:`, and change `- originals:/usr/src/app/originals` to `Photos:/usr/src/app/photos`.
|
||||
|
||||
```diff
|
||||
...
|
||||
services:
|
||||
immich-server:
|
||||
...
|
||||
volumes:
|
||||
# Do not edit the next line. If you want to change the media storage location on your system, edit the value of UPLOAD_LOCATION in the .env file
|
||||
- ${UPLOAD_LOCATION}:/usr/src/app/upload
|
||||
- /etc/localtime:/etc/localtime:ro
|
||||
+ - originals:/usr/src/app/originals
|
||||
...
|
||||
volumes:
|
||||
model-cache:
|
||||
+ originals:
|
||||
+ driver_opts:
|
||||
+ type: cifs
|
||||
+ o: 'iocharset=utf8,username=USERNAMEHERE,password=PASSWORDHERE,rw' # change to `ro` if read only desired
|
||||
+ device: '//localipaddress/sharename'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Albums
|
||||
@@ -278,7 +315,7 @@ The initial backup is the most intensive due to the number of jobs running. The
|
||||
- For facial recognition on new images to work properly, You must re-run the Face Detection job for all images after this.
|
||||
- At the container level, you can [set resource constraints](/docs/FAQ#can-i-limit-cpu-and-ram-usage) to lower usage further.
|
||||
- It's recommended to only apply these constraints _after_ taking some of the measures here for best performance.
|
||||
- If these changes are not enough, see [below](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
|
||||
- If these changes are not enough, see [above](/docs/FAQ#how-can-i-disable-machine-learning) for instructions on how to disable machine learning.
|
||||
|
||||
### Can I limit CPU and RAM usage?
|
||||
|
||||
@@ -421,7 +458,7 @@ A result of `on` means that checksums are enabled.
|
||||
<summary>Check if checksums are enabled</summary>
|
||||
|
||||
```bash
|
||||
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="show data_checksums"
|
||||
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="show data_checksums"
|
||||
data_checksums
|
||||
----------------
|
||||
on
|
||||
@@ -436,7 +473,7 @@ If checksums are enabled, you can check the status of the database with the foll
|
||||
<summary>Check for database corruption</summary>
|
||||
|
||||
```bash
|
||||
docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
|
||||
docker exec -it immich_postgres psql --dbname=postgres --username=<DB_USERNAME> --command="SELECT datname, checksum_failures, checksum_last_failure FROM pg_stat_database WHERE datname IS NOT NULL"
|
||||
datname | checksum_failures | checksum_last_failure
|
||||
-----------+-------------------+-----------------------
|
||||
postgres | 0 |
|
||||
|
||||
@@ -55,7 +55,7 @@ sleep 10 # Wait for Postgres server to start up
|
||||
# Check the database user if you deviated from the default
|
||||
gunzip < "/path/to/backup/dump.sql.gz" \
|
||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
||||
| docker exec -i immich_postgres psql --username=postgres # Restore Backup
|
||||
| docker exec -i immich_postgres psql --dbname=postgres --username=<DB_USERNAME> # Restore Backup
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
@@ -70,18 +70,16 @@ docker compose up -d # Start remainder of Immich apps
|
||||
docker compose down -v # CAUTION! Deletes all Immich data to start from scratch
|
||||
## Uncomment the next line and replace DB_DATA_LOCATION with your Postgres path to permanently reset the Postgres database
|
||||
# Remove-Item -Recurse -Force DB_DATA_LOCATION # CAUTION! Deletes all Immich data to start from scratch
|
||||
## You should mount the backup (as a volume, example: - 'C:\path\to\backup\dump.sql':/dump.sql) into the immich_postgres container using the docker-compose.yml
|
||||
docker compose pull # Update to latest version of Immich (if desired)
|
||||
docker compose create # Create Docker containers for Immich apps without running them
|
||||
docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||
# Check the database user if you deviated from the default
|
||||
cat "/dump.sql" \
|
||||
| sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" \
|
||||
| psql --username=postgres # Restore Backup
|
||||
exit # Exit the Docker shell
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
## You should mount the backup (as a volume, example: `- 'C:\path\to\backup\dump.sql:/dump.sql'`) into the immich_postgres container using the docker-compose.yml
|
||||
docker compose pull # Update to latest version of Immich (if desired)
|
||||
docker compose create # Create Docker containers for Immich apps without running them
|
||||
docker start immich_postgres # Start Postgres server
|
||||
sleep 10 # Wait for Postgres server to start up
|
||||
docker exec -it immich_postgres bash # Enter the Docker shell and run the following command
|
||||
# Check the database user if you deviated from the default. If your backup ends in `.gz`, replace `cat` with `gunzip`
|
||||
cat < "/dump.sql" | sed "s/SELECT pg_catalog.set_config('search_path', '', false);/SELECT pg_catalog.set_config('search_path', 'public, pg_catalog', true);/g" | psql --dbname=postgres --username=<DB_USERNAME>
|
||||
exit # Exit the Docker shell
|
||||
docker compose up -d # Start remainder of Immich apps
|
||||
```
|
||||
|
||||
</TabItem>
|
||||
@@ -95,12 +93,14 @@ Some deployment methods make it difficult to start the database without also sta
|
||||
|
||||
## Filesystem
|
||||
|
||||
Immich stores two types of content in the filesystem: (1) original, unmodified assets (photos and videos), and (2) generated content. Only the original content needs to be backed-up, which is stored in the following folders:
|
||||
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
|
||||
|
||||
1. `UPLOAD_LOCATION/library`
|
||||
2. `UPLOAD_LOCATION/upload`
|
||||
3. `UPLOAD_LOCATION/profile`
|
||||
|
||||
If you choose to back up only those folders, you will need to rerun the transcoding and thumbnail generation jobs for all assets after you restore from a backup.
|
||||
|
||||
:::caution
|
||||
If you moved some of these folders onto a different storage device, such as `profile/`, make sure to adjust the backup path to match your setup
|
||||
:::
|
||||
|
||||
@@ -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>;`.
|
||||
|
||||
[vectors-install]: https://docs.pgvecto.rs/getting-started/installation.html
|
||||
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
|
||||
|
||||
@@ -50,19 +50,18 @@ The Immich CLI is an [npm](https://www.npmjs.com/) package that lets users contr
|
||||
|
||||
The Immich backend is divided into several services, which are run as individual docker containers.
|
||||
|
||||
1. `immich-server` - Handle and respond to REST API requests
|
||||
1. `immich-microservices` - Execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
||||
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
||||
1. `immich-machine-learning` - Execute machine learning models
|
||||
1. `postgres` - Persistent data storage
|
||||
1. `redis`- Queue management for `immich-microservices`
|
||||
1. `redis`- Queue management for background jobs
|
||||
|
||||
### Immich Server
|
||||
|
||||
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, with [TypeORM](https://typeorm.io/) for database management. The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`infra/`) from core business logic (`domain/`).
|
||||
The Immich Server is a [TypeScript](https://www.typescriptlang.org/) project written for [Node.js](https://nodejs.org/). It uses the [Nest.js](https://nestjs.com) framework, [Express](https://expressjs.com/) server, and the query builder [Kysely](https://kysely.dev/). The server codebase also loosely follows the [Hexagonal Architecture](<https://en.wikipedia.org/wiki/Hexagonal_architecture_(software)>). Specifically, we aim to separate technology specific implementations (`src/repositories`) from core business logic (`src/services`).
|
||||
|
||||
#### REST Endpoints
|
||||
### API Endpoints
|
||||
|
||||
The server is a list of HTTP endpoints and associated handlers (controllers). Each controller usually implements the following CRUD operations:
|
||||
An incoming HTTP request is mapped to a controller (`src/controllers`). Controllers are collections of HTTP endpoints. Each controller usually implements the following CRUD operations for its respective resource type:
|
||||
|
||||
- `POST` `/<type>` - **Create**
|
||||
- `GET` `/<type>` - **Read** (all)
|
||||
@@ -70,13 +69,13 @@ The server is a list of HTTP endpoints and associated handlers (controllers). Ea
|
||||
- `PUT` `/<type>/:id` - **Updated** (by id)
|
||||
- `DELETE` `/<type>/:id` - **Delete** (by id)
|
||||
|
||||
#### DTOs
|
||||
### Domain Transfer Objects (DTOs)
|
||||
|
||||
The server uses [Domain Transfer Objects](https://en.wikipedia.org/wiki/Data_transfer_object) as public interfaces for the inputs (query, params, and body) and outputs (response) for each endpoint. DTOs translate to [OpenAPI](./open-api.md) schemas and control the generated code used by each client.
|
||||
|
||||
### Microservices
|
||||
### Background Jobs
|
||||
|
||||
The Immich Microservices image uses the same `Dockerfile` as the Immich Server, but with a different entrypoint. The Immich Microservices service mainly handles executing jobs, which include the following:
|
||||
Immich uses a [worker](https://github.com/immich-app/immich/blob/main/server/src/utils/misc.ts#L266) to run background jobs. These jobs include:
|
||||
|
||||
- Thumbnail Generation
|
||||
- Metadata Extraction
|
||||
|
||||
@@ -63,9 +63,20 @@ If you only want to do web development connected to an existing, remote backend,
|
||||
IMMICH_SERVER_URL=https://demo.immich.app/ npm run dev
|
||||
```
|
||||
|
||||
#### `@immich/ui`
|
||||
|
||||
To see local changes to `@immich/ui` in Immich, do the following:
|
||||
|
||||
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
|
||||
1. Build the `@immich/ui` project via `npm run build`
|
||||
1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
||||
1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
||||
1. Start up the stack via `make dev`
|
||||
1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
||||
|
||||
### Mobile app
|
||||
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x to be installed on your system.
|
||||
The mobile app `(/mobile)` will required Flutter toolchain 3.13.x and FVM to be installed on your system.
|
||||
|
||||
Please refer to the [Flutter's official documentation](https://flutter.dev/docs/get-started/install) for more information on setting up the toolchain on your machine.
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ If your photos are on a network drive, automatic file watching likely won't work
|
||||
|
||||
#### Troubleshooting
|
||||
|
||||
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watched` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
||||
If you encounter an `ENOSPC` error, you need to increase your file watcher limit. In sysctl, this key is called `fs.inotify.max_user_watches` and has a default value of 8192. Increase this number to a suitable value greater than the number of files you will be watching. Note that Immich has to watch all files in your import paths including any ignored files.
|
||||
|
||||
```
|
||||
ERROR [LibraryService] Library watcher for library c69faf55-f96d-4aa0-b83b-2d80cbc27d98 encountered error: Error: ENOSPC: System limit for number of file watchers reached, watch '/media/photo.jpg'
|
||||
|
||||
@@ -11,7 +11,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
- ARM NN (Mali)
|
||||
- CUDA (NVIDIA GPUs with [compute capability](https://developer.nvidia.com/cuda-gpus) 5.2 or higher)
|
||||
- OpenVINO (Intel discrete GPUs such as Iris Xe and Arc)
|
||||
- OpenVINO (Intel GPUs such as Iris Xe and Arc)
|
||||
|
||||
## Limitations
|
||||
|
||||
@@ -43,8 +43,9 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
||||
|
||||
#### OpenVINO
|
||||
|
||||
- The server must have a discrete GPU, i.e. Iris Xe or Arc. Expect issues when attempting to use integrated graphics.
|
||||
- Integrated GPUs are more likely to experience issues than discrete GPUs, especially for older processors or servers with low RAM.
|
||||
- Ensure the server's kernel version is new enough to use the device for hardware accceleration.
|
||||
- Expect higher RAM usage when using OpenVINO compared to CPU processing.
|
||||
|
||||
## Setup
|
||||
|
||||
|
||||
@@ -36,11 +36,15 @@ You can enable automatic backup on supported devices. For more information see [
|
||||
If you have a large number of photos on the device, and you would prefer not to backup all the photos, then it might be prudent to only backup selected photos from device to the Immich server.
|
||||
|
||||
First, you need to enable the Storage Indicator in your app's settings. Navigate to **<ins>Settings -> Photo Grid</ins>** and enable **"Show Storage indicator on asset tiles"**; this makes it easy to distinguish local-only assets and synced assets.
|
||||
|
||||
:::note
|
||||
|
||||
This will enable a small cloud icon on the bottom right corner of the asset tile, indicating that the asset is synced to the server:
|
||||
|
||||
1. <Icon path={mdiCloudOffOutline} size={1} /> - Local-only asset; not synced to the server
|
||||
2. <Icon path={mdiCloudCheckOutline} size={1} /> - Asset is synced to the server :::
|
||||
2. <Icon path={mdiCloudCheckOutline} size={1} /> - Asset is synced to the server
|
||||
|
||||
:::
|
||||
|
||||
Now make sure that the local album is selected in the backup screen (steps 1-2 above). You can find these albums listed in **<ins>Library -> On this device</ins>**. To selectively upload photos from these albums, simply select the local-only photos and tap on "Upload" button in the dynamic bottom menu.
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ After bringing down the containers with `docker compose down` and back up with `
|
||||
:::note
|
||||
To see exactly what metrics are made available, you can additionally add `8081:8081` to the server container's ports and `8082:8082` to the microservices container's ports.
|
||||
Visiting the `/metrics` endpoint for these services will show the same raw data that Prometheus collects.
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](../install/environment-variables/#general).
|
||||
To configure these ports see [`IMMICH_API_METRICS_PORT` & `IMMICH_MICROSERVICES_METRICS_PORT`](/docs/install/environment-variables/#general).
|
||||
:::
|
||||
|
||||
### Usage
|
||||
|
||||
@@ -8,22 +8,23 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
||||
|
||||
## Image formats
|
||||
|
||||
| Format | Extension(s) | Supported? | Notes |
|
||||
| :-------- | :---------------------------- | :----------------: | :-------------- |
|
||||
| `AVIF` | `.avif` | :white_check_mark: | |
|
||||
| `BMP` | `.bmp` | :white_check_mark: | |
|
||||
| `GIF` | `.gif` | :white_check_mark: | |
|
||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||
| `PNG` | `.webp` | :white_check_mark: | |
|
||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||
| `RAW` | `.raw` | :white_check_mark: | |
|
||||
| `RW2` | `.rw2` | :white_check_mark: | |
|
||||
| `SVG` | `.svg` | :white_check_mark: | |
|
||||
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
|
||||
| `WEBP` | `.webp` | :white_check_mark: | |
|
||||
| Format | Extension(s) | Supported? | Notes |
|
||||
| :---------- | :---------------------------- | :----------------: | :-------------- |
|
||||
| `AVIF` | `.avif` | :white_check_mark: | |
|
||||
| `BMP` | `.bmp` | :white_check_mark: | |
|
||||
| `GIF` | `.gif` | :white_check_mark: | |
|
||||
| `HEIC` | `.heic` | :white_check_mark: | |
|
||||
| `HEIF` | `.heif` | :white_check_mark: | |
|
||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||
| `JPEG` | `.webp` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||
| `PNG` | `.webp` | :white_check_mark: | |
|
||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||
| `RAW` | `.raw` | :white_check_mark: | |
|
||||
| `RW2` | `.rw2` | :white_check_mark: | |
|
||||
| `SVG` | `.svg` | :white_check_mark: | |
|
||||
| `TIFF` | `.tif` `.tiff` | :white_check_mark: | |
|
||||
| `WEBP` | `.webp` | :white_check_mark: | |
|
||||
|
||||
## Video formats
|
||||
|
||||
|
||||
@@ -49,5 +49,3 @@ The `thumbs/` folder contains both the small thumbnails displayed in the timelin
|
||||
|
||||
The storage metrics of the Immich server will track available storage at `UPLOAD_LOCATION`, so the administrator must set up some sort of monitoring to ensure the storage does not run out of space. The `profile/` folder is much smaller, usually less than 1 MB.
|
||||
:::
|
||||
|
||||
Thanks to [Jrasm91](https://github.com/immich-app/immich/discussions/2110#discussioncomment-5477767) for writing the guide.
|
||||
|
||||
@@ -5,9 +5,9 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo
|
||||
:::
|
||||
|
||||
:::tip
|
||||
Run `docker exec -it immich_postgres psql --dbname=immich --username=<DB_USERNAME>` to connect to the database via the container directly.
|
||||
Run `docker exec -it immich_postgres psql --dbname=<DB_DATABASE_NAME> --username=<DB_USERNAME>` to connect to the database via the container directly.
|
||||
|
||||
(Replace `<DB_USERNAME>` with the value from your [`.env` file](/docs/install/environment-variables#database)).
|
||||
(Replace `<DB_DATABASE_NAME>` and `<DB_USERNAME>` with the values from your [`.env` file](/docs/install/environment-variables#database)).
|
||||
:::
|
||||
|
||||
## Assets
|
||||
@@ -27,6 +27,10 @@ SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09
|
||||
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
|
||||
```
|
||||
|
||||
```sql title="Find by ID"
|
||||
SELECT * FROM "assets" WHERE "id" = '9f94e60f-65b6-47b7-ae44-a4df7b57f0e9';
|
||||
```
|
||||
|
||||
:::note
|
||||
You can calculate the checksum for a particular file by using the command `sha1sum <filename>`.
|
||||
:::
|
||||
|
||||
@@ -148,24 +148,26 @@ Redis (Sentinel) URL example JSON before encoding:
|
||||
|
||||
## Machine Learning
|
||||
|
||||
| Variable | Description | Default | Containers |
|
||||
| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||
| Variable | Description | Default | Containers |
|
||||
| :---------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------: | :--------------- |
|
||||
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
|
||||
| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
|
||||
| `MACHINE_LEARNING_REQUEST_THREADS`<sup>\*1</sup> | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKERS`<sup>\*2</sup> | Number of worker processes to spawn | `1` | machine learning |
|
||||
| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`<sup>\*3</sup> | HTTP Keep-alive time in seconds | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO) | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__TEXTUAL` | Comma-separated list of (textual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__CLIP__VISUAL` | Comma-separated list of (visual) CLIP model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__RECOGNITION` | Comma-separated list of (recognition) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION__DETECTION` | Comma-separated list of (detection) facial recognition model(s) to preload and cache | | machine learning |
|
||||
| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
|
||||
| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
|
||||
| `MACHINE_LEARNING_DEVICE_IDS`<sup>\*4</sup> | Device IDs to use in multi-GPU environments | `0` | machine learning |
|
||||
| `MACHINE_LEARNING_MAX_BATCH_SIZE__FACIAL_RECOGNITION` | Set the maximum number of faces that will be processed at once by the facial recognition model | None (`1` if using OpenVINO) | machine learning |
|
||||
|
||||
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ The script will perform the following actions:
|
||||
1. Download [docker-compose.yml](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml), and the [.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file from the main branch of the [repository](https://github.com/immich-app/immich).
|
||||
2. Start the containers.
|
||||
|
||||
The web application will be available at `http://<machine-ip-address>:2283`, and the server URL for the mobile app will be `http://<machine-ip-address>:2283/api`
|
||||
The web application and mobile app will be available at `http://<machine-ip-address>:2283`
|
||||
|
||||
The directory which is used to store the library files is `./immich-app` relative to the current directory.
|
||||
|
||||
|
||||
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
|
||||
The **pgData** dataset must be owned by the user `netdata` (UID 999) for postgres to start. The other datasets must be owned by the user `root` (UID 0) or a group that includes the user `root` (UID 0) for immich to have the necessary permissions.
|
||||
|
||||
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
|
||||
If the **library** dataset uses ACL it must have [ACL mode](https://www.truenas.com/docs/core/coretutorials/storage/pools/permissions/#access-control-lists) set to `Passthrough` if you plan on using a [storage template](/docs/administration/storage-template.mdx) and the dataset is configured for network sharing (its ACL type is set to `SMB/NFSv4`). When the template is applied and files need to be moved from **upload** to **library**, Immich performs `chmod` internally and needs to be allowed to execute the command. [More info.](https://github.com/immich-app/immich/pull/13017)
|
||||
:::
|
||||
|
||||
## Installing the Immich Application
|
||||
@@ -160,6 +160,10 @@ The image above has example values.
|
||||
|
||||
### Additional Storage [(External Libraries)](/docs/features/libraries)
|
||||
|
||||
:::danger Advanced Users Only
|
||||
This feature should only be used by advanced users. If this is your first time installing Immich, then DO NOT mount an external library until you have a working setup. Also, your mount path MUST be something unique and should NOT be your library or upload location or a Linux directory like `/lib`. The picture below shows a valid example.
|
||||
:::
|
||||
|
||||
<img
|
||||
src={require('./img/truenas10.webp').default}
|
||||
width="40%"
|
||||
@@ -168,7 +172,7 @@ className="border rounded-xl"
|
||||
/>
|
||||
|
||||
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
|
||||
The **Mount Path** is the loaction you will need to copy and paste into the External Library settings within Immich.
|
||||
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
|
||||
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
|
||||
|
||||
<!-- A section for Labels would go here but I don't know what they do. -->
|
||||
|
||||
@@ -72,7 +72,7 @@ alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
||||
</ul>
|
||||
</details>
|
||||
|
||||
5. Click "**Save Changes**", you will be promoted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||
|
||||
@@ -111,7 +111,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||
|
||||
<details >
|
||||
<summary>Using the FolderView plugin for organizing your Docker containers? Click me! Otherwise you're complete!</summary>
|
||||
<p>If you are using the FolderView plugin go the Docker tab and select "<b>New Folder</b>".<br />Label it <i>"Immich"</i> and use this URL as the logo: https://raw.githubusercontent.com/immich-app/immich/main/design/immich-logo.webp<br/>Then simply select all the Immich related containers before clicking "<b>Submit</b>"</p>
|
||||
<p>If you are using the FolderView plugin go the Docker tab and select "<b>New Folder</b>".<br />Label it <i>"Immich"</i> and use this URL as the logo: https://raw.githubusercontent.com/immich-app/immich/main/design/immich-logo.png<br/>Then simply select all the Immich related containers before clicking "<b>Submit</b>"</p>
|
||||
<img
|
||||
src={require('./img/unraid07.webp').default}
|
||||
width="80%"
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -110,9 +110,9 @@ const config = {
|
||||
label: 'API',
|
||||
},
|
||||
{
|
||||
to: '/blog',
|
||||
href: 'https://immich.store',
|
||||
position: 'right',
|
||||
label: 'Blog',
|
||||
label: 'Merch',
|
||||
},
|
||||
{
|
||||
href: 'https://github.com/immich-app/immich',
|
||||
|
||||
6627
docs/package-lock.json
generated
6627
docs/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -16,8 +16,8 @@
|
||||
"write-heading-ids": "docusaurus write-heading-ids"
|
||||
},
|
||||
"dependencies": {
|
||||
"@docusaurus/core": "~3.5.2",
|
||||
"@docusaurus/preset-classic": "~3.5.2",
|
||||
"@docusaurus/core": "~3.7.0",
|
||||
"@docusaurus/preset-classic": "~3.7.0",
|
||||
"@mdi/js": "^7.3.67",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mdx-js/react": "^3.0.0",
|
||||
@@ -35,7 +35,7 @@
|
||||
"url": "^0.11.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@docusaurus/module-type-aliases": "~3.5.2",
|
||||
"@docusaurus/module-type-aliases": "~3.7.0",
|
||||
"prettier": "^3.2.4",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
@@ -55,6 +55,6 @@
|
||||
"node": ">=20"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.12.0"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,6 +99,11 @@ const projects: CommunityProjectProps[] = [
|
||||
description: 'Downloads a configurable number of random photos based on people or album ID.',
|
||||
url: 'https://github.com/jon6fingrs/immich-dl',
|
||||
},
|
||||
{
|
||||
title: 'Immich Upload Optimizer',
|
||||
description: 'Automatically optimize files uploaded to Immich in order to save storage space',
|
||||
url: 'https://github.com/miguelangel-nubla/immich-upload-optimizer',
|
||||
},
|
||||
];
|
||||
|
||||
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
|
||||
|
||||
@@ -44,12 +44,12 @@ export default function VersionSwitcher(): JSX.Element {
|
||||
return (
|
||||
versions.length > 0 && (
|
||||
<DropdownNavbarItem
|
||||
className="navbar__item"
|
||||
className="version-switcher-34ab39"
|
||||
label={label}
|
||||
mobile={windowSize === 'mobile'}
|
||||
items={versions.map(({ label, url }) => ({
|
||||
label,
|
||||
to: url,
|
||||
to: url + location.pathname + location.hash,
|
||||
target: '_self',
|
||||
}))}
|
||||
/>
|
||||
|
||||
@@ -75,6 +75,11 @@ div[class^='announcementBar_'] {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* workaround for version switcher PR 15894 */
|
||||
div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
code {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
@@ -50,6 +50,13 @@ function HomepageHeader() {
|
||||
>
|
||||
Demo
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||
to="https://immich.store"
|
||||
>
|
||||
Buy Merch
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
||||
@@ -73,9 +80,9 @@ function HomepageHeader() {
|
||||
/>
|
||||
|
||||
<div>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download mobile app</p>
|
||||
<p className="font-bold text-2xl md:text-5xl ">Download the mobile app</p>
|
||||
<p className="text-lg">
|
||||
Download Immich app and start backing up your photos and videos securely to your own server
|
||||
Download the Immich app and start backing up your photos and videos securely to your own server
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-4 gap-1">
|
||||
|
||||
40
docs/static/archived-versions.json
vendored
40
docs/static/archived-versions.json
vendored
@@ -1,4 +1,44 @@
|
||||
[
|
||||
{
|
||||
"label": "v1.126.1",
|
||||
"url": "https://v1.126.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.126.0",
|
||||
"url": "https://v1.126.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.7",
|
||||
"url": "https://v1.125.7.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.6",
|
||||
"url": "https://v1.125.6.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.5",
|
||||
"url": "https://v1.125.5.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.4",
|
||||
"url": "https://v1.125.4.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.3",
|
||||
"url": "https://v1.125.3.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.2",
|
||||
"url": "https://v1.125.2.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.1",
|
||||
"url": "https://v1.125.1.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.125.0",
|
||||
"url": "https://v1.125.0.archive.immich.app"
|
||||
},
|
||||
{
|
||||
"label": "v1.124.2",
|
||||
"url": "https://v1.124.2.archive.immich.app"
|
||||
|
||||
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 |
@@ -1 +1 @@
|
||||
22.12.0
|
||||
22.14.0
|
||||
|
||||
@@ -34,7 +34,7 @@ services:
|
||||
- 2285:2285
|
||||
|
||||
redis:
|
||||
image: redis:6.2-alpine@sha256:eaba718fecd1196d88533de7ba49bf903ad33664a92debb24660a922ecd9cac8
|
||||
image: redis:6.2-alpine@sha256:148bb5411c184abd288d9aaed139c98123eeb8824c5d3fce03cf721db58066d8
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
|
||||
1151
e2e/package-lock.json
generated
1151
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich-e2e",
|
||||
"version": "1.124.2",
|
||||
"version": "1.126.1",
|
||||
"description": "",
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
@@ -25,16 +25,16 @@
|
||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||
"@playwright/test": "^1.44.1",
|
||||
"@types/luxon": "^3.4.2",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/node": "^22.13.2",
|
||||
"@types/oidc-provider": "^8.5.1",
|
||||
"@types/pg": "^8.11.0",
|
||||
"@types/pngjs": "^6.0.4",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"@typescript-eslint/eslint-plugin": "^8.15.0",
|
||||
"@typescript-eslint/parser": "^8.15.0",
|
||||
"@vitest/coverage-v8": "^2.0.5",
|
||||
"@vitest/coverage-v8": "^3.0.0",
|
||||
"eslint": "^9.14.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-config-prettier": "^10.0.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^56.0.1",
|
||||
"exiftool-vendored": "^28.3.1",
|
||||
@@ -50,9 +50,9 @@
|
||||
"supertest": "^7.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1",
|
||||
"vitest": "^2.0.5"
|
||||
"vitest": "^3.0.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.12.0"
|
||||
"node": "22.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,79 +22,92 @@ const user1NotShared = 'user1NotShared';
|
||||
const user2SharedUser = 'user2SharedUser';
|
||||
const user2SharedLink = 'user2SharedLink';
|
||||
const user2NotShared = 'user2NotShared';
|
||||
const user4DeletedAsset = 'user4DeletedAsset';
|
||||
const user4Empty = 'user4Empty';
|
||||
|
||||
describe('/albums', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user1: LoginResponseDto;
|
||||
let user1Asset1: AssetMediaResponseDto;
|
||||
let user1Asset2: AssetMediaResponseDto;
|
||||
let user4Asset1: AssetMediaResponseDto;
|
||||
let user1Albums: AlbumResponseDto[];
|
||||
let user2: LoginResponseDto;
|
||||
let user2Albums: AlbumResponseDto[];
|
||||
let deletedAssetAlbum: AlbumResponseDto;
|
||||
let user3: LoginResponseDto; // deleted
|
||||
let user4: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
|
||||
admin = await utils.adminSetup();
|
||||
|
||||
[user1, user2, user3] = await Promise.all([
|
||||
[user1, user2, user3, user4] = await Promise.all([
|
||||
utils.userSetup(admin.accessToken, createUserDto.user1),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user2),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user3),
|
||||
utils.userSetup(admin.accessToken, createUserDto.user4),
|
||||
]);
|
||||
|
||||
[user1Asset1, user1Asset2] = await Promise.all([
|
||||
[user1Asset1, user1Asset2, user4Asset1] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken, { isFavorite: true }),
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
user1Albums = await Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Editor }],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
[user1Albums, user2Albums, deletedAssetAlbum] = await Promise.all([
|
||||
Promise.all([
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedEditorUser,
|
||||
albumUsers: [
|
||||
{ userId: admin.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user2.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedLink,
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1NotShared,
|
||||
assetIds: [user1Asset1.id, user1Asset2.id],
|
||||
}),
|
||||
utils.createAlbum(user1.accessToken, {
|
||||
albumName: user1SharedViewerUser,
|
||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||
assetIds: [user1Asset1.id],
|
||||
}),
|
||||
]),
|
||||
Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4DeletedAsset }),
|
||||
utils.createAlbum(user4.accessToken, { albumName: user4Empty }),
|
||||
utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
}),
|
||||
]);
|
||||
|
||||
user2Albums = await Promise.all([
|
||||
utils.createAlbum(user2.accessToken, {
|
||||
albumName: user2SharedUser,
|
||||
albumUsers: [
|
||||
{ userId: user1.userId, role: AlbumUserRole.Editor },
|
||||
{ userId: user3.userId, role: AlbumUserRole.Editor },
|
||||
],
|
||||
}),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2SharedLink }),
|
||||
utils.createAlbum(user2.accessToken, { albumName: user2NotShared }),
|
||||
]);
|
||||
|
||||
await utils.createAlbum(user3.accessToken, {
|
||||
albumName: 'Deleted',
|
||||
albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }],
|
||||
});
|
||||
|
||||
await addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
);
|
||||
|
||||
user2Albums[0] = await getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) });
|
||||
|
||||
await Promise.all([
|
||||
addAssetsToAlbum(
|
||||
{ id: user2Albums[0].id, bulkIdsDto: { ids: [user1Asset1.id, user1Asset2.id] } },
|
||||
{ headers: asBearerAuth(user1.accessToken) },
|
||||
),
|
||||
addAssetsToAlbum(
|
||||
{ id: deletedAssetAlbum.id, bulkIdsDto: { ids: [user4Asset1.id] } },
|
||||
{ headers: asBearerAuth(user4.accessToken) },
|
||||
),
|
||||
// add shared link to user1SharedLink album
|
||||
utils.createSharedLink(user1.accessToken, {
|
||||
type: SharedLinkType.Album,
|
||||
@@ -107,7 +120,11 @@ describe('/albums', () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
await deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||
[user2Albums[0]] = await Promise.all([
|
||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
utils.deleteAssets(user1.accessToken, [user4Asset1.id]),
|
||||
]);
|
||||
});
|
||||
|
||||
describe('GET /albums', () => {
|
||||
@@ -142,6 +159,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ isFavorite: false })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
shared: true,
|
||||
albumUsers: expect.any(Array),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -280,6 +301,25 @@ describe('/albums', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(5);
|
||||
});
|
||||
|
||||
it('should return empty albums and albums where all assets are deleted', async () => {
|
||||
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user4.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4DeletedAsset,
|
||||
shared: false,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
ownerId: user4.userId,
|
||||
albumName: user4Empty,
|
||||
shared: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /albums/:id', () => {
|
||||
@@ -299,6 +339,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -330,6 +374,10 @@ describe('/albums', () => {
|
||||
...user1Albums[0],
|
||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -344,6 +392,30 @@ describe('/albums', () => {
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not count trashed assets', async () => {
|
||||
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual({
|
||||
...user2Albums[0],
|
||||
assets: [],
|
||||
assetCount: 1,
|
||||
lastModifiedAssetTimestamp: expect.any(String),
|
||||
endDate: expect.any(String),
|
||||
startDate: expect.any(String),
|
||||
albumUsers: expect.any(Array),
|
||||
shared: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,11 +3,11 @@ import {
|
||||
AssetMediaStatus,
|
||||
AssetResponseDto,
|
||||
AssetTypeEnum,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
getAssetInfo,
|
||||
getConfig,
|
||||
getMyUser,
|
||||
LoginResponseDto,
|
||||
SharedLinkType,
|
||||
updateConfig,
|
||||
} from '@immich/sdk';
|
||||
import { exiftool } from 'exiftool-vendored';
|
||||
@@ -19,7 +19,7 @@ import { Socket } from 'socket.io-client';
|
||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||
import { makeRandomImage } from 'src/generators';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -41,8 +41,6 @@ const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||
return dto;
|
||||
};
|
||||
|
||||
const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
||||
@@ -538,7 +536,7 @@ describe('/asset', () => {
|
||||
expect(body).toMatchObject({
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
||||
dateTimeOriginal: '2023-11-20T01:11:00+00:00',
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
@@ -608,7 +606,7 @@ describe('/asset', () => {
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const assetInfo = await utils.getAssetInfo(user1.accessToken, id);
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBe('2024-07-11T10:32:52.000Z');
|
||||
expect(assetInfo.exifInfo?.dateTimeOriginal).toBe('2024-07-11T10:32:52+00:00');
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${id}`)
|
||||
@@ -618,7 +616,7 @@ describe('/asset', () => {
|
||||
expect(body).toMatchObject({
|
||||
id,
|
||||
exifInfo: expect.objectContaining({
|
||||
dateTimeOriginal: '2023-11-20T01:11:00.000Z',
|
||||
dateTimeOriginal: '2023-11-20T01:11:00+00:00',
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
@@ -703,6 +701,20 @@ describe('/asset', () => {
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should set the negative rating', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/assets/${user1Assets[0].id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
.send({ rating: -1 });
|
||||
expect(body).toMatchObject({
|
||||
id: user1Assets[0].id,
|
||||
exifInfo: expect.objectContaining({
|
||||
rating: -1,
|
||||
}),
|
||||
});
|
||||
expect(status).toEqual(200);
|
||||
});
|
||||
|
||||
it('should reject invalid rating', async () => {
|
||||
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||
const { status, body } = await request(app)
|
||||
@@ -985,8 +997,6 @@ describe('/asset', () => {
|
||||
exifImageHeight: 1080,
|
||||
exifImageWidth: 1617,
|
||||
fileSizeInByte: 862_424,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -996,11 +1006,9 @@ describe('/asset', () => {
|
||||
type: AssetTypeEnum.Image,
|
||||
originalFileName: 'el_torcal_rocks.jpg',
|
||||
exifInfo: {
|
||||
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
||||
dateTimeOriginal: '2012-08-05T11:39:59+00:00',
|
||||
exifImageWidth: 512,
|
||||
exifImageHeight: 341,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
focalLength: 75,
|
||||
iso: 200,
|
||||
fNumber: 11,
|
||||
@@ -1008,7 +1016,6 @@ describe('/asset', () => {
|
||||
fileSizeInByte: 53_493,
|
||||
make: 'SONY',
|
||||
model: 'DSLR-A550',
|
||||
orientation: null,
|
||||
description: 'SONY DSC',
|
||||
},
|
||||
},
|
||||
@@ -1023,8 +1030,6 @@ describe('/asset', () => {
|
||||
exifImageHeight: 1080,
|
||||
exifImageWidth: 1440,
|
||||
fileSizeInByte: 1_780_777,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1035,7 +1040,7 @@ describe('/asset', () => {
|
||||
originalFileName: 'IMG_2682.heic',
|
||||
fileCreatedAt: '2019-03-21T16:04:22.348Z',
|
||||
exifInfo: {
|
||||
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
|
||||
dateTimeOriginal: '2019-03-21T16:04:22.348+00:00',
|
||||
exifImageWidth: 4032,
|
||||
exifImageHeight: 3024,
|
||||
latitude: 41.2203,
|
||||
@@ -1060,8 +1065,6 @@ describe('/asset', () => {
|
||||
exifInfo: {
|
||||
exifImageWidth: 800,
|
||||
exifImageHeight: 800,
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
fileSizeInByte: 25_408,
|
||||
},
|
||||
},
|
||||
@@ -1080,9 +1083,7 @@ describe('/asset', () => {
|
||||
focalLength: 18,
|
||||
iso: 100,
|
||||
fileSizeInByte: 9_057_784,
|
||||
dateTimeOriginal: '2010-07-20T17:27:12.000Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
dateTimeOriginal: '2010-07-20T17:27:12+00:00',
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
@@ -1101,9 +1102,7 @@ describe('/asset', () => {
|
||||
focalLength: 85,
|
||||
iso: 200,
|
||||
fileSizeInByte: 15_856_335,
|
||||
dateTimeOriginal: '2016-09-22T21:10:29.060Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
dateTimeOriginal: '2016-09-22T21:10:29.06+00:00',
|
||||
orientation: '1',
|
||||
timeZone: 'UTC-4',
|
||||
},
|
||||
@@ -1125,9 +1124,7 @@ describe('/asset', () => {
|
||||
focalLength: 35,
|
||||
iso: 400,
|
||||
fileSizeInByte: 19_587_072,
|
||||
dateTimeOriginal: '2018-05-10T08:42:37.842Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
dateTimeOriginal: '2018-05-10T08:42:37.842+00:00',
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
@@ -1149,9 +1146,7 @@ describe('/asset', () => {
|
||||
iso: 100,
|
||||
lensModel: 'E PZ 18-105mm F4 G OSS',
|
||||
fileSizeInByte: 25_001_984,
|
||||
dateTimeOriginal: '2016-09-27T10:51:44.000Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
dateTimeOriginal: '2016-09-27T10:51:44+00:00',
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
@@ -1173,9 +1168,7 @@ describe('/asset', () => {
|
||||
iso: 100,
|
||||
lensModel: 'E 25mm F2',
|
||||
fileSizeInByte: 49_512_448,
|
||||
dateTimeOriginal: '2016-01-08T14:08:01.000Z',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
dateTimeOriginal: '2016-01-08T14:08:01+00:00',
|
||||
orientation: '1',
|
||||
},
|
||||
},
|
||||
@@ -1197,7 +1190,7 @@ describe('/asset', () => {
|
||||
iso: 80,
|
||||
lensModel: null,
|
||||
fileSizeInByte: 11_113_617,
|
||||
dateTimeOriginal: '2015-12-27T09:55:40.000Z',
|
||||
dateTimeOriginal: '2015-12-27T09:55:40+00:00',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
orientation: '1',
|
||||
@@ -1221,7 +1214,7 @@ describe('/asset', () => {
|
||||
iso: 160,
|
||||
lensModel: null,
|
||||
fileSizeInByte: 13_551_312,
|
||||
dateTimeOriginal: '2024-10-12T21:01:01.000Z',
|
||||
dateTimeOriginal: '2024-10-12T21:01:01+00:00',
|
||||
latitude: null,
|
||||
longitude: null,
|
||||
orientation: '6',
|
||||
@@ -1235,7 +1228,7 @@ describe('/asset', () => {
|
||||
originalFileName: 'Ricoh_GR3-450.DNG',
|
||||
fileCreatedAt: '2024-06-08T13:48:39.000Z',
|
||||
exifInfo: {
|
||||
dateTimeOriginal: '2024-06-08T13:48:39.000Z',
|
||||
dateTimeOriginal: '2024-06-08T13:48:39+00:00',
|
||||
exifImageHeight: 4064,
|
||||
exifImageWidth: 6112,
|
||||
exposureTime: '1/400',
|
||||
|
||||
86
e2e/src/api/specs/jobs.e2e-spec.ts
Normal file
86
e2e/src/api/specs/jobs.e2e-spec.ts
Normal file
@@ -0,0 +1,86 @@
|
||||
import { JobCommand, JobName, LoginResponseDto } from '@immich/sdk';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { basename } from 'node:path';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterEach, beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
describe('/jobs', () => {
|
||||
let admin: LoginResponseDto;
|
||||
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup({ onboarding: false });
|
||||
});
|
||||
|
||||
describe('PUT /jobs', () => {
|
||||
afterEach(async () => {
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).put('/jobs/metadataExtraction');
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it('should queue metadata extraction for missing assets', async () => {
|
||||
const path1 = `${testAssetDir}/formats/raw/Nikon/D700/philadelphia.nef`;
|
||||
const path2 = `${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`;
|
||||
|
||||
await utils.createAsset(admin.accessToken, {
|
||||
assetData: { bytes: await readFile(path1), filename: basename(path1) },
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Pause,
|
||||
force: false,
|
||||
});
|
||||
|
||||
const { id } = await utils.createAsset(admin.accessToken, {
|
||||
assetData: { bytes: await readFile(path2), filename: basename(path2) },
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
{
|
||||
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||
|
||||
expect(asset.exifInfo).toBeDefined();
|
||||
expect(asset.exifInfo?.make).toBeNull();
|
||||
}
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Empty,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Resume,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||
command: JobCommand.Start,
|
||||
force: false,
|
||||
});
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
{
|
||||
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||
|
||||
expect(asset.exifInfo).toBeDefined();
|
||||
expect(asset.exifInfo?.make).toBe('NIKON CORPORATION');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries, scanLibrary } from '@immich/sdk';
|
||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { userDto, uuidDto } from 'src/fixtures';
|
||||
@@ -8,8 +8,6 @@ import request from 'supertest';
|
||||
import { utimes } from 'utimes';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
const scan = async (accessToken: string, id: string) => scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
describe('/libraries', () => {
|
||||
let admin: LoginResponseDto;
|
||||
let user: LoginResponseDto;
|
||||
@@ -298,6 +296,8 @@ describe('/libraries', () => {
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
@@ -312,15 +312,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
originalPath: `${testAssetDirInternal}/temp/directoryA/assetA.png`,
|
||||
@@ -340,13 +332,7 @@ describe('/libraries', () => {
|
||||
exclusionPatterns: ['**/directoryA'],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -360,13 +346,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/directoryA`, `${testAssetDirInternal}/temp/directoryB`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -385,13 +365,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder, a/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder, b/assetB.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -413,13 +387,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder{ a/assetA.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder} b/assetB.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -471,13 +439,7 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}1/asset1.png`);
|
||||
utils.createImageFile(`${testAssetDir}/temp/folder${char}2/asset2.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -501,23 +463,12 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_001);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -548,21 +499,12 @@ describe('/libraries', () => {
|
||||
utils.createImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/albums/nature/tanners_ridge.jpg`, `${testAssetDir}/temp/reimport/asset.jpg`);
|
||||
await utimes(`${testAssetDir}/temp/reimport/asset.jpg`, 447_775_200_000);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -592,21 +534,14 @@ describe('/libraries', () => {
|
||||
|
||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assets.count).toBe(1);
|
||||
|
||||
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||
@@ -624,8 +559,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assets.count).toBe(1);
|
||||
@@ -636,13 +570,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
|
||||
});
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
||||
@@ -662,8 +590,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
||||
libraryId: library.id,
|
||||
@@ -673,8 +600,7 @@ describe('/libraries', () => {
|
||||
|
||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(trashedAsset.isTrashed).toBe(true);
|
||||
@@ -696,19 +622,12 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
||||
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -725,11 +644,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -752,10 +667,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -779,10 +691,7 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -806,19 +715,13 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -841,18 +744,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -875,18 +772,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -910,19 +801,13 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -946,18 +831,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -981,18 +860,12 @@ describe('/libraries', () => {
|
||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
@@ -1015,22 +888,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
expect(offlineAsset.isTrashed).toBe(true);
|
||||
@@ -1044,15 +908,7 @@ describe('/libraries', () => {
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1074,22 +930,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
{
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||
@@ -1110,15 +957,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/another-path`],
|
||||
});
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1142,22 +981,13 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
||||
|
||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
{
|
||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
||||
@@ -1174,15 +1004,7 @@ describe('/libraries', () => {
|
||||
|
||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
||||
|
||||
{
|
||||
const { status } = await request(app)
|
||||
.post(`/libraries/${library.id}/scan`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send();
|
||||
expect(status).toBe(204);
|
||||
}
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
||||
|
||||
@@ -1302,8 +1124,7 @@ describe('/libraries', () => {
|
||||
importPaths: [`${testAssetDirInternal}/temp`],
|
||||
});
|
||||
|
||||
await scan(admin.accessToken, library.id);
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'library');
|
||||
await utils.scan(admin.accessToken, library.id);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.delete(`/libraries/${library.id}`)
|
||||
|
||||
@@ -93,8 +93,6 @@ describe('/memories', () => {
|
||||
data: { year: 2021 },
|
||||
createdAt: expect.any(String),
|
||||
updatedAt: expect.any(String),
|
||||
deletedAt: null,
|
||||
seenAt: null,
|
||||
isSaved: false,
|
||||
memoryAt: expect.any(String),
|
||||
ownerId: user.userId,
|
||||
|
||||
@@ -13,8 +13,8 @@ import request from 'supertest';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
|
||||
const authServer = {
|
||||
internal: 'http://auth-server:3000',
|
||||
external: 'http://127.0.0.1:3000',
|
||||
internal: 'http://auth-server:2286',
|
||||
external: 'http://127.0.0.1:2286',
|
||||
};
|
||||
|
||||
const mobileOverrideRedirectUri = 'https://photos.immich.app/oauth/mobile-redirect';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||
import { getPerson, LoginResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||
import { uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||
|
||||
@@ -195,12 +195,29 @@ describe('/people', () => {
|
||||
.send({
|
||||
name: 'New Person',
|
||||
birthDate: '1990-01-01',
|
||||
color: '#333',
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'New Person',
|
||||
birthDate: '1990-01-01',
|
||||
birthDate: '1990-01-01T00:00:00.000Z',
|
||||
});
|
||||
});
|
||||
|
||||
it('should create a favorite person', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post(`/people`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({
|
||||
name: 'New Favorite Person',
|
||||
isFavorite: true,
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
expect(body).toMatchObject({
|
||||
id: expect.any(String),
|
||||
name: 'New Favorite Person',
|
||||
isFavorite: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -216,6 +233,7 @@ describe('/people', () => {
|
||||
{ key: 'name', type: 'string' },
|
||||
{ key: 'featureFaceAssetId', type: 'string' },
|
||||
{ key: 'isHidden', type: 'boolean value' },
|
||||
{ key: 'isFavorite', type: 'boolean value' },
|
||||
]) {
|
||||
it(`should not allow null ${key}`, async () => {
|
||||
const { status, body } = await request(app)
|
||||
@@ -244,7 +262,7 @@ describe('/people', () => {
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ birthDate: '1990-01-01' });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01' });
|
||||
expect(body).toMatchObject({ birthDate: '1990-01-01T00:00:00.000Z' });
|
||||
});
|
||||
|
||||
it('should clear a date of birth', async () => {
|
||||
@@ -255,6 +273,42 @@ describe('/people', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ birthDate: null });
|
||||
});
|
||||
|
||||
it('should set a color', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/people/${visiblePerson.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ color: '#555' });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ color: '#555' });
|
||||
});
|
||||
|
||||
it('should clear a color', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.put(`/people/${visiblePerson.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ color: null });
|
||||
expect(status).toBe(200);
|
||||
expect(body.color).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should mark a person as favorite', async () => {
|
||||
const person = await utils.createPerson(admin.accessToken, {
|
||||
name: 'visible_person',
|
||||
});
|
||||
|
||||
expect(person.isFavorite).toBe(false);
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.put(`/people/${person.id}`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||
.send({ isFavorite: true });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toMatchObject({ isFavorite: true });
|
||||
|
||||
const person2 = await getPerson({ id: person.id }, { headers: asBearerAuth(admin.accessToken) });
|
||||
expect(person2).toMatchObject({ id: person.id, isFavorite: true });
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /people/:id/merge', () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, updateAsset } from '@immich/sdk';
|
||||
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
|
||||
import { DateTime } from 'luxon';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { Socket } from 'socket.io-client';
|
||||
import { errorDto } from 'src/responses';
|
||||
import { app, asBearerAuth, testAssetDir, utils } from 'src/utils';
|
||||
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||
import request from 'supertest';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
const today = DateTime.now();
|
||||
@@ -462,6 +462,55 @@ describe('/search', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /search/random', () => {
|
||||
beforeAll(async () => {
|
||||
await Promise.all([
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
utils.createAsset(admin.accessToken),
|
||||
]);
|
||||
|
||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||
});
|
||||
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
|
||||
|
||||
expect(status).toBe(401);
|
||||
expect(body).toEqual(errorDto.unauthorized);
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/search/random')
|
||||
.send({ size: 1 })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(1);
|
||||
expect(assets[0].ownerId).toBe(admin.userId);
|
||||
});
|
||||
|
||||
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/search/random')
|
||||
.send({ size: 2 })
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
|
||||
const assets: AssetResponseDto[] = body;
|
||||
expect(assets.length).toBe(2);
|
||||
expect(assets[0].ownerId).toBe(admin.userId);
|
||||
expect(assets[1].ownerId).toBe(admin.userId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /search/explore', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/search/explore');
|
||||
|
||||
@@ -89,7 +89,7 @@ describe('/shared-links', () => {
|
||||
await deleteUserAdmin({ id: user2.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||
});
|
||||
|
||||
describe('GET /share/${key}', () => {
|
||||
describe('GET /share/:key', () => {
|
||||
it('should have correct asset count in meta tag for non-empty album', async () => {
|
||||
const resp = await request(shareUrl).get(`/${linkWithMetadata.key}`);
|
||||
expect(resp.status).toBe(200);
|
||||
@@ -139,7 +139,10 @@ describe('/shared-links', () => {
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: linkWithAlbum.id }),
|
||||
expect.objectContaining({ id: linkWithAssets.id }),
|
||||
expect.objectContaining({
|
||||
id: linkWithAssets.id,
|
||||
assets: expect.arrayContaining([expect.objectContaining({ id: asset1.id })]),
|
||||
}),
|
||||
expect.objectContaining({ id: linkWithPassword.id }),
|
||||
expect.objectContaining({ id: linkWithMetadata.id }),
|
||||
expect.objectContaining({ id: linkWithoutMetadata.id }),
|
||||
@@ -147,6 +150,30 @@ describe('/shared-links', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter on albumId', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/shared-links?albumId=${album.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(2);
|
||||
expect(body).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({ id: linkWithAlbum.id }),
|
||||
expect.objectContaining({ id: linkWithPassword.id }),
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should find 0 albums', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get(`/shared-links?albumId=${uuidDto.notFound}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should not get shared links created by other users', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/shared-links')
|
||||
@@ -170,13 +197,19 @@ describe('/shared-links', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
album,
|
||||
album: expect.objectContaining({ id: album.id }),
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should increment the view count', async () => {
|
||||
const request1 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
const request2 = await request(app).get('/shared-links/me').query({ key: linkWithAlbum.key });
|
||||
expect(request2.body.viewCount).toBe(request1.body.viewCount + 1);
|
||||
});
|
||||
|
||||
it('should return unauthorized for incorrect shared link', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/shared-links/me')
|
||||
@@ -208,7 +241,7 @@ describe('/shared-links', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
album,
|
||||
album: expect.objectContaining({ id: album.id }),
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
@@ -262,7 +295,7 @@ describe('/shared-links', () => {
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
album,
|
||||
album: expect.objectContaining({ id: album.id }),
|
||||
userId: user1.userId,
|
||||
type: SharedLinkType.Album,
|
||||
}),
|
||||
|
||||
@@ -119,93 +119,84 @@ describe('/stacks', () => {
|
||||
const stacksAfter = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) });
|
||||
expect(stacksAfter.length).toBe(stacksBefore.length);
|
||||
});
|
||||
|
||||
// it('should require a valid parent id', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
// .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID']));
|
||||
// });
|
||||
});
|
||||
|
||||
// it('should require access to the parent', async () => {
|
||||
// const { status, body } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${user1.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
|
||||
describe('GET /assets/:id', () => {
|
||||
it('should include stack details for the primary asset', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// expect(status).toBe(400);
|
||||
// expect(body).toEqual(errorDto.noPermission);
|
||||
// });
|
||||
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
|
||||
|
||||
// it('should add stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
|
||||
const { status, body } = await request(app)
|
||||
.get(`/assets/${asset1.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// expect(status).toBe(204);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: asset1.id,
|
||||
stack: {
|
||||
id: expect.any(String),
|
||||
assetCount: 2,
|
||||
primaryAssetId: asset1.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })]));
|
||||
// });
|
||||
it('should include stack details for a non-primary asset', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// it('should remove stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ removeParent: true, ids: [stackAssets[1].id] });
|
||||
await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
|
||||
|
||||
// expect(status).toBe(204);
|
||||
const { status, body } = await request(app)
|
||||
.get(`/assets/${asset2.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(
|
||||
// expect.arrayContaining([
|
||||
// expect.objectContaining({ id: stackAssets[2].id }),
|
||||
// expect.objectContaining({ id: stackAssets[3].id }),
|
||||
// ]),
|
||||
// );
|
||||
// });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: asset2.id,
|
||||
stack: {
|
||||
id: expect.any(String),
|
||||
assetCount: 2,
|
||||
primaryAssetId: asset1.id,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
// it('should remove all stack children', async () => {
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
|
||||
describe('GET /stacks/:id', () => {
|
||||
it('should include exifInfo in stack assets', async () => {
|
||||
const [asset1, asset2] = await Promise.all([
|
||||
utils.createAsset(user1.accessToken),
|
||||
utils.createAsset(user1.accessToken),
|
||||
]);
|
||||
|
||||
// expect(status).toBe(204);
|
||||
const stack = await utils.createStack(user1.accessToken, [asset1.id, asset2.id]);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).toBeUndefined();
|
||||
// });
|
||||
const { status, body } = await request(app)
|
||||
.get(`/stacks/${stack.id}`)
|
||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||
|
||||
// it('should merge stack children', async () => {
|
||||
// // create stack after previous test removed stack children
|
||||
// await updateAssets(
|
||||
// { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } },
|
||||
// { headers: asBearerAuth(stackUser.accessToken) },
|
||||
// );
|
||||
|
||||
// const { status } = await request(app)
|
||||
// .put('/assets')
|
||||
// .set('Authorization', `Bearer ${stackUser.accessToken}`)
|
||||
// .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
|
||||
|
||||
// expect(status).toBe(204);
|
||||
|
||||
// const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) });
|
||||
// expect(asset.stack).not.toBeUndefined();
|
||||
// expect(asset.stack).toEqual(
|
||||
// expect.arrayContaining([
|
||||
// expect.objectContaining({ id: stackAssets[0].id }),
|
||||
// expect.objectContaining({ id: stackAssets[1].id }),
|
||||
// expect.objectContaining({ id: stackAssets[2].id }),
|
||||
// ]),
|
||||
// );
|
||||
// });
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: stack.id,
|
||||
primaryAssetId: asset1.id,
|
||||
assets: expect.arrayContaining([
|
||||
expect.objectContaining({ id: asset1.id, exifInfo: expect.any(Object) }),
|
||||
expect.objectContaining({ id: asset2.id, exifInfo: expect.any(Object) }),
|
||||
]),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('/timeline', () => {
|
||||
it('should require authentication', async () => {
|
||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||
size: TimeBucketSize.Month,
|
||||
timeBucket: '1900-01-01T00:00:00.000Z',
|
||||
timeBucket: '1900-01-01',
|
||||
});
|
||||
|
||||
expect(status).toBe(401);
|
||||
@@ -161,7 +161,7 @@ describe('/timeline', () => {
|
||||
it('should handle 5 digit years', async () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '+012345-01-01T00:00:00.000Z' })
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||
|
||||
expect(status).toBe(200);
|
||||
@@ -183,7 +183,7 @@ describe('/timeline', () => {
|
||||
const { status, body } = await request(app)
|
||||
.get('/timeline/bucket')
|
||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10T00:00:00.000Z' });
|
||||
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
|
||||
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual([]);
|
||||
|
||||
@@ -356,5 +356,24 @@ describe('/admin/users', () => {
|
||||
expect(status).toBe(403);
|
||||
expect(body).toEqual(errorDto.forbidden);
|
||||
});
|
||||
|
||||
it('should restore a user', async () => {
|
||||
const user = await utils.userSetup(admin.accessToken, createUserDto.create('restore'));
|
||||
|
||||
await deleteUserAdmin({ id: user.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) });
|
||||
|
||||
const { status, body } = await request(app)
|
||||
.post(`/admin/users/${user.userId}/restore`)
|
||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
expect(body).toEqual(
|
||||
expect.objectContaining({
|
||||
id: user.userId,
|
||||
email: user.userEmail,
|
||||
status: 'active',
|
||||
deletedAt: null,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -129,6 +129,8 @@ describe('/users', () => {
|
||||
expect(body).toEqual({
|
||||
...before,
|
||||
updatedAt: expect.any(String),
|
||||
profileChangedAt: expect.any(String),
|
||||
createdAt: expect.any(String),
|
||||
name: 'Name',
|
||||
});
|
||||
});
|
||||
@@ -177,6 +179,8 @@ describe('/users', () => {
|
||||
...before,
|
||||
email: 'non-admin@immich.cloud',
|
||||
updatedAt: expect.anything(),
|
||||
createdAt: expect.anything(),
|
||||
profileChangedAt: expect.anything(),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -51,7 +51,7 @@ const setup = async () => {
|
||||
const { privateKey, publicKey } = await generateKeyPair('RS256');
|
||||
|
||||
const redirectUris = ['http://127.0.0.1:2285/auth/login', 'https://photos.immich.app/oauth/mobile-redirect'];
|
||||
const port = 3000;
|
||||
const port = 2286;
|
||||
const host = '0.0.0.0';
|
||||
const oidc = new Provider(`http://${host}:${port}`, {
|
||||
renderError: async (ctx, out, error) => {
|
||||
|
||||
@@ -6,6 +6,8 @@ import {
|
||||
CheckExistingAssetsDto,
|
||||
CreateAlbumDto,
|
||||
CreateLibraryDto,
|
||||
JobCommandDto,
|
||||
JobName,
|
||||
MetadataSearchDto,
|
||||
Permission,
|
||||
PersonCreateDto,
|
||||
@@ -28,7 +30,9 @@ import {
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
login,
|
||||
scanLibrary,
|
||||
searchAssets,
|
||||
sendJobCommand,
|
||||
setBaseUrl,
|
||||
signUpAdmin,
|
||||
tagAssets,
|
||||
@@ -76,6 +80,7 @@ export const immichCli = (args: string[]) =>
|
||||
export const immichAdmin = (args: string[]) =>
|
||||
executeCommand('docker', ['exec', '-i', 'immich-e2e-server', '/bin/bash', '-c', `immich-admin ${args.join(' ')}`]);
|
||||
export const specialCharStrings = ["'", '"', ',', '{', '}', '*'];
|
||||
export const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
|
||||
const executeCommand = (command: string, args: string[]) => {
|
||||
let _resolve: (value: CommandResponse) => void;
|
||||
@@ -474,6 +479,9 @@ export const utils = {
|
||||
tagAssets: (accessToken: string, tagId: string, assetIds: string[]) =>
|
||||
tagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
jobCommand: async (accessToken: string, jobName: JobName, jobCommandDto: JobCommandDto) =>
|
||||
sendJobCommand({ id: jobName, jobCommandDto }, { headers: asBearerAuth(accessToken) }),
|
||||
|
||||
setAuthCookies: async (context: BrowserContext, accessToken: string, domain = '127.0.0.1') =>
|
||||
await context.addCookies([
|
||||
{
|
||||
@@ -546,6 +554,14 @@ export const utils = {
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
return key.secret;
|
||||
},
|
||||
|
||||
scan: async (accessToken: string, id: string) => {
|
||||
await scanLibrary({ id }, { headers: asBearerAuth(accessToken) });
|
||||
|
||||
await utils.waitForQueueFinish(accessToken, 'library');
|
||||
await utils.waitForQueueFinish(accessToken, 'sidecar');
|
||||
await utils.waitForQueueFinish(accessToken, 'metadataExtraction');
|
||||
},
|
||||
};
|
||||
|
||||
utils.initSdk();
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
SharedLinkType,
|
||||
createAlbum,
|
||||
} from '@immich/sdk';
|
||||
import { test } from '@playwright/test';
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { asBearerAuth, utils } from 'src/utils';
|
||||
|
||||
test.describe('Shared Links', () => {
|
||||
@@ -65,6 +65,38 @@ test.describe('Shared Links', () => {
|
||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||
});
|
||||
|
||||
test('show-password button visible', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
await page.getByPlaceholder('Password').fill('test-password');
|
||||
await page.getByRole('button', { name: 'Show password' }).waitFor();
|
||||
});
|
||||
|
||||
test('view password for shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
const input = page.getByPlaceholder('Password');
|
||||
await input.fill('test-password');
|
||||
await page.getByRole('button', { name: 'Show password' }).click();
|
||||
// await page.getByText('test-password', { exact: true }).waitFor();
|
||||
await expect(input).toHaveAttribute('type', 'text');
|
||||
});
|
||||
|
||||
test('hide-password button visible', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
const input = page.getByPlaceholder('Password');
|
||||
await input.fill('test-password');
|
||||
await page.getByRole('button', { name: 'Show password' }).click();
|
||||
await page.getByRole('button', { name: 'Hide password' }).waitFor();
|
||||
});
|
||||
|
||||
test('hide password for shared link', async ({ page }) => {
|
||||
await page.goto(`/share/${sharedLinkPassword.key}`);
|
||||
const input = page.getByPlaceholder('Password');
|
||||
await input.fill('test-password');
|
||||
await page.getByRole('button', { name: 'Show password' }).click();
|
||||
await page.getByRole('button', { name: 'Hide password' }).click();
|
||||
await expect(input).toHaveAttribute('type', 'password');
|
||||
});
|
||||
|
||||
test('show error for invalid shared link', async ({ page }) => {
|
||||
await page.goto('/share/invalid');
|
||||
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"about": "حول",
|
||||
"about": "من نحن",
|
||||
"account": "الحساب",
|
||||
"account_settings": "إعدادات الحساب",
|
||||
"acknowledge": "أُدرك ذلك",
|
||||
|
||||
22
i18n/bg.json
22
i18n/bg.json
@@ -437,8 +437,8 @@
|
||||
"birthdate_set_description": "Датата на раждане се използва за изчисляване на възрастта на този човек към момента на снимката.",
|
||||
"blurred_background": "Замъглен заден фон",
|
||||
"bugs_and_feature_requests": "Бъгове и заявки за функции",
|
||||
"build": "Създаване",
|
||||
"build_image": "Създаване на изображение",
|
||||
"build": "Версия",
|
||||
"build_image": "Docker версия",
|
||||
"bulk_delete_duplicates_confirmation": "Сигурни ли сте, че искате да изтриете масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще изтрие трайно всички други дубликати. Не можете да отмените това действие!",
|
||||
"bulk_keep_duplicates_confirmation": "Сигурни ли сте, че искате да запазите {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще потвърди всички групи дубликати, без да изтрива нищо.",
|
||||
"bulk_trash_duplicates_confirmation": "Сигурни ли сте, че искате да преместите в кошчето масово {count, plural, one {# дублиран файл} other {# дублирани файла}}? Това ще запази най-големия файл от всяка група и ще премести в кошчето всички други дубликати.",
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Период от време",
|
||||
"day": "Ден",
|
||||
"deduplicate_all": "Дедупликиране на всички",
|
||||
"deduplication_criteria_1": "Размер на снимката в байтове",
|
||||
"deduplication_criteria_2": "Брой EXIF данни",
|
||||
"deduplication_info": "Информация за дедупликацията",
|
||||
"deduplication_info_description": "За автоматично предварително избиране на ресурси и премахване на дубликати на едро, разглеждаме:",
|
||||
"default_locale": "Локализация по подразбиране",
|
||||
"default_locale_description": "Форматиране на дати и числа в зависимост от местоположението на браузъра",
|
||||
"delete": "Изтрий",
|
||||
@@ -669,7 +673,7 @@
|
||||
"unable_to_download_files": "Не могат да се изтеглят файловете",
|
||||
"unable_to_edit_exclusion_pattern": "Не може да се редактира шаблон за изключване",
|
||||
"unable_to_edit_import_path": "Пътят за импортиране не може да се редактира",
|
||||
"unable_to_empty_trash": "Не може да изпразни кошчето",
|
||||
"unable_to_empty_trash": "Неуспешно изпразване на кошчето",
|
||||
"unable_to_enter_fullscreen": "Не може да се отвори в цял екран",
|
||||
"unable_to_exit_fullscreen": "Не може да излезе от цял екран",
|
||||
"unable_to_get_comments_number": "Не може да получи брой коментари",
|
||||
@@ -765,7 +769,7 @@
|
||||
"group_no": "Няма група",
|
||||
"group_owner": "Групиране по собственик",
|
||||
"group_year": "Групиране по година",
|
||||
"has_quota": "Има лимит",
|
||||
"has_quota": "Лимит",
|
||||
"hi_user": "Здравей, {name} {email}",
|
||||
"hide_all_people": "Скрий всички хора",
|
||||
"hide_gallery": "Скрий галерия",
|
||||
@@ -1009,7 +1013,7 @@
|
||||
"purchase_button_select": "Избери",
|
||||
"purchase_failed_activation": "Неуспешна активация! Моля, проверете имейла си за правилния продуктов ключ!",
|
||||
"purchase_individual_description_1": "За индивидуален потребител",
|
||||
"purchase_individual_description_2": "Поддръжнически статус",
|
||||
"purchase_individual_description_2": "Статус на поддръжник",
|
||||
"purchase_individual_title": "Индивидуален",
|
||||
"purchase_input_suggestion": "Имате продуктов ключ? Въведете ключа по-долу",
|
||||
"purchase_license_subtitle": "Закупете Immich, за да подкрепите продължаващото развитие на услугата",
|
||||
@@ -1025,7 +1029,7 @@
|
||||
"purchase_remove_server_product_key": "Премахни продуктовия ключ на сървъра",
|
||||
"purchase_remove_server_product_key_prompt": "Сигурни ли сте, че искате да премахнете продуктовия ключ на сървъра?",
|
||||
"purchase_server_description_1": "За целият сървър",
|
||||
"purchase_server_description_2": "Статус на поддръжника",
|
||||
"purchase_server_description_2": "Статус на поддръжник",
|
||||
"purchase_server_title": "Сървър",
|
||||
"purchase_settings_server_activated": "Продуктовият ключ на сървъра се управлява от администратора",
|
||||
"rating": "Оценка със звезди",
|
||||
@@ -1205,7 +1209,7 @@
|
||||
"sort_people_by_similarity": "Сортиране на хора по прилика",
|
||||
"sort_recent": "Най-новата снимка",
|
||||
"sort_title": "Заглавие",
|
||||
"source": "Източник",
|
||||
"source": "Код",
|
||||
"stack": "Събери",
|
||||
"stack_duplicates": "Подреждане на дубликати",
|
||||
"stack_select_one_photo": "Избери една главна снимка за събраните снимки",
|
||||
@@ -1258,9 +1262,9 @@
|
||||
"toggle_theme": "Превключване на тема",
|
||||
"total": "Общо",
|
||||
"total_usage": "Общо използвано",
|
||||
"trash": "кошче",
|
||||
"trash": "Кошче",
|
||||
"trash_all": "Изхвърли всички",
|
||||
"trash_count": "Кошче {count, number}",
|
||||
"trash_count": "В Кошчето {count, number}",
|
||||
"trash_delete_asset": "Вкарай в Кошчето/Изтрий елемент",
|
||||
"trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.",
|
||||
"trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# ден} other {# дни}}.",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"about": "Sobre",
|
||||
"about": "Quant a",
|
||||
"account": "Compte",
|
||||
"account_settings": "Configuració del compte",
|
||||
"acknowledge": "D'acord",
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Interval de dates",
|
||||
"day": "Dia",
|
||||
"deduplicate_all": "Desduplica-ho tot",
|
||||
"deduplication_criteria_1": "Mida d'imatge en bytes",
|
||||
"deduplication_criteria_2": "Quantitat de dades EXIF",
|
||||
"deduplication_info": "Informació de deduplicació",
|
||||
"deduplication_info_description": "Per preseleccionar recursos automàticament i eliminar els duplicats de manera massiva, ens fixem en:",
|
||||
"default_locale": "Localització predeterminada",
|
||||
"default_locale_description": "Format de dates i números segons la configuració del navegador",
|
||||
"delete": "Esborra",
|
||||
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Rozsah dat",
|
||||
"day": "Den",
|
||||
"deduplicate_all": "Odstranit všechny duplicity",
|
||||
"deduplication_criteria_1": "Velikost obrázku v bajtech",
|
||||
"deduplication_criteria_2": "Počet EXIF dat",
|
||||
"deduplication_info": "Informace o deduplikaci",
|
||||
"deduplication_info_description": "Pro automatický předvýběr položek a hromadné odstranění duplicit se zohledňuje:",
|
||||
"default_locale": "Výchozí jazyk",
|
||||
"default_locale_description": "Formátovat datumy a čísla podle místního prostředí prohlížeče",
|
||||
"delete": "Smazat",
|
||||
@@ -921,7 +925,7 @@
|
||||
"oldest_first": "Nejstarší první",
|
||||
"onboarding": "Zahájení",
|
||||
"onboarding_privacy_description": "Následující (volitelné) funkce jsou závislé na externích službách a lze je kdykoli zakázat v nastavení správy.",
|
||||
"onboarding_theme_description": "Zvolte si barevné téma pro svou instanci. Můžete to později změnit v nastavení.",
|
||||
"onboarding_theme_description": "Zvolte si barevný motiv pro svou instanci. Můžete to později změnit v nastavení.",
|
||||
"onboarding_welcome_description": "Nastavíme vaši instanci pomocí několika běžných nastavení.",
|
||||
"onboarding_welcome_user": "Vítej, {user}",
|
||||
"online": "Online",
|
||||
@@ -1282,7 +1286,7 @@
|
||||
"unselect_all": "Zrušit výběr všech",
|
||||
"unselect_all_duplicates": "Zrušit výběr všech duplicit",
|
||||
"unstack": "Zrušit seskupení",
|
||||
"unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}",
|
||||
"unstacked_assets_count": "{count, plural, one {Rozložená # položka} few {Rozložené # položky} other {Rozložených # položiek}}",
|
||||
"untracked_files": "Nesledované soubory",
|
||||
"untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě",
|
||||
"up_next": "To je prozatím vše",
|
||||
|
||||
13
i18n/cv.json
13
i18n/cv.json
@@ -50,8 +50,16 @@
|
||||
"map_gps_settings_description": "Карттӑпа GPS (каялла геоюмлани) ӗнерленисене йӗркелесе тӑрӑр",
|
||||
"map_settings": "Карттӑ"
|
||||
},
|
||||
"albums": "Албумсем",
|
||||
"albums_count": "{count, plural, one {{count, number} албум} other {{count, number} албумсем}}",
|
||||
"all": "Пурте",
|
||||
"all_albums": "Пурте албумсем",
|
||||
"explore": "Тишкер",
|
||||
"explorer": "Тишкерӳҫӗ",
|
||||
"favorite": "Юратнӑ",
|
||||
"favorite_or_unfavorite_photo": "Юратнӑ е юратман сӑнӳкерчӗк",
|
||||
"favorites": "Юратнисем",
|
||||
"feature_photo_updated": "Уйрӑм сӑнӳкерчӗк ҫӗнетнӗ",
|
||||
"manage_sharing_with_partners": "Партнерсемпе пайланассине йӗркелесе пырӑр",
|
||||
"map": "Карттӑ",
|
||||
"map_marker_for_images": "{city}, {country} ҫинче ӳкернӗ ӳкерчӗксем валли карттӑ маркерӗ",
|
||||
@@ -60,10 +68,15 @@
|
||||
"no_explore_results_message": "Хӑвӑр коллекципе киленмешкӗн сӑнӳкерчӗксем ытларах тийӗр.",
|
||||
"open_in_openstreetmap": "OpenStreetMap-па уҫ",
|
||||
"partner_sharing": "Партнер пайланӑвӗ",
|
||||
"people": "Ҫынсем",
|
||||
"photos": "Сӑнӳкерчӗксем",
|
||||
"photos_and_videos": "Сӑнӳкерчӗксем тете Видеосем",
|
||||
"photos_count": "{count, plural, one {{count, number} Сӑнӳкерчӗк} other {{count, number} Сӑнӳкерчӗксем}}",
|
||||
"photos_from_previous_years": "Иртнӗ ҫулсенчи сӑнӳкерчӗксем",
|
||||
"place": "Тӗл",
|
||||
"places": "Тӗлсем",
|
||||
"play": "Выля",
|
||||
"play_memories": "Асаилӳсем выля",
|
||||
"search_your_photos": "Сӑнӳкерчӗксене шырӑр",
|
||||
"select_photos": "Сӑнӳкерчӗксем суйлӑр",
|
||||
"sharing": "Пайлани",
|
||||
|
||||
10
i18n/da.json
10
i18n/da.json
@@ -523,6 +523,10 @@
|
||||
"date_range": "Datointerval",
|
||||
"day": "Dag",
|
||||
"deduplicate_all": "Dedupliker alle",
|
||||
"deduplication_criteria_1": "Billedstørrelse i bytes",
|
||||
"deduplication_criteria_2": "Antal EXIF-data",
|
||||
"deduplication_info": "Deduplikerings info",
|
||||
"deduplication_info_description": "For automatisk at forudvælge emner og fjerne dubletter i bulk ser vi på:",
|
||||
"default_locale": "Standardlokalitet",
|
||||
"default_locale_description": "Formatér datoer og tal",
|
||||
"delete": "Slet",
|
||||
@@ -644,6 +648,7 @@
|
||||
"unable_to_add_partners": "Ikke i stand til at tilføje partnere",
|
||||
"unable_to_add_remove_archive": "Kan Ikke {archived, select, true {fjerne aktiv fra} other {tilføje aktiv til}} Arkiv",
|
||||
"unable_to_add_remove_favorites": "Kan ikke {favorite, select, true {tilføje aktiv til} other {fjerne aktiv fra}} favoritter",
|
||||
"unable_to_archive_unarchive": "Ude af stand til at {arkiveret, vælg, sand {arkiv} andet {arkiv}}",
|
||||
"unable_to_change_album_user_role": "Ikke i stand til at ændre albumbrugerens rolle",
|
||||
"unable_to_change_date": "Ikke i stand til at ændre dato",
|
||||
"unable_to_change_favorite": "Kan ikke ændre favorit for aktiv",
|
||||
@@ -730,6 +735,7 @@
|
||||
"expired": "Udløbet",
|
||||
"expires_date": "Udløber {date}",
|
||||
"explore": "Udforsk",
|
||||
"explorer": "Udforske",
|
||||
"export": "Eksportér",
|
||||
"export_as_json": "Eksportér som JSON",
|
||||
"extension": "Udvidelse",
|
||||
@@ -917,6 +923,7 @@
|
||||
"offline_paths_description": "Disse resultater kan være på grund af manuel sletning af filer, som ikke er en del af et eksternt bibliotek.",
|
||||
"ok": "Ok",
|
||||
"oldest_first": "Ældste først",
|
||||
"onboarding": "Onboarding",
|
||||
"onboarding_privacy_description": "Følgende (valgfrie) funktioner er afhængige af eksterne tjenester, og kan til enhver tid deaktiveres i administrationsindstillingerne.",
|
||||
"onboarding_theme_description": "Vælg et farvetema til din forekomst. Du kan ændre dette senere i dine indstillinger.",
|
||||
"onboarding_welcome_description": "Lad os få din instans sat op med nogle almindelige indstillinger.",
|
||||
@@ -1249,6 +1256,7 @@
|
||||
"to_change_password": "Skift adgangskode",
|
||||
"to_favorite": "Gør til favorit",
|
||||
"to_login": "Login",
|
||||
"to_parent": "Gå op",
|
||||
"to_trash": "Papirkurv",
|
||||
"toggle_settings": "Slå indstillinger til eller fra",
|
||||
"toggle_theme": "Slå mørkt tema til eller fra",
|
||||
@@ -1334,7 +1342,7 @@
|
||||
"warning": "Advarsel",
|
||||
"week": "Uge",
|
||||
"welcome": "Velkommen",
|
||||
"welcome_to_immich": "Velkommen til immich",
|
||||
"welcome_to_immich": "Velkommen til Immich",
|
||||
"year": "År",
|
||||
"years_ago": "{years, plural, one {# år} other {# år}} siden",
|
||||
"yes": "Ja",
|
||||
|
||||
38
i18n/de.json
38
i18n/de.json
@@ -34,7 +34,7 @@
|
||||
"authentication_settings_description": "Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen verwalten",
|
||||
"authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
|
||||
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
|
||||
"background_task_job": "Hintergrund-Aufgaben",
|
||||
"background_task_job": "Hintergrundaufgaben",
|
||||
"backup_database": "Datenbank sichern",
|
||||
"backup_database_enable_description": "Sicherung der Datenbank aktivieren",
|
||||
"backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
|
||||
@@ -83,9 +83,9 @@
|
||||
"job_concurrency": "{job} (Anzahl gleichzeitiger Prozesse)",
|
||||
"job_created": "Aufgabe erstellt",
|
||||
"job_not_concurrency_safe": "Diese Aufgabe ist nicht parallelisierungssicher.",
|
||||
"job_settings": "Aufgaben-Einstellungen",
|
||||
"job_settings_description": "Gleichzeitige Aufgaben-Prozesse verwalten",
|
||||
"job_status": "Aufgaben-Status",
|
||||
"job_settings": "Aufgabeneinstellungen",
|
||||
"job_settings_description": "Die gleichzeitige Ausführung von Aufgaben verwalten",
|
||||
"job_status": "Aufgabenstatus",
|
||||
"jobs_delayed": "{jobCount, plural, other {# verzögert}}",
|
||||
"jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}",
|
||||
"library_created": "Bibliothek erstellt: {library}",
|
||||
@@ -211,7 +211,7 @@
|
||||
"quota_size_gib": "Kontingent (GiB)",
|
||||
"refreshing_all_libraries": "Alle Bibliotheken aktualisieren",
|
||||
"registration": "Admin-Registrierung",
|
||||
"registration_description": "Da du der erste Benutzer im System bist, wirst du als Admin zugewiesen und bist für administrative Aufgaben zuständig. Weitere Benutzer werden von dir erstellt.",
|
||||
"registration_description": "Da du der erste Benutzer im System bist, wird dir die Rolle des Administrators zugewiesen, womit du für die Verwaltungsaufgaben verantwortlich bist. Weitere Benutzer werden von dir erstellt.",
|
||||
"repair_all": "Alle reparieren",
|
||||
"repair_matched_items": "{count, plural, one {# Eintrag} other {# Einträge}} gefunden",
|
||||
"repaired_items": "{count, plural, one {# Eintrag} other {# Einträge}} repariert",
|
||||
@@ -287,10 +287,10 @@
|
||||
"transcoding_constant_quality_mode": "Modus für konstante Qualität",
|
||||
"transcoding_constant_quality_mode_description": "ICQ ist besser als CQP, aber einige Hardware-Beschleunigungsgeräte unterstützen diesen Modus nicht. Wenn diese Option gesetzt wird, wird der angegebene Modus bevorzugt, sobald qualitätsbasierte Kodierung verwendet wird. Wird von NVENC ignoriert, da es ICQ nicht unterstützt.",
|
||||
"transcoding_constant_rate_factor": "Faktor der konstanten Rate (-crf)",
|
||||
"transcoding_constant_rate_factor_description": "Video-Qualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
||||
"transcoding_constant_rate_factor_description": "Videoqualitätsstufe. Typische Werte sind 23 für H.264, 28 für HEVC, 31 für VP9 und 35 für AV1. Ein niedrigerer Wert ist besser, erzeugt aber größere Dateien.",
|
||||
"transcoding_disabled_description": "Videos nicht transkodieren, dies kann die Wiedergabe auf manchen Geräten beeinträchtigen",
|
||||
"transcoding_encoding_options": "Kodierungsoptionen",
|
||||
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für Kodierte Videos",
|
||||
"transcoding_encoding_options_description": "Setze Codec, Auflösung, Qualität und andere Optionen für kodierte Videos",
|
||||
"transcoding_hardware_acceleration": "Hardware-Beschleunigung",
|
||||
"transcoding_hardware_acceleration_description": "Experimentell; viel schneller, aber bei gleicher Bitrate mit geringerer Qualität",
|
||||
"transcoding_hardware_decoding": "Hardware-Dekodierung",
|
||||
@@ -304,7 +304,7 @@
|
||||
"transcoding_max_keyframe_interval_description": "Legt den maximalen Frame-Abstand zwischen Keyframes fest. Niedrigere Werte verschlechtern die Komprimierungseffizienz, verbessern aber die Suchzeiten und können die Qualität in Szenen mit schnellen Bewegungen verbessern. Bei 0 wird dieser Wert automatisch eingestellt.",
|
||||
"transcoding_optimal_description": "Videos mit einer höheren Auflösung als der Zielauflösung oder in einem nicht akzeptierten Format",
|
||||
"transcoding_policy": "Transkodierungsrichtlinie",
|
||||
"transcoding_policy_description": "Bestimme, wann ein Video Transkodiert wird",
|
||||
"transcoding_policy_description": "Bestimme, wann ein Video transkodiert wird",
|
||||
"transcoding_preferred_hardware_device": "Bevorzugtes Hardwaregerät",
|
||||
"transcoding_preferred_hardware_device_description": "Gilt nur für VAAPI und QSV. Legt den für die Hardware-Transkodierung verwendeten dri-Node fest.",
|
||||
"transcoding_preset_preset": "Voreinstellung (-preset)",
|
||||
@@ -312,14 +312,14 @@
|
||||
"transcoding_reference_frames": "Referenz-Frames",
|
||||
"transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.",
|
||||
"transcoding_required_description": "Nur Videos in einem nicht akzeptierten Format",
|
||||
"transcoding_settings": "Video-Transkodierungseinstellungen",
|
||||
"transcoding_settings_description": "Auflösungs- und Kodierungsinformationen von Videodateien verwalten",
|
||||
"transcoding_settings": "Einstellungen für die Videotranskodierung",
|
||||
"transcoding_settings_description": "Verwalten welche Videos transkodiert werden und wie diese verarbeitet werden",
|
||||
"transcoding_target_resolution": "Ziel-Auflösung",
|
||||
"transcoding_target_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Codierung, haben größere Dateigrößen und können die Reaktionszeit der Anwendung beeinträchtigen.",
|
||||
"transcoding_temporal_aq": "Temporäre AQ",
|
||||
"transcoding_temporal_aq_description": "Gilt nur für NVENC. Verbessert die Qualität von Szenen mit hohem Detailreichtum und geringen Bewegungen. Dies ist möglicherweise nicht mit älteren Geräten kompatibel.",
|
||||
"transcoding_threads": "Threads",
|
||||
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.",
|
||||
"transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Kodierung, lassen dem Server jedoch weniger Spielraum für die Verarbeitung anderer Aufgaben im aktiven Zustand. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Maximiert die Auslastung, wenn der Wert auf 0 gesetzt wird.",
|
||||
"transcoding_tone_mapping": "Farbton-Mapping",
|
||||
"transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.",
|
||||
"transcoding_transcode_policy": "Transcodierungsrichtlinie",
|
||||
@@ -328,11 +328,11 @@
|
||||
"transcoding_two_pass_encoding_setting_description": "Führt eine Transkodierung in zwei Durchgängen durch, um besser kodierte Videos zu erzeugen. Wenn die maximale Bitrate aktiviert ist (erforderlich für die Verwendung mit H.264 und HEVC), verwendet dieser Modus einen Bitratenbereich, der auf der maximalen Bitrate basiert, und ignoriert CRF. Für VP9 kann CRF verwendet werden, wenn die maximale Bitrate deaktiviert ist.",
|
||||
"transcoding_video_codec": "Video-Codec",
|
||||
"transcoding_video_codec_description": "VP9 hat eine hohe Effizienz und Webkompatibilität, braucht aber länger für die Transkodierung. HEVC bietet eine ähnliche Leistung, ist aber weniger web-kompatibel. H.264 ist weitgehend kompatibel und lässt sich schnell transkodieren, erzeugt aber viel größere Dateien. AV1 ist der effizienteste Codec, wird aber von älteren Geräten nicht unterstützt.",
|
||||
"trash_enabled_description": "Papierkorb-Funktionen aktivieren",
|
||||
"trash_enabled_description": "Papierkorbfunktionen aktivieren",
|
||||
"trash_number_of_days": "Anzahl der Tage",
|
||||
"trash_number_of_days_description": "Anzahl der Tage, welche die Objekte im Papierkorb verbleiben, bevor sie endgültig entfernt werden",
|
||||
"trash_settings": "Papierkorb-Einstellungen",
|
||||
"trash_settings_description": "Papierkorb-Einstellungen verwalten",
|
||||
"trash_settings": "Papierkorbeinstellungen",
|
||||
"trash_settings_description": "Papierkorbeinstellungen verwalten",
|
||||
"untracked_files": "Unverfolgte Dateien",
|
||||
"untracked_files_description": "Diese Dateien werden nicht von der Anwendung getrackt. Sie können das Ergebnis fehlgeschlagener Verschiebungen, unterbrochener Uploads oder aufgrund eines Fehlers sein",
|
||||
"user_cleanup_job": "Benutzer aufräumen",
|
||||
@@ -346,8 +346,8 @@
|
||||
"user_password_reset_description": "Bitte gib dem Benutzer das temporäre Passwort und informiere ihn, dass das Passwort beim nächsten Login geändert werden muss.",
|
||||
"user_restore_description": "Das Konto von <b>{user}</b> wird wiederhergestellt.",
|
||||
"user_restore_scheduled_removal": "Wiederherstellung des Benutzers - geplante Entfernung am {date, date, long}",
|
||||
"user_settings": "Benutzer-Einstellungen",
|
||||
"user_settings_description": "Benutzer-Einstellungen verwalten",
|
||||
"user_settings": "Benutzereinstellungen",
|
||||
"user_settings_description": "Benutzereinstellungen verwalten",
|
||||
"user_successfully_removed": "Benutzer {email} wurde erfolgreich entfernt.",
|
||||
"version_check_enabled_description": "Versionsprüfung aktivieren",
|
||||
"version_check_implications": "Die Funktion zur Versionsprüfung basiert auf regelmäßiger Kommunikation mit GitHub.com",
|
||||
@@ -523,10 +523,10 @@
|
||||
"date_range": "Datumsbereich",
|
||||
"day": "Tag",
|
||||
"deduplicate_all": "Alle Duplikate entfernen",
|
||||
"deduplication_info": "Deduplizierungsinformationen",
|
||||
"deduplication_info_description": "Für die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berücksichtigen wir:",
|
||||
"deduplication_criteria_1": "Bildgröße in Bytes",
|
||||
"deduplication_criteria_2": "Anzahl der EXIF-Daten",
|
||||
"deduplication_info": "Deduplizierungsinformationen",
|
||||
"deduplication_info_description": "Für die automatische Datei-Vorauswahl und das Deduplizieren aller Dateien berücksichtigen wir:",
|
||||
"default_locale": "Standard-Sprache",
|
||||
"default_locale_description": "Datumsangaben und Zahlen basierend auf dem Gebietsschema des Browsers formatieren",
|
||||
"delete": "Löschen",
|
||||
@@ -1324,7 +1324,7 @@
|
||||
"version_history_item": "{version} am {date} installiert",
|
||||
"video": "Video",
|
||||
"video_hover_setting": "Videovorschau beim Hovern abspielen",
|
||||
"video_hover_setting_description": "Video-Miniaturansicht wiedergeben, wenn der Mauszeiger über dem Element verweilt. Auch wenn diese Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem der Mauszeiger auf das Wiedergabesymbol bewegt wird.",
|
||||
"video_hover_setting_description": "Spiele die Miniaturansicht des Videos ab, wenn sich die Maus über dem Element befindet. Auch wenn die Funktion deaktiviert ist, kann die Wiedergabe gestartet werden, indem du mit der Maus über das Wiedergabesymbol fährst.",
|
||||
"videos": "Videos",
|
||||
"videos_count": "{count, plural, one {# Video} other {# Videos}}",
|
||||
"view": "Ansicht",
|
||||
|
||||
13
i18n/el.json
13
i18n/el.json
@@ -289,6 +289,8 @@
|
||||
"transcoding_constant_rate_factor": "Σταθερός παράγοντας ρυθμού (-crf)",
|
||||
"transcoding_constant_rate_factor_description": "Επίπεδο ποιότητας βίντεο. Οι τυπικές τιμές είναι οι, 23 για το H.264, 28 για το HEVC, 31 για το VP9 και 35 για το AV1. Χαμηλότερες τιμές σημαίνουν καλύτερη ποιότητα, αλλά παράγουν μεγαλύτερα αρχεία.",
|
||||
"transcoding_disabled_description": "Να μην μετατραπεί κανένα βίντεο γιατί δύναται να προκαλέσει πρόβλημα αναπαραγωγής σε ορισμένες συσκευές/εφαρμογές",
|
||||
"transcoding_encoding_options": "Επιλογές κωδικοποίησης",
|
||||
"transcoding_encoding_options_description": "Ορίστε τους κωδικοποιητές, την ανάλυση, την ποιότητα και άλλες επιλογές για τα κωδικοποιημένα βίντεο",
|
||||
"transcoding_hardware_acceleration": "Επιτάχυνση υλικού",
|
||||
"transcoding_hardware_acceleration_description": "Πειραματικό· πολύ πιο γρήγορο, αλλά θα έχει χαμηλότερη ποιότητα με τον ίδιο ρυθμό μετάδοσης (bitrate)",
|
||||
"transcoding_hardware_decoding": "Αποκωδικοποίηση μέσω υλικού",
|
||||
@@ -301,6 +303,8 @@
|
||||
"transcoding_max_keyframe_interval": "Μέγιστο χρονικό διάστημα μεταξύ των καρέ αναφοράς (keyframe)",
|
||||
"transcoding_max_keyframe_interval_description": "Ορίζει το μέγιστο διάστημα μεταξύ των καρέ αναφοράς. Χαμηλότερες τιμές μειώνουν την αποδοτικότητα συμπίεσης, αλλά βελτιώνουν τον χρόνο αναζήτησης και μπορεί να βελτιώσουν την ποιότητα σε σκηνές με γρήγορη κίνηση. Η τιμή 0 ρυθμίζει αυτό το διάστημα αυτόματα.",
|
||||
"transcoding_optimal_description": "Βίντεο με ανώτερη ανάλυση από την επιθυμητή ή σε μη αποδεκτή μορφή",
|
||||
"transcoding_policy": "Πολιτική Μετακωδικοποίησης",
|
||||
"transcoding_policy_description": "Ορίστε πότε θα γίνει η μετακωδικοποίηση ενός βίντεο",
|
||||
"transcoding_preferred_hardware_device": "Προτιμώμενη συσκευή",
|
||||
"transcoding_preferred_hardware_device_description": "Ισχύει μόνο για VAAPI και QSV. Ορίζει τον κόμβο DRI που χρησιμοποιείται για την επιτάχυνση υλικού κατά την κωδικοποίηση.",
|
||||
"transcoding_preset_preset": "Προκαθορισμένη ρύθμιση (-preset)",
|
||||
@@ -309,7 +313,7 @@
|
||||
"transcoding_reference_frames_description": "Ο αριθμός των καρέ που χρησιμοποιούνται ως αναφορά κατά τη συμπίεση ενός δεδομένου καρέ. Υψηλότερες τιμές βελτιώνουν την αποδοτικότητα της συμπίεσης, αλλά επιβραδύνουν την κωδικοποίηση. Η τιμή 0 ρυθμίζει αυτό τον αριθμό, αυτόματα.",
|
||||
"transcoding_required_description": "Μόνο βίντεο που δεν είναι σε αποδεκτή μορφή",
|
||||
"transcoding_settings": "Ρυθμίσεις μετατροπής βίντεο",
|
||||
"transcoding_settings_description": "Διαχείριση της ανάλυσης και των πληροφοριών κωδικοποίησης των αρχείων βίντεο",
|
||||
"transcoding_settings_description": "Διαχείριση των βίντεο που θα μετακωδικοποιηθούν και του τρόπου επεξεργασίας τους",
|
||||
"transcoding_target_resolution": "Επιθυμητή ανάλυση",
|
||||
"transcoding_target_resolution_description": "Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά απαιτούν περισσότερο χρόνο για κωδικοποίηση, παράγουν μεγαλύτερα αρχεία και μπορεί να μειώσουν την απόκριση της εφαρμογής.",
|
||||
"transcoding_temporal_aq": "Χρονική Προσαρμοστική Ποιότητα AQ(Adaptive Quantization)",
|
||||
@@ -322,7 +326,7 @@
|
||||
"transcoding_transcode_policy_description": "Πολιτική για το πότε πρέπει να μετατραπεί ένα βίντεο. Τα βίντεο HDR θα μετατρέπονται πάντα (εκτός αν η μετατροπή είναι απενεργοποιημένη).",
|
||||
"transcoding_two_pass_encoding": "Κωδικοποίηση δύο περασμάτων",
|
||||
"transcoding_two_pass_encoding_setting_description": "Μετατροπή σε δύο περάσματα για την παραγωγή βίντεο με καλύτερη κωδικοποίηση. Όταν είναι ενεργοποιημένος ο μέγιστος ρυθμός μετάδοσης (απαραίτητος για λειτουργία με H.264 και HEVC), αυτή η λειτουργία χρησιμοποιεί ένα εύρος ρυθμού μετάδοσης βάσει του μέγιστου ρυθμού μετάδοσης και αγνοεί το CRF. Στον κωδικοποιητή VP9, το CRF μπορεί να χρησιμοποιηθεί εάν ο μέγιστος ρυθμός μετάδοσης είναι απενεργοποιημένος.",
|
||||
"transcoding_video_codec": "Κωδικοποιητής Βίντεο",
|
||||
"transcoding_video_codec": "Κωδικοποιητής βίντεο",
|
||||
"transcoding_video_codec_description": "Ο VP9 έχει υψηλή απόδοση και συμβατότητα με τον ιστότοπο, αλλά απαιτεί περισσότερο χρόνο για μετατροπή. Ο HEVC έχει παρόμοια απόδοση, αλλά χαμηλότερη συμβατότητα με τον ιστότοπο. Ο H.264 είναι ευρέως συμβατός και γρήγορος στη μετατροπή, αλλά παράγει πολύ μεγαλύτερα αρχεία. Ο AV1 είναι ο πιο αποδοτικός κωδικοποιητής, αλλά δεν υποστηρίζεται σε παλαιότερες συσκευές.",
|
||||
"trash_enabled_description": "Ενεργοποίηση λειτουργιών Κάδου Απορριμμάτων",
|
||||
"trash_number_of_days": "Αριθμός ημερών",
|
||||
@@ -519,6 +523,10 @@
|
||||
"date_range": "Εύρος ημερομηνιών",
|
||||
"day": "Ημέρα",
|
||||
"deduplicate_all": "Αφαίρεση όλων των διπλότυπων",
|
||||
"deduplication_criteria_1": "Μέγεθος εικόνας σε byte",
|
||||
"deduplication_criteria_2": "Αριθμός δεδομένων EXIF",
|
||||
"deduplication_info": "Πληροφορίες Αφαίρεσης Διπλοτύπων",
|
||||
"deduplication_info_description": "Για να προεπιλέξουμε αυτόματα τα αρχεία και να αφαιρέσουμε τα διπλότυπα σε μαζική επεξεργασία, εξετάζουμε σε:",
|
||||
"default_locale": "Προεπιλεγμένη Τοπική Ρύθμιση",
|
||||
"default_locale_description": "Μορφοποιήστε τις ημερομηνίες και τους αριθμούς με βάση την τοπική ρύθμιση του προγράμματος περιήγησής σας",
|
||||
"delete": "Διαγραφή",
|
||||
@@ -755,6 +763,7 @@
|
||||
"get_help": "Ζητήστε βοήθεια",
|
||||
"getting_started": "Ξεκινώντας",
|
||||
"go_back": "Πηγαίνετε πίσω",
|
||||
"go_to_folder": "Μετάβαση στο φάκελο",
|
||||
"go_to_search": "Πηγαίνετε στην αναζήτηση",
|
||||
"group_albums_by": "Ομαδοποίηση άλμπουμ κατά...",
|
||||
"group_no": "Καμία ομοδοποίηση",
|
||||
|
||||
14
i18n/en.json
14
i18n/en.json
@@ -1,4 +1,6 @@
|
||||
{
|
||||
"search_by_description_example": "Hiking day in Sapa",
|
||||
"search_by_description": "Search by description",
|
||||
"about": "About",
|
||||
"account": "Account",
|
||||
"account_settings": "Account Settings",
|
||||
@@ -434,6 +436,7 @@
|
||||
"back_close_deselect": "Back, close, or deselect",
|
||||
"backward": "Backward",
|
||||
"birthdate_saved": "Date of birth saved successfully",
|
||||
"show_shared_links": "Show shared links",
|
||||
"birthdate_set_description": "Date of birth is used to calculate the age of this person at the time of a photo.",
|
||||
"blurred_background": "Blurred background",
|
||||
"bugs_and_feature_requests": "Bugs & Feature Requests",
|
||||
@@ -766,8 +769,10 @@
|
||||
"go_to_search": "Go to search",
|
||||
"go_to_folder": "Go to folder",
|
||||
"group_albums_by": "Group albums by...",
|
||||
"group_country": "Group by country",
|
||||
"group_no": "No grouping",
|
||||
"group_owner": "Group by owner",
|
||||
"group_places_by": "Group places by...",
|
||||
"group_year": "Group by year",
|
||||
"has_quota": "Has quota",
|
||||
"hi_user": "Hi {name} ({email})",
|
||||
@@ -800,6 +805,7 @@
|
||||
"include_shared_albums": "Include shared albums",
|
||||
"include_shared_partner_assets": "Include shared partner assets",
|
||||
"individual_share": "Individual share",
|
||||
"individual_shares": "Individual shares",
|
||||
"info": "Info",
|
||||
"interval": {
|
||||
"day_at_onepm": "Every day at 1pm",
|
||||
@@ -810,6 +816,7 @@
|
||||
"invite_people": "Invite People",
|
||||
"invite_to_album": "Invite to album",
|
||||
"items_count": "{count, plural, one {# item} other {# items}}",
|
||||
"views_count": "{count, plural, one {# view} other {# views}}",
|
||||
"jobs": "Jobs",
|
||||
"keep": "Keep",
|
||||
"keep_all": "Keep All",
|
||||
@@ -822,6 +829,7 @@
|
||||
"latest_version": "Latest Version",
|
||||
"latitude": "Latitude",
|
||||
"leave": "Leave",
|
||||
"lens_model": "Lens model",
|
||||
"let_others_respond": "Let others respond",
|
||||
"level": "Level",
|
||||
"library": "Library",
|
||||
@@ -984,6 +992,7 @@
|
||||
"pick_a_location": "Pick a location",
|
||||
"place": "Place",
|
||||
"places": "Places",
|
||||
"places_count": "{count, plural, one {{count, number} Place} other {{count, number} Places}}",
|
||||
"play": "Play",
|
||||
"play_memories": "Play memories",
|
||||
"play_motion_photo": "Play Motion Photo",
|
||||
@@ -1113,6 +1122,7 @@
|
||||
"search_camera_model": "Search camera model...",
|
||||
"search_city": "Search city...",
|
||||
"search_country": "Search country...",
|
||||
"search_for": "Search for",
|
||||
"search_for_existing_person": "Search for existing person",
|
||||
"search_no_people": "No people",
|
||||
"search_no_people_named": "No people named \"{name}\"",
|
||||
@@ -1165,6 +1175,7 @@
|
||||
"shared_from_partner": "Photos from {partner}",
|
||||
"shared_link_options": "Shared link options",
|
||||
"shared_links": "Shared links",
|
||||
"shared_links_description": "Share photos and videos with a link",
|
||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",
|
||||
"shared_with_partner": "Shared with {partner}",
|
||||
"sharing": "Sharing",
|
||||
@@ -1274,6 +1285,7 @@
|
||||
"unfavorite": "Unfavorite",
|
||||
"unhide_person": "Unhide person",
|
||||
"unknown": "Unknown",
|
||||
"unknown_country": "Unknown Country",
|
||||
"unknown_year": "Unknown Year",
|
||||
"unlimited": "Unlimited",
|
||||
"unlink_motion_video": "Unlink motion video",
|
||||
@@ -1348,4 +1360,4 @@
|
||||
"yes": "Yes",
|
||||
"you_dont_have_any_shared_links": "You don't have any shared links",
|
||||
"zoom_image": "Zoom Image"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Rango de fechas",
|
||||
"day": "Día",
|
||||
"deduplicate_all": "Deduplicar todo",
|
||||
"deduplication_criteria_1": "Tamaño de imagen en bytes",
|
||||
"deduplication_criteria_2": "Conteo de datos EXIF",
|
||||
"deduplication_info": "Información de Deduplicación",
|
||||
"deduplication_info_description": "Para automáticamente preseleccionar recursos y eliminar duplicados en conjunto, nosotros consideramos lo siguiente:",
|
||||
"default_locale": "Configuración regional predeterminada",
|
||||
"default_locale_description": "Formatee fechas y números según la configuración regional de su navegador",
|
||||
"delete": "Eliminar",
|
||||
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Kuupäevavahemik",
|
||||
"day": "Päev",
|
||||
"deduplicate_all": "Dedubleeri kõik",
|
||||
"deduplication_criteria_1": "Pildi suurus baitides",
|
||||
"deduplication_criteria_2": "EXIF andmete hulk",
|
||||
"deduplication_info": "Dedubleerimise info",
|
||||
"deduplication_info_description": "Üksuste automaatsel eelvalimisel ja duplikaatide eemaldamisel võetakse arvesse:",
|
||||
"default_locale": "Vaikimisi lokaat",
|
||||
"default_locale_description": "Vorminda kuupäevad ja numbrid vastavalt brauseri lokaadile",
|
||||
"delete": "Kustuta",
|
||||
@@ -748,6 +752,7 @@
|
||||
"filetype": "Failitüüp",
|
||||
"filter_people": "Filtreeri isikuid",
|
||||
"find_them_fast": "Leia teda kiiresti nime järgi otsides",
|
||||
"fix_incorrect_match": "Paranda ebaõige vaste",
|
||||
"folders": "Kaustad",
|
||||
"folders_feature_description": "Kaustavaate abil failisüsteemis olevate fotode ja videote sirvimine",
|
||||
"forward": "Edasi",
|
||||
@@ -755,6 +760,7 @@
|
||||
"get_help": "Küsi abi",
|
||||
"getting_started": "Alustamine",
|
||||
"go_back": "Tagasi",
|
||||
"go_to_folder": "Mine kausta",
|
||||
"go_to_search": "Otsingusse",
|
||||
"group_albums_by": "Grupeeri albumid...",
|
||||
"group_no": "Ära grupeeri",
|
||||
@@ -1029,6 +1035,7 @@
|
||||
"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",
|
||||
"reassing_hint": "Seosta valitud üksused olemasoleva isikuga",
|
||||
"recent": "Hiljutine",
|
||||
"recent-albums": "Hiljutised albumid",
|
||||
"recent_searches": "Hiljutised otsingud",
|
||||
"refresh": "Värskenda",
|
||||
@@ -1189,6 +1196,7 @@
|
||||
"sort_items": "Üksuste arv",
|
||||
"sort_modified": "Muutmise aeg",
|
||||
"sort_oldest": "Vanim foto",
|
||||
"sort_people_by_similarity": "Sorteeri isikud sarnasuse järgi",
|
||||
"sort_recent": "Uusim foto",
|
||||
"sort_title": "Pealkiri",
|
||||
"source": "Lähtekood",
|
||||
@@ -1309,6 +1317,7 @@
|
||||
"view_all_users": "Vaata kõiki kasutajaid",
|
||||
"view_in_timeline": "Vaata ajajoonel",
|
||||
"view_links": "Vaata linke",
|
||||
"view_name": "Vaade",
|
||||
"view_next_asset": "Vaata järgmist üksust",
|
||||
"view_previous_asset": "Vaata eelmist üksust",
|
||||
"view_stack": "Vaata virna",
|
||||
|
||||
874
i18n/fa.json
874
i18n/fa.json
@@ -312,157 +312,157 @@
|
||||
"admin_password": "رمز عبور مدیر",
|
||||
"administration": "مدیریت",
|
||||
"advanced": "پیشرفته",
|
||||
"album_added": "",
|
||||
"album_added": "آلبوم اضافه شد",
|
||||
"album_added_notification_setting_description": "",
|
||||
"album_cover_updated": "",
|
||||
"album_info_updated": "",
|
||||
"album_name": "",
|
||||
"album_options": "",
|
||||
"album_updated": "",
|
||||
"album_cover_updated": "جلد آلبوم بهروزرسانی شد",
|
||||
"album_info_updated": "اطلاعات آلبوم بهروزرسانی شد",
|
||||
"album_name": "نام آلبوم",
|
||||
"album_options": "گزینههای آلبوم",
|
||||
"album_updated": "آلبوم بهروزرسانی شد",
|
||||
"album_updated_setting_description": "",
|
||||
"albums": "",
|
||||
"albums": "آلبومها",
|
||||
"albums_count": "",
|
||||
"all": "",
|
||||
"all_people": "",
|
||||
"allow_dark_mode": "",
|
||||
"allow_edits": "",
|
||||
"api_key": "",
|
||||
"api_keys": "",
|
||||
"app_settings": "",
|
||||
"appears_in": "",
|
||||
"archive": "",
|
||||
"all": "همه",
|
||||
"all_people": "همه افراد",
|
||||
"allow_dark_mode": "اجازه دادن به حالت تاریک",
|
||||
"allow_edits": "اجازه ویرایش",
|
||||
"api_key": "کلید API",
|
||||
"api_keys": "کلیدهای API",
|
||||
"app_settings": "تنظیمات برنامه",
|
||||
"appears_in": "ظاهر میشود در",
|
||||
"archive": "بایگانی",
|
||||
"archive_or_unarchive_photo": "",
|
||||
"archive_size": "",
|
||||
"archive_size": "اندازه بایگانی",
|
||||
"archive_size_description": "",
|
||||
"asset_offline": "",
|
||||
"assets": "",
|
||||
"authorized_devices": "",
|
||||
"back": "",
|
||||
"backward": "",
|
||||
"blurred_background": "",
|
||||
"asset_offline": "محتوا آفلاین",
|
||||
"assets": "محتواها",
|
||||
"authorized_devices": "دستگاههای مجاز",
|
||||
"back": "بازگشت",
|
||||
"backward": "عقب",
|
||||
"blurred_background": "پسزمینه محو",
|
||||
"bulk_delete_duplicates_confirmation": "",
|
||||
"bulk_keep_duplicates_confirmation": "",
|
||||
"bulk_trash_duplicates_confirmation": "",
|
||||
"camera": "",
|
||||
"camera_brand": "",
|
||||
"camera_model": "",
|
||||
"cancel": "",
|
||||
"cancel_search": "",
|
||||
"cannot_merge_people": "",
|
||||
"cannot_update_the_description": "",
|
||||
"change_date": "",
|
||||
"change_expiration_time": "",
|
||||
"change_location": "",
|
||||
"change_name": "",
|
||||
"change_name_successfully": "",
|
||||
"change_password": "",
|
||||
"change_your_password": "",
|
||||
"camera": "دوربین",
|
||||
"camera_brand": "برند دوربین",
|
||||
"camera_model": "مدل دوربین",
|
||||
"cancel": "لغو",
|
||||
"cancel_search": "لغو جستجو",
|
||||
"cannot_merge_people": "نمیتوان افراد را ادغام کرد",
|
||||
"cannot_update_the_description": "نمیتوان توضیحات را بهروزرسانی کرد",
|
||||
"change_date": "تغییر تاریخ",
|
||||
"change_expiration_time": "تغییر زمان انقضا",
|
||||
"change_location": "تغییر مکان",
|
||||
"change_name": "تغییر نام",
|
||||
"change_name_successfully": "نام با موفقیت تغییر یافت",
|
||||
"change_password": "تغییر رمز عبور",
|
||||
"change_your_password": "رمز عبور خود را تغییر دهید",
|
||||
"changed_visibility_successfully": "",
|
||||
"check_all": "",
|
||||
"check_logs": "",
|
||||
"check_all": "انتخاب همه",
|
||||
"check_logs": "بررسی لاگها",
|
||||
"choose_matching_people_to_merge": "",
|
||||
"city": "",
|
||||
"clear": "",
|
||||
"clear_all": "",
|
||||
"clear_message": "",
|
||||
"clear_value": "",
|
||||
"close": "",
|
||||
"collapse_all": "",
|
||||
"color_theme": "",
|
||||
"comment_options": "",
|
||||
"comments_are_disabled": "",
|
||||
"confirm": "",
|
||||
"confirm_admin_password": "",
|
||||
"city": "شهر",
|
||||
"clear": "پاک کردن",
|
||||
"clear_all": "پاک کردن همه",
|
||||
"clear_message": "پاک کردن پیام",
|
||||
"clear_value": "پاک کردن مقدار",
|
||||
"close": "بستن",
|
||||
"collapse_all": "جمع کردن همه",
|
||||
"color_theme": "تم رنگ",
|
||||
"comment_options": "گزینههای نظر",
|
||||
"comments_are_disabled": "نظرات غیرفعال هستند",
|
||||
"confirm": "تأیید",
|
||||
"confirm_admin_password": "تأیید رمز عبور مدیر",
|
||||
"confirm_delete_shared_link": "",
|
||||
"confirm_password": "",
|
||||
"contain": "",
|
||||
"context": "",
|
||||
"continue": "",
|
||||
"copied_image_to_clipboard": "",
|
||||
"copied_to_clipboard": "",
|
||||
"copy_error": "",
|
||||
"copy_file_path": "",
|
||||
"copy_image": "",
|
||||
"copy_link": "",
|
||||
"copy_link_to_clipboard": "",
|
||||
"copy_password": "",
|
||||
"copy_to_clipboard": "",
|
||||
"country": "",
|
||||
"cover": "",
|
||||
"covers": "",
|
||||
"create": "",
|
||||
"create_album": "",
|
||||
"create_library": "",
|
||||
"create_link": "",
|
||||
"create_link_to_share": "",
|
||||
"create_new_person": "",
|
||||
"create_new_user": "",
|
||||
"create_user": "",
|
||||
"created": "",
|
||||
"current_device": "",
|
||||
"confirm_password": "تأیید رمز عبور",
|
||||
"contain": "شامل",
|
||||
"context": "زمینه",
|
||||
"continue": "ادامه",
|
||||
"copied_image_to_clipboard": "تصویر به کلیپبورد کپی شد.",
|
||||
"copied_to_clipboard": "به کلیپبورد کپی شد!",
|
||||
"copy_error": "خطا در کپی",
|
||||
"copy_file_path": "کپی مسیر فایل",
|
||||
"copy_image": "کپی تصویر",
|
||||
"copy_link": "کپی لینک",
|
||||
"copy_link_to_clipboard": "کپی لینک به کلیپبورد",
|
||||
"copy_password": "کپی رمز عبور",
|
||||
"copy_to_clipboard": "کپی به کلیپبورد",
|
||||
"country": "کشور",
|
||||
"cover": "جلد",
|
||||
"covers": "جلدها",
|
||||
"create": "ایجاد",
|
||||
"create_album": "ایجاد آلبوم",
|
||||
"create_library": "ایجاد کتابخانه",
|
||||
"create_link": "ایجاد لینک",
|
||||
"create_link_to_share": "ایجاد لینک برای اشتراکگذاری",
|
||||
"create_new_person": "ایجاد فرد جدید",
|
||||
"create_new_user": "ایجاد کاربر جدید",
|
||||
"create_user": "ایجاد کاربر",
|
||||
"created": "ایجاد شد",
|
||||
"current_device": "دستگاه فعلی",
|
||||
"custom_locale": "",
|
||||
"custom_locale_description": "",
|
||||
"dark": "",
|
||||
"date_after": "",
|
||||
"date_and_time": "",
|
||||
"date_before": "",
|
||||
"date_range": "",
|
||||
"day": "",
|
||||
"deduplicate_all": "",
|
||||
"dark": "تاریک",
|
||||
"date_after": "تاریخ پس از",
|
||||
"date_and_time": "تاریخ و زمان",
|
||||
"date_before": "تاریخ قبل از",
|
||||
"date_range": "بازه زمانی",
|
||||
"day": "روز",
|
||||
"deduplicate_all": "حذف تکراریها به صورت کامل",
|
||||
"default_locale": "",
|
||||
"default_locale_description": "",
|
||||
"delete": "",
|
||||
"delete_album": "",
|
||||
"delete": "حذف",
|
||||
"delete_album": "حذف آلبوم",
|
||||
"delete_api_key_prompt": "",
|
||||
"delete_duplicates_confirmation": "",
|
||||
"delete_key": "",
|
||||
"delete_library": "",
|
||||
"delete_link": "",
|
||||
"delete_shared_link": "",
|
||||
"delete_user": "",
|
||||
"deleted_shared_link": "",
|
||||
"description": "",
|
||||
"details": "",
|
||||
"direction": "",
|
||||
"disabled": "",
|
||||
"disallow_edits": "",
|
||||
"discover": "",
|
||||
"dismiss_all_errors": "",
|
||||
"dismiss_error": "",
|
||||
"display_options": "",
|
||||
"display_order": "",
|
||||
"display_original_photos": "",
|
||||
"delete_key": "حذف کلید",
|
||||
"delete_library": "حذف کتابخانه",
|
||||
"delete_link": "حذف لینک",
|
||||
"delete_shared_link": "حذف لینک اشتراکی",
|
||||
"delete_user": "حذف کاربر",
|
||||
"deleted_shared_link": "لینک اشتراکی حذف شد",
|
||||
"description": "توضیحات",
|
||||
"details": "جزئیات",
|
||||
"direction": "جهت",
|
||||
"disabled": "غیرفعال",
|
||||
"disallow_edits": "عدم اجازه ویرایش",
|
||||
"discover": "کشف کردن",
|
||||
"dismiss_all_errors": "رد تمام خطاها",
|
||||
"dismiss_error": "رد خطا",
|
||||
"display_options": "گزینههای نمایش",
|
||||
"display_order": "ترتیب نمایش",
|
||||
"display_original_photos": "نمایش عکسهای اصلی",
|
||||
"display_original_photos_setting_description": "",
|
||||
"done": "",
|
||||
"download": "",
|
||||
"download_settings": "",
|
||||
"download_settings_description": "",
|
||||
"downloading": "",
|
||||
"duplicates": "",
|
||||
"done": "انجام شد",
|
||||
"download": "دانلود",
|
||||
"download_settings": "تنظیمات دانلود",
|
||||
"download_settings_description": "مدیریت تنظیمات مرتبط با دانلود محتوا",
|
||||
"downloading": "در حال دانلود",
|
||||
"duplicates": "تکراریها",
|
||||
"duplicates_description": "",
|
||||
"duration": "",
|
||||
"edit_album": "",
|
||||
"edit_avatar": "",
|
||||
"edit_date": "",
|
||||
"edit_date_and_time": "",
|
||||
"edit_exclusion_pattern": "",
|
||||
"edit_faces": "",
|
||||
"duration": "مدت زمان",
|
||||
"edit_album": "ویرایش آلبوم",
|
||||
"edit_avatar": "ویرایش آواتار",
|
||||
"edit_date": "ویرایش تاریخ",
|
||||
"edit_date_and_time": "ویرایش تاریخ و زمان",
|
||||
"edit_exclusion_pattern": "ویرایش الگوی استثناء",
|
||||
"edit_faces": "ویرایش چهرهها",
|
||||
"edit_import_path": "",
|
||||
"edit_import_paths": "",
|
||||
"edit_key": "",
|
||||
"edit_link": "",
|
||||
"edit_location": "",
|
||||
"edit_name": "",
|
||||
"edit_people": "",
|
||||
"edit_title": "",
|
||||
"edit_user": "",
|
||||
"edited": "",
|
||||
"editor": "",
|
||||
"email": "",
|
||||
"empty_trash": "",
|
||||
"end_date": "",
|
||||
"error": "",
|
||||
"error_loading_image": "",
|
||||
"edit_key": "ویرایش کلید",
|
||||
"edit_link": "ویرایش لینک",
|
||||
"edit_location": "ویرایش مکان",
|
||||
"edit_name": "ویرایش نام",
|
||||
"edit_people": "ویرایش افراد",
|
||||
"edit_title": "ویرایش عنوان",
|
||||
"edit_user": "ویرایش کاربر",
|
||||
"edited": "ویرایش شد",
|
||||
"editor": "ویرایشگر",
|
||||
"email": "ایمیل",
|
||||
"empty_trash": "خالی کردن سطل زباله",
|
||||
"end_date": "تاریخ پایان",
|
||||
"error": "خطا",
|
||||
"error_loading_image": "خطا در بارگذاری تصویر",
|
||||
"errors": {
|
||||
"exclusion_pattern_already_exists": "",
|
||||
"import_path_already_exists": "",
|
||||
@@ -530,400 +530,400 @@
|
||||
"unable_to_update_timeline_display_status": "",
|
||||
"unable_to_update_user": ""
|
||||
},
|
||||
"exit_slideshow": "",
|
||||
"expand_all": "",
|
||||
"expire_after": "",
|
||||
"expired": "",
|
||||
"explore": "",
|
||||
"export": "",
|
||||
"export_as_json": "",
|
||||
"extension": "",
|
||||
"external": "",
|
||||
"external_libraries": "",
|
||||
"favorite": "",
|
||||
"exit_slideshow": "خروج از نمایش اسلاید",
|
||||
"expand_all": "باز کردن همه",
|
||||
"expire_after": "منقضی شدن بعد از",
|
||||
"expired": "منقضی شده",
|
||||
"explore": "کاوش کردن",
|
||||
"export": "صادر کردن",
|
||||
"export_as_json": "صادر کردن بهصورت JSON",
|
||||
"extension": "پسوند",
|
||||
"external": "خارجی",
|
||||
"external_libraries": "کتابخانههای خارجی",
|
||||
"favorite": "علاقهمندی",
|
||||
"favorite_or_unfavorite_photo": "",
|
||||
"favorites": "",
|
||||
"favorites": "علاقهمندیها",
|
||||
"feature_photo_updated": "",
|
||||
"file_name": "",
|
||||
"file_name_or_extension": "",
|
||||
"filename": "",
|
||||
"filetype": "",
|
||||
"filter_people": "",
|
||||
"file_name": "نام فایل",
|
||||
"file_name_or_extension": "نام فایل یا پسوند",
|
||||
"filename": "نام فایل",
|
||||
"filetype": "نوع فایل",
|
||||
"filter_people": "فیلتر افراد",
|
||||
"find_them_fast": "",
|
||||
"fix_incorrect_match": "",
|
||||
"forward": "",
|
||||
"general": "",
|
||||
"get_help": "",
|
||||
"getting_started": "",
|
||||
"go_back": "",
|
||||
"go_to_search": "",
|
||||
"group_albums_by": "",
|
||||
"has_quota": "",
|
||||
"hide_gallery": "",
|
||||
"hide_password": "",
|
||||
"hide_person": "",
|
||||
"host": "",
|
||||
"hour": "",
|
||||
"image": "",
|
||||
"immich_logo": "",
|
||||
"immich_web_interface": "",
|
||||
"import_from_json": "",
|
||||
"import_path": "",
|
||||
"fix_incorrect_match": "رفع تطابق نادرست",
|
||||
"forward": "جلو",
|
||||
"general": "عمومی",
|
||||
"get_help": "دریافت کمک",
|
||||
"getting_started": "شروع به کار",
|
||||
"go_back": "بازگشت",
|
||||
"go_to_search": "رفتن به جستجو",
|
||||
"group_albums_by": "گروهبندی آلبومها براساس...",
|
||||
"has_quota": "دارای سهمیه",
|
||||
"hide_gallery": "پنهان کردن گالری",
|
||||
"hide_password": "پنهان کردن رمز عبور",
|
||||
"hide_person": "پنهان کردن فرد",
|
||||
"host": "میزبان",
|
||||
"hour": "ساعت",
|
||||
"image": "تصویر",
|
||||
"immich_logo": "لوگوی Immich",
|
||||
"immich_web_interface": "رابط وب Immich",
|
||||
"import_from_json": "وارد کردن از JSON",
|
||||
"import_path": "مسیر وارد کردن",
|
||||
"in_albums": "",
|
||||
"in_archive": "",
|
||||
"include_archived": "",
|
||||
"include_shared_albums": "",
|
||||
"in_archive": "در بایگانی",
|
||||
"include_archived": "شامل بایگانی شدهها",
|
||||
"include_shared_albums": "شامل آلبومهای اشتراکی",
|
||||
"include_shared_partner_assets": "",
|
||||
"individual_share": "",
|
||||
"info": "",
|
||||
"individual_share": "اشتراک فردی",
|
||||
"info": "اطلاعات",
|
||||
"interval": {
|
||||
"day_at_onepm": "",
|
||||
"hours": "",
|
||||
"night_at_midnight": "",
|
||||
"night_at_twoam": ""
|
||||
},
|
||||
"invite_people": "",
|
||||
"invite_to_album": "",
|
||||
"jobs": "",
|
||||
"keep": "",
|
||||
"keep_all": "",
|
||||
"keyboard_shortcuts": "",
|
||||
"language": "",
|
||||
"language_setting_description": "",
|
||||
"last_seen": "",
|
||||
"leave": "",
|
||||
"let_others_respond": "",
|
||||
"level": "",
|
||||
"library": "",
|
||||
"library_options": "",
|
||||
"light": "",
|
||||
"link_options": "",
|
||||
"link_to_oauth": "",
|
||||
"linked_oauth_account": "",
|
||||
"list": "",
|
||||
"loading": "",
|
||||
"loading_search_results_failed": "",
|
||||
"log_out": "",
|
||||
"log_out_all_devices": "",
|
||||
"login_has_been_disabled": "",
|
||||
"look": "",
|
||||
"loop_videos": "",
|
||||
"invite_people": "دعوت افراد",
|
||||
"invite_to_album": "دعوت به آلبوم",
|
||||
"jobs": "وظایف",
|
||||
"keep": "نگه داشتن",
|
||||
"keep_all": "نگه داشتن همه",
|
||||
"keyboard_shortcuts": "میانبرهای صفحهکلید",
|
||||
"language": "زبان",
|
||||
"language_setting_description": "انتخاب زبان دلخواه شما",
|
||||
"last_seen": "آخرین مشاهده",
|
||||
"leave": "ترک کردن",
|
||||
"let_others_respond": "اجازه به دیگران برای پاسخگویی",
|
||||
"level": "سطح",
|
||||
"library": "کتابخانه",
|
||||
"library_options": "گزینههای کتابخانه",
|
||||
"light": "روشن",
|
||||
"link_options": "گزینههای لینک",
|
||||
"link_to_oauth": "اتصال به OAuth",
|
||||
"linked_oauth_account": "حساب OAuth متصل شده",
|
||||
"list": "لیست",
|
||||
"loading": "در حال بارگذاری",
|
||||
"loading_search_results_failed": "بارگذاری نتایج جستجو ناموفق بود",
|
||||
"log_out": "خروج از سیستم",
|
||||
"log_out_all_devices": "خروج از همه دستگاهها",
|
||||
"login_has_been_disabled": "ورود غیرفعال شده است.",
|
||||
"look": "نگاه کردن",
|
||||
"loop_videos": "پخش مداوم ویدئوها",
|
||||
"loop_videos_description": "",
|
||||
"make": "",
|
||||
"manage_shared_links": "",
|
||||
"make": "ساختن",
|
||||
"manage_shared_links": "مدیریت لینکهای اشتراکی",
|
||||
"manage_sharing_with_partners": "",
|
||||
"manage_the_app_settings": "",
|
||||
"manage_your_account": "",
|
||||
"manage_your_api_keys": "",
|
||||
"manage_your_devices": "",
|
||||
"manage_your_oauth_connection": "",
|
||||
"map": "",
|
||||
"manage_the_app_settings": "مدیریت تنظیمات برنامه",
|
||||
"manage_your_account": "مدیریت حساب کاربری شما",
|
||||
"manage_your_api_keys": "مدیریت کلیدهای API شما",
|
||||
"manage_your_devices": "مدیریت دستگاههای متصل",
|
||||
"manage_your_oauth_connection": "مدیریت اتصال OAuth شما",
|
||||
"map": "نقشه",
|
||||
"map_marker_with_image": "",
|
||||
"map_settings": "",
|
||||
"matches": "",
|
||||
"media_type": "",
|
||||
"memories": "",
|
||||
"map_settings": "تنظیمات نقشه",
|
||||
"matches": "تطابقها",
|
||||
"media_type": "نوع رسانه",
|
||||
"memories": "خاطرات",
|
||||
"memories_setting_description": "",
|
||||
"memory": "",
|
||||
"menu": "",
|
||||
"merge": "",
|
||||
"merge_people": "",
|
||||
"memory": "خاطره",
|
||||
"menu": "منو",
|
||||
"merge": "ادغام",
|
||||
"merge_people": "ادغام افراد",
|
||||
"merge_people_limit": "",
|
||||
"merge_people_prompt": "",
|
||||
"merge_people_successfully": "",
|
||||
"minimize": "",
|
||||
"minute": "",
|
||||
"missing": "",
|
||||
"model": "",
|
||||
"month": "",
|
||||
"more": "",
|
||||
"moved_to_trash": "",
|
||||
"my_albums": "",
|
||||
"name": "",
|
||||
"name_or_nickname": "",
|
||||
"never": "",
|
||||
"new_api_key": "",
|
||||
"new_password": "",
|
||||
"new_person": "",
|
||||
"new_user_created": "",
|
||||
"newest_first": "",
|
||||
"next": "",
|
||||
"next_memory": "",
|
||||
"no": "",
|
||||
"merge_people_successfully": "ادغام افراد با موفقیت انجام شد",
|
||||
"minimize": "کوچک کردن",
|
||||
"minute": "دقیقه",
|
||||
"missing": "گمشده",
|
||||
"model": "مدل",
|
||||
"month": "ماه",
|
||||
"more": "بیشتر",
|
||||
"moved_to_trash": "به سطل زباله منتقل شد",
|
||||
"my_albums": "آلبومهای من",
|
||||
"name": "نام",
|
||||
"name_or_nickname": "نام یا لقب",
|
||||
"never": "هرگز",
|
||||
"new_api_key": "کلید API جدید",
|
||||
"new_password": "رمز عبور جدید",
|
||||
"new_person": "فرد جدید",
|
||||
"new_user_created": "کاربر جدید ایجاد شد",
|
||||
"newest_first": "جدیدترین ابتدا",
|
||||
"next": "بعدی",
|
||||
"next_memory": "خاطره بعدی",
|
||||
"no": "خیر",
|
||||
"no_albums_message": "",
|
||||
"no_archived_assets_message": "",
|
||||
"no_assets_message": "",
|
||||
"no_duplicates_found": "",
|
||||
"no_exif_info_available": "",
|
||||
"no_duplicates_found": "هیچ تکراری یافت نشد.",
|
||||
"no_exif_info_available": "اطلاعات EXIF موجود نیست",
|
||||
"no_explore_results_message": "",
|
||||
"no_favorites_message": "",
|
||||
"no_libraries_message": "",
|
||||
"no_name": "",
|
||||
"no_places": "",
|
||||
"no_results": "",
|
||||
"no_name": "بدون نام",
|
||||
"no_places": "مکانی یافت نشد",
|
||||
"no_results": "نتیجهای یافت نشد",
|
||||
"no_shared_albums_message": "",
|
||||
"not_in_any_album": "",
|
||||
"not_in_any_album": "در هیچ آلبومی نیست",
|
||||
"note_apply_storage_label_to_previously_uploaded assets": "",
|
||||
"note_unlimited_quota": "",
|
||||
"notes": "",
|
||||
"notification_toggle_setting_description": "",
|
||||
"notifications": "",
|
||||
"notifications_setting_description": "",
|
||||
"oauth": "",
|
||||
"offline": "",
|
||||
"offline_paths": "",
|
||||
"notes": "یادداشتها",
|
||||
"notification_toggle_setting_description": "اعلانهای ایمیلی را فعال کنید",
|
||||
"notifications": "اعلانها",
|
||||
"notifications_setting_description": "مدیریت اعلانها",
|
||||
"oauth": "OAuth",
|
||||
"offline": "آفلاین",
|
||||
"offline_paths": "مسیرهای آفلاین",
|
||||
"offline_paths_description": "",
|
||||
"ok": "",
|
||||
"oldest_first": "",
|
||||
"online": "",
|
||||
"only_favorites": "",
|
||||
"open_the_search_filters": "",
|
||||
"options": "",
|
||||
"organize_your_library": "",
|
||||
"other": "",
|
||||
"other_devices": "",
|
||||
"other_variables": "",
|
||||
"owned": "",
|
||||
"owner": "",
|
||||
"partner": "",
|
||||
"partner_can_access": "",
|
||||
"ok": "تأیید",
|
||||
"oldest_first": "قدیمیترین ابتدا",
|
||||
"online": "آنلاین",
|
||||
"only_favorites": "فقط علاقهمندیها",
|
||||
"open_the_search_filters": "باز کردن فیلترهای جستجو",
|
||||
"options": "گزینهها",
|
||||
"organize_your_library": "کتابخانه خود را سازماندهی کنید",
|
||||
"other": "دیگر",
|
||||
"other_devices": "دستگاههای دیگر",
|
||||
"other_variables": "متغیرهای دیگر",
|
||||
"owned": "مالکیت",
|
||||
"owner": "مالک",
|
||||
"partner": "شریک",
|
||||
"partner_can_access": "{partner} میتواند دسترسی داشته باشد",
|
||||
"partner_can_access_assets": "",
|
||||
"partner_can_access_location": "",
|
||||
"partner_sharing": "",
|
||||
"partners": "",
|
||||
"password": "",
|
||||
"password_does_not_match": "",
|
||||
"password_required": "",
|
||||
"password_reset_success": "",
|
||||
"partner_can_access_location": "مکانهایی که عکسهای شما گرفته شدهاند",
|
||||
"partner_sharing": "اشتراکگذاری با شریک",
|
||||
"partners": "شرکا",
|
||||
"password": "رمز عبور",
|
||||
"password_does_not_match": "رمز عبور مطابقت ندارد",
|
||||
"password_required": "رمز عبور مورد نیاز است",
|
||||
"password_reset_success": "بازنشانی رمز عبور موفقیتآمیز بود",
|
||||
"past_durations": {
|
||||
"days": "",
|
||||
"hours": "",
|
||||
"years": ""
|
||||
},
|
||||
"path": "",
|
||||
"pattern": "",
|
||||
"pause": "",
|
||||
"pause_memories": "",
|
||||
"paused": "",
|
||||
"pending": "",
|
||||
"people": "",
|
||||
"path": "مسیر",
|
||||
"pattern": "الگو",
|
||||
"pause": "توقف",
|
||||
"pause_memories": "توقف خاطرات",
|
||||
"paused": "متوقف شده",
|
||||
"pending": "در انتظار",
|
||||
"people": "افراد",
|
||||
"people_sidebar_description": "",
|
||||
"permanent_deletion_warning": "",
|
||||
"permanent_deletion_warning_setting_description": "",
|
||||
"permanently_delete": "",
|
||||
"permanently_deleted_asset": "",
|
||||
"person": "",
|
||||
"photos": "",
|
||||
"permanent_deletion_warning": "هشدار حذف دائمی",
|
||||
"permanent_deletion_warning_setting_description": "نمایش هشدار هنگام حذف دائمی محتواها",
|
||||
"permanently_delete": "حذف دائمی",
|
||||
"permanently_deleted_asset": "محتوای حذف شده دائمی",
|
||||
"person": "فرد",
|
||||
"photos": "عکسها",
|
||||
"photos_count": "",
|
||||
"photos_from_previous_years": "",
|
||||
"pick_a_location": "",
|
||||
"place": "",
|
||||
"places": "",
|
||||
"play": "",
|
||||
"play_memories": "",
|
||||
"play_motion_photo": "",
|
||||
"play_or_pause_video": "",
|
||||
"port": "",
|
||||
"preset": "",
|
||||
"preview": "",
|
||||
"previous": "",
|
||||
"previous_memory": "",
|
||||
"previous_or_next_photo": "",
|
||||
"primary": "",
|
||||
"profile_picture_set": "",
|
||||
"public_share": "",
|
||||
"reaction_options": "",
|
||||
"read_changelog": "",
|
||||
"recent": "",
|
||||
"recent_searches": "",
|
||||
"refresh": "",
|
||||
"refreshed": "",
|
||||
"photos_from_previous_years": "عکسهای سالهای گذشته",
|
||||
"pick_a_location": "یک مکان انتخاب کنید",
|
||||
"place": "مکان",
|
||||
"places": "مکانها",
|
||||
"play": "پخش",
|
||||
"play_memories": "پخش خاطرات",
|
||||
"play_motion_photo": "پخش عکس متحرک",
|
||||
"play_or_pause_video": "پخش یا توقف ویدیو",
|
||||
"port": "پورت",
|
||||
"preset": "پیشفرض",
|
||||
"preview": "پیشنمایش",
|
||||
"previous": "قبلی",
|
||||
"previous_memory": "خاطره قبلی",
|
||||
"previous_or_next_photo": "عکس قبلی یا بعدی",
|
||||
"primary": "اصلی",
|
||||
"profile_picture_set": "تصویر پروفایل تنظیم شد.",
|
||||
"public_share": "اشتراک عمومی",
|
||||
"reaction_options": "گزینههای واکنش",
|
||||
"read_changelog": "مطالعه تغییرات نسخه",
|
||||
"recent": "اخیر",
|
||||
"recent_searches": "جستجوهای اخیر",
|
||||
"refresh": "تازه سازی",
|
||||
"refreshed": "تازه سازی شد",
|
||||
"refreshes_every_file": "",
|
||||
"remove": "",
|
||||
"remove_deleted_assets": "",
|
||||
"remove_from_album": "",
|
||||
"remove_from_favorites": "",
|
||||
"remove": "حذف",
|
||||
"remove_deleted_assets": "حذف محتواهای حذفشده",
|
||||
"remove_from_album": "حذف از آلبوم",
|
||||
"remove_from_favorites": "حذف از علاقهمندیها",
|
||||
"remove_from_shared_link": "",
|
||||
"removed_api_key": "",
|
||||
"rename": "",
|
||||
"repair": "",
|
||||
"rename": "تغییر نام",
|
||||
"repair": "تعمیر",
|
||||
"repair_no_results_message": "",
|
||||
"replace_with_upload": "",
|
||||
"replace_with_upload": "جایگزینی با آپلود",
|
||||
"require_password": "",
|
||||
"require_user_to_change_password_on_first_login": "",
|
||||
"reset": "",
|
||||
"reset_password": "",
|
||||
"reset": "بازنشانی",
|
||||
"reset_password": "بازنشانی رمز عبور",
|
||||
"reset_people_visibility": "",
|
||||
"resolved_all_duplicates": "",
|
||||
"restore": "",
|
||||
"restore_all": "",
|
||||
"restore_user": "",
|
||||
"resume": "",
|
||||
"restore": "بازیابی",
|
||||
"restore_all": "بازیابی همه",
|
||||
"restore_user": "بازیابی کاربر",
|
||||
"resume": "ادامه",
|
||||
"retry_upload": "",
|
||||
"review_duplicates": "",
|
||||
"role": "",
|
||||
"save": "",
|
||||
"review_duplicates": "بررسی تکراریها",
|
||||
"role": "نقش",
|
||||
"save": "ذخیره",
|
||||
"saved_api_key": "",
|
||||
"saved_profile": "",
|
||||
"saved_settings": "",
|
||||
"say_something": "",
|
||||
"scan_all_libraries": "",
|
||||
"scan_settings": "",
|
||||
"saved_profile": "پروفایل ذخیره شد",
|
||||
"saved_settings": "تنظیمات ذخیره شد",
|
||||
"say_something": "چیزی بگویید",
|
||||
"scan_all_libraries": "اسکن همه کتابخانهها",
|
||||
"scan_settings": "تنظیمات اسکن",
|
||||
"scanning_for_album": "",
|
||||
"search": "",
|
||||
"search_albums": "",
|
||||
"search_by_context": "",
|
||||
"search_camera_make": "",
|
||||
"search_camera_model": "",
|
||||
"search_city": "",
|
||||
"search_country": "",
|
||||
"search_for_existing_person": "",
|
||||
"search_people": "",
|
||||
"search_places": "",
|
||||
"search_state": "",
|
||||
"search_timezone": "",
|
||||
"search_type": "",
|
||||
"search": "جستجو",
|
||||
"search_albums": "جستجوی آلبومها",
|
||||
"search_by_context": "جستجو براساس زمینه",
|
||||
"search_camera_make": "جستجوی برند دوربین...",
|
||||
"search_camera_model": "جستجوی مدل دوربین...",
|
||||
"search_city": "جستجوی شهر...",
|
||||
"search_country": "جستجوی کشور...",
|
||||
"search_for_existing_person": "جستجوی فرد موجود",
|
||||
"search_people": "جستجوی افراد",
|
||||
"search_places": "جستجوی مکانها",
|
||||
"search_state": "جستجوی ایالت...",
|
||||
"search_timezone": "جستجوی منطقه زمانی...",
|
||||
"search_type": "نوع جستجو",
|
||||
"search_your_photos": "",
|
||||
"searching_locales": "",
|
||||
"second": "",
|
||||
"select_album_cover": "",
|
||||
"select_all": "",
|
||||
"select_avatar_color": "",
|
||||
"select_face": "",
|
||||
"select_featured_photo": "",
|
||||
"select_keep_all": "",
|
||||
"select_library_owner": "",
|
||||
"select_new_face": "",
|
||||
"select_photos": "",
|
||||
"second": "ثانیه",
|
||||
"select_album_cover": "انتخاب جلد آلبوم",
|
||||
"select_all": "انتخاب همه",
|
||||
"select_avatar_color": "انتخاب رنگ آواتار",
|
||||
"select_face": "انتخاب چهره",
|
||||
"select_featured_photo": "انتخاب عکس ویژه",
|
||||
"select_keep_all": "انتخاب نگهداری همه",
|
||||
"select_library_owner": "انتخاب مالک کتابخانه",
|
||||
"select_new_face": "انتخاب چهره جدید",
|
||||
"select_photos": "انتخاب عکسها",
|
||||
"select_trash_all": "",
|
||||
"selected": "",
|
||||
"send_message": "",
|
||||
"send_welcome_email": "",
|
||||
"server_stats": "",
|
||||
"set": "",
|
||||
"selected": "انتخاب شده",
|
||||
"send_message": "ارسال پیام",
|
||||
"send_welcome_email": "ارسال ایمیل خوشآمدگویی",
|
||||
"server_stats": "آمار سرور",
|
||||
"set": "تنظیم",
|
||||
"set_as_album_cover": "",
|
||||
"set_as_profile_picture": "",
|
||||
"set_date_of_birth": "",
|
||||
"set_profile_picture": "",
|
||||
"set_date_of_birth": "تنظیم تاریخ تولد",
|
||||
"set_profile_picture": "تنظیم تصویر پروفایل",
|
||||
"set_slideshow_to_fullscreen": "",
|
||||
"settings": "",
|
||||
"settings_saved": "",
|
||||
"share": "",
|
||||
"shared": "",
|
||||
"shared_by": "",
|
||||
"settings": "تنظیمات",
|
||||
"settings_saved": "تنظیمات ذخیره شد",
|
||||
"share": "اشتراکگذاری",
|
||||
"shared": "مشترک",
|
||||
"shared_by": "مشترک توسط",
|
||||
"shared_by_you": "",
|
||||
"shared_from_partner": "",
|
||||
"shared_links": "",
|
||||
"shared_from_partner": "عکسها از {partner}",
|
||||
"shared_links": "لینکهای اشتراکی",
|
||||
"shared_photos_and_videos_count": "",
|
||||
"shared_with_partner": "",
|
||||
"sharing": "",
|
||||
"shared_with_partner": "مشترک با {partner}",
|
||||
"sharing": "اشتراکگذاری",
|
||||
"sharing_sidebar_description": "",
|
||||
"show_album_options": "",
|
||||
"show_album_options": "نمایش گزینههای آلبوم",
|
||||
"show_and_hide_people": "",
|
||||
"show_file_location": "",
|
||||
"show_gallery": "",
|
||||
"show_hidden_people": "",
|
||||
"show_file_location": "نمایش مسیر فایل",
|
||||
"show_gallery": "نمایش گالری",
|
||||
"show_hidden_people": "نمایش افراد پنهان",
|
||||
"show_in_timeline": "",
|
||||
"show_in_timeline_setting_description": "",
|
||||
"show_keyboard_shortcuts": "",
|
||||
"show_metadata": "",
|
||||
"show_metadata": "نمایش اطلاعات متا",
|
||||
"show_or_hide_info": "",
|
||||
"show_password": "",
|
||||
"show_password": "نمایش رمز عبور",
|
||||
"show_person_options": "",
|
||||
"show_progress_bar": "",
|
||||
"show_search_options": "",
|
||||
"shuffle": "",
|
||||
"sign_out": "",
|
||||
"sign_up": "",
|
||||
"size": "",
|
||||
"skip_to_content": "",
|
||||
"slideshow": "",
|
||||
"slideshow_settings": "",
|
||||
"show_progress_bar": "نمایش نوار پیشرفت",
|
||||
"show_search_options": "نمایش گزینههای جستجو",
|
||||
"shuffle": "تصادفی",
|
||||
"sign_out": "خروج",
|
||||
"sign_up": "ثبتنام",
|
||||
"size": "اندازه",
|
||||
"skip_to_content": "رفتن به محتوا",
|
||||
"slideshow": "نمایش اسلاید",
|
||||
"slideshow_settings": "تنظیمات نمایش اسلاید",
|
||||
"sort_albums_by": "",
|
||||
"stack": "",
|
||||
"stack": "پشته",
|
||||
"stack_selected_photos": "",
|
||||
"stacktrace": "",
|
||||
"start": "",
|
||||
"start_date": "",
|
||||
"state": "",
|
||||
"status": "",
|
||||
"stop_motion_photo": "",
|
||||
"start": "شروع",
|
||||
"start_date": "تاریخ شروع",
|
||||
"state": "ایالت",
|
||||
"status": "وضعیت",
|
||||
"stop_motion_photo": "توقف عکس متحرک",
|
||||
"stop_photo_sharing": "",
|
||||
"stop_photo_sharing_description": "",
|
||||
"stop_sharing_photos_with_user": "",
|
||||
"storage": "",
|
||||
"storage_label": "",
|
||||
"storage": "فضای ذخیرهسازی",
|
||||
"storage_label": "برچسب فضای ذخیرهسازی",
|
||||
"storage_usage": "",
|
||||
"submit": "",
|
||||
"suggestions": "",
|
||||
"submit": "ارسال",
|
||||
"suggestions": "پیشنهادات",
|
||||
"sunrise_on_the_beach": "",
|
||||
"swap_merge_direction": "",
|
||||
"sync": "",
|
||||
"template": "",
|
||||
"theme": "",
|
||||
"theme_selection": "",
|
||||
"swap_merge_direction": "تغییر جهت ادغام",
|
||||
"sync": "همگامسازی",
|
||||
"template": "الگو",
|
||||
"theme": "تم",
|
||||
"theme_selection": "انتخاب تم",
|
||||
"theme_selection_description": "",
|
||||
"time_based_memories": "",
|
||||
"timezone": "",
|
||||
"to_archive": "",
|
||||
"to_favorite": "",
|
||||
"timezone": "منطقه زمانی",
|
||||
"to_archive": "بایگانی",
|
||||
"to_favorite": "به علاقهمندیها",
|
||||
"to_trash": "",
|
||||
"toggle_settings": "",
|
||||
"toggle_theme": "",
|
||||
"total_usage": "",
|
||||
"trash": "",
|
||||
"toggle_settings": "تغییر تنظیمات",
|
||||
"toggle_theme": "تغییر تم تاریک",
|
||||
"total_usage": "استفاده کلی",
|
||||
"trash": "سطل زباله",
|
||||
"trash_all": "",
|
||||
"trash_count": "",
|
||||
"trash_no_results_message": "",
|
||||
"trashed_items_will_be_permanently_deleted_after": "",
|
||||
"type": "",
|
||||
"type": "نوع",
|
||||
"unarchive": "",
|
||||
"unfavorite": "",
|
||||
"unhide_person": "",
|
||||
"unknown": "",
|
||||
"unknown_year": "",
|
||||
"unlimited": "",
|
||||
"unlink_oauth": "",
|
||||
"unfavorite": "حذف از علاقهمندیها",
|
||||
"unhide_person": "آشکار کردن فرد",
|
||||
"unknown": "ناشناخته",
|
||||
"unknown_year": "سال نامشخص",
|
||||
"unlimited": "نامحدود",
|
||||
"unlink_oauth": "لغو اتصال OAuth",
|
||||
"unlinked_oauth_account": "",
|
||||
"unnamed_album": "",
|
||||
"unnamed_share": "",
|
||||
"unselect_all": "",
|
||||
"unnamed_album": "آلبوم بدون نام",
|
||||
"unnamed_share": "اشتراک بدون نام",
|
||||
"unselect_all": "لغو انتخاب همه",
|
||||
"unstack": "",
|
||||
"untracked_files": "",
|
||||
"untracked_files_decription": "",
|
||||
"up_next": "",
|
||||
"up_next": "مورد بعدی",
|
||||
"updated_password": "",
|
||||
"upload": "",
|
||||
"upload_concurrency": "",
|
||||
"url": "",
|
||||
"usage": "",
|
||||
"user": "",
|
||||
"user_id": "",
|
||||
"user_usage_detail": "",
|
||||
"username": "",
|
||||
"users": "",
|
||||
"utilities": "",
|
||||
"validate": "",
|
||||
"variables": "",
|
||||
"version": "",
|
||||
"upload": "آپلود",
|
||||
"upload_concurrency": "تعداد آپلود همزمان",
|
||||
"url": "آدرس",
|
||||
"usage": "استفاده",
|
||||
"user": "کاربر",
|
||||
"user_id": "شناسه کاربر",
|
||||
"user_usage_detail": "جزئیات استفاده کاربر",
|
||||
"username": "نام کاربری",
|
||||
"users": "کاربران",
|
||||
"utilities": "ابزارها",
|
||||
"validate": "اعتبارسنجی",
|
||||
"variables": "متغیرها",
|
||||
"version": "نسخه",
|
||||
"version_announcement_message": "",
|
||||
"video": "",
|
||||
"video": "ویدیو",
|
||||
"video_hover_setting": "",
|
||||
"video_hover_setting_description": "",
|
||||
"videos": "",
|
||||
"videos": "ویدیوها",
|
||||
"videos_count": "",
|
||||
"view": "",
|
||||
"view_all": "",
|
||||
"view_all_users": "",
|
||||
"view_links": "",
|
||||
"view_next_asset": "",
|
||||
"view_previous_asset": "",
|
||||
"waiting": "",
|
||||
"week": "",
|
||||
"welcome": "",
|
||||
"view": "مشاهده",
|
||||
"view_all": "مشاهده همه",
|
||||
"view_all_users": "مشاهده همه کاربران",
|
||||
"view_links": "مشاهده لینکها",
|
||||
"view_next_asset": "مشاهده محتوای بعدی",
|
||||
"view_previous_asset": "مشاهده محتوای قبلی",
|
||||
"waiting": "در انتظار",
|
||||
"week": "هفته",
|
||||
"welcome": "خوش آمدید",
|
||||
"welcome_to_immich": "",
|
||||
"year": "",
|
||||
"yes": "",
|
||||
"year": "سال",
|
||||
"yes": "بله",
|
||||
"you_dont_have_any_shared_links": "",
|
||||
"zoom_image": "بزرگنمایی تصویر"
|
||||
}
|
||||
|
||||
19
i18n/fi.json
19
i18n/fi.json
@@ -182,7 +182,7 @@
|
||||
"oauth_auto_register_description": "Rekisteröi uudet OAuth:lla kirjautuvat käyttäjät automaattisesti",
|
||||
"oauth_button_text": "Painikkeen teksti",
|
||||
"oauth_client_id": "Client ID",
|
||||
"oauth_client_secret": "Client Secret",
|
||||
"oauth_client_secret": "Asiakassalaisuusavain",
|
||||
"oauth_enable_description": "Kirjaudu käyttäen OAuthia",
|
||||
"oauth_issuer_url": "Toimitsijan URL",
|
||||
"oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI",
|
||||
@@ -289,11 +289,13 @@
|
||||
"transcoding_constant_rate_factor": "Vakionopeustekijä",
|
||||
"transcoding_constant_rate_factor_description": "Videon laatu. Yleisimmät arvot ovat 23 H.264:lle, 28 HEVC:lle, 31 VP9:lle ja 35 AV1:lle. Matalampi arvo on parempi, mutta tekee isompia tiedostoja.",
|
||||
"transcoding_disabled_description": "Älä muunna videoita. Voi joissakin päätelaitteissa aiheuttaa videotoiston toimimattomuutta",
|
||||
"transcoding_encoding_options": "Enkoodausasetukset",
|
||||
"transcoding_encoding_options_description": "Aseta koodekit, tarkkuus, laatu ja muut asetukset enkoodatuille videoille",
|
||||
"transcoding_hardware_acceleration": "Laitteistokiihdytys",
|
||||
"transcoding_hardware_acceleration_description": "Kokeellinen. Paljon nopeampi, mutta huonompaa laatua samalla bittinopeudella",
|
||||
"transcoding_hardware_decoding": "Laitteiston dekoodaus",
|
||||
"transcoding_hardware_decoding_setting_description": "Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.",
|
||||
"transcoding_hevc_codec": "HEVC koodekki",
|
||||
"transcoding_hevc_codec": "HEVC-koodekki",
|
||||
"transcoding_max_b_frames": "B-kehysten enimmäismäärä",
|
||||
"transcoding_max_b_frames_description": "Korkeampi arvo parantaa pakkausta, mutta hidastaa enkoodausta. Ei välttämättä ole yhteensopiva vanhempien laitteiden kanssa. 0 poistaa B-kehykset käytöstä, -1 määrittää arvon automaattisesti.",
|
||||
"transcoding_max_bitrate": "Suurin bittinopeus",
|
||||
@@ -301,6 +303,8 @@
|
||||
"transcoding_max_keyframe_interval": "Suurin avainkehysten väli",
|
||||
"transcoding_max_keyframe_interval_description": "Asettaa avainkehysten välin maksimiarvon. Alempi arvo huonontaa pakkauksen tehoa, mutta parantaa hakuaikoja ja voi parantaa laatua nopealiikkeisissä kohtauksissa. 0 asettaa arvon automaattisesti.",
|
||||
"transcoding_optimal_description": "Videot, joiden resoluutio on korkeampi kuin kohteen, tai ei hyväksytyssä formaatissa",
|
||||
"transcoding_policy": "Transkoodauskäytäntö",
|
||||
"transcoding_policy_description": "Aseta milloin video transkoodataan",
|
||||
"transcoding_preferred_hardware_device": "Ensisijainen laite",
|
||||
"transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.",
|
||||
"transcoding_preset_preset": "Esiasetus (-asetus)",
|
||||
@@ -309,7 +313,7 @@
|
||||
"transcoding_reference_frames_description": "Viittaavien kehysten määrä kun tiettyä kehystä pakataan. Korkeampi arvo parantaa pakkausta mutta hidastaa enkoodausta. 0 määrittää arvon automaattisesti.",
|
||||
"transcoding_required_description": "Vain videoille, jotka eivät ole hyväksytyssä muodossa",
|
||||
"transcoding_settings": "Videoiden transkoodausasetukset",
|
||||
"transcoding_settings_description": "Hallitse videoiden resoluutiota ja koodaustietueita",
|
||||
"transcoding_settings_description": "Hallitse, mitkä videot transkoodataan ja miten niitä käsitellään",
|
||||
"transcoding_target_resolution": "Kohderesoluutio",
|
||||
"transcoding_target_resolution_description": "Korkeampi resoluutio on tarkempi, mutta kestää kauemmin enkoodata, vie enemmän tilaa ja voi hidastaa sovelluksen responsiivisuutta.",
|
||||
"transcoding_temporal_aq": "Temporal AQ",
|
||||
@@ -519,6 +523,10 @@
|
||||
"date_range": "Päivämäärän rajaus",
|
||||
"day": "Päivä",
|
||||
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
|
||||
"deduplication_criteria_1": "Kuvan koko tavuina",
|
||||
"deduplication_criteria_2": "EXIF-datan määrä",
|
||||
"deduplication_info": "Deduplikaatiotieto",
|
||||
"deduplication_info_description": "Jotta voimme automaattisesti esivalita aineistot ja poistaa duplikaatit suurina erinä, tarkastelemme:",
|
||||
"default_locale": "Oletuskieliasetus",
|
||||
"default_locale_description": "Muotoile päivämäärät ja numerot selaimesi kielen mukaan",
|
||||
"delete": "Poista",
|
||||
@@ -532,7 +540,7 @@
|
||||
"delete_shared_link": "Poista jaettu linkki",
|
||||
"delete_tag": "Poista tunniste",
|
||||
"delete_tag_confirmation_prompt": "Haluatko varmasti poistaa tunnisteen {tagName}?",
|
||||
"delete_user": "Poista käyttäjä",
|
||||
"delete_user": "Poista käyttäjä pysyvästi",
|
||||
"deleted_shared_link": "Jaettu linkki poistettu",
|
||||
"deletes_missing_assets": "Poistaa levyltä puuttuvat resurssit",
|
||||
"description": "Kuvaus",
|
||||
@@ -755,6 +763,7 @@
|
||||
"get_help": "Hae apua",
|
||||
"getting_started": "Aloittaminen",
|
||||
"go_back": "Palaa",
|
||||
"go_to_folder": "Mene kansioon",
|
||||
"go_to_search": "Siirry hakuun",
|
||||
"group_albums_by": "Ryhmitä albumi...",
|
||||
"group_no": "Ei ryhmitystä",
|
||||
@@ -1141,6 +1150,7 @@
|
||||
"server_version": "Palvelimen versio",
|
||||
"set": "Aseta",
|
||||
"set_as_album_cover": "Aseta albumin kanneksi",
|
||||
"set_as_featured_photo": "Käytä esittelykuvana",
|
||||
"set_as_profile_picture": "Aseta profiilikuvaksi",
|
||||
"set_date_of_birth": "Aseta syntymäaika",
|
||||
"set_profile_picture": "Aseta profiilikuva",
|
||||
@@ -1196,6 +1206,7 @@
|
||||
"sort_items": "Tietueiden määrä",
|
||||
"sort_modified": "Muokkauspäivä",
|
||||
"sort_oldest": "Vanhin kuva",
|
||||
"sort_people_by_similarity": "Lajittele ihmiset samankaltaisuuden mukaan",
|
||||
"sort_recent": "Tuorein kuva",
|
||||
"sort_title": "Otsikko",
|
||||
"source": "Lähdekoodi",
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"about": "I-refresh",
|
||||
"about": "Tungkol sa app na ito",
|
||||
"account": "Account",
|
||||
"account_settings": "Mga Setting ng Account",
|
||||
"acknowledge": "Tanggapin",
|
||||
@@ -24,9 +24,15 @@
|
||||
"added_to_favorites_count": "Idinagdag ang {count, number} sa mga paborito",
|
||||
"admin": {
|
||||
"asset_offline_description": "Ang external library asset na ito ay hindi na makikita sa disk at nailipat na sa trash. Kung ang file ay nailipat sa loob ng library, tignan ang iyong timeline para sa kaukulang asset. Para maibalik ang asset na ito, siguraduhin na ang file path ay maa-access ng Immich para iscan ang library.",
|
||||
"authentication_settings_disable_all": "Sigurado ka bang gusto mo patayin lahat ng paraan ng pag-login? Ang pag-login ay ganap na idi-disable.",
|
||||
"authentication_settings_reenable": "Para i-enable ulit, gamitin ang <link>Server Command</link>.",
|
||||
"cleared_jobs": "Lahat nang mga trabaho para sa {job} ay tinanggal na",
|
||||
"confirm_delete_library": "Sigurado ka na gusto mo tanggalin ang {library} library?",
|
||||
"confirm_email_below": "Para isigurado, i-type ito sa baba: \"{email}\"",
|
||||
"confirm_user_password_reset": "Sigurado ka na gusto mo i-reset ang password ni {user}?",
|
||||
"disable_login": "I-disable ang login",
|
||||
"force_delete_user_warning": "BABALA:",
|
||||
"force_delete_user_warning": "BABALA: Tatanggalin itong user at lahat ng asset nila, Hindi ito mababawi at ang kanilang files ay hindi na mababalik",
|
||||
"image_format": "Format",
|
||||
"library_import_path_description": "Tukuyin ang folder na i-import. Ang folder na ito, kasama ang subfolders, ay mag sa-scan para sa mga imahe at mga videos.",
|
||||
"note_cannot_be_changed_later": "TANDAAN: Hindi na ito pwede baguhin sa susunod!",
|
||||
"repair_all": "Ayusin lahat",
|
||||
@@ -40,5 +46,22 @@
|
||||
"are_these_the_same_person": "Itong tao na ito ay parehas?",
|
||||
"asset_adding_to_album": "Dinadagdag sa album...",
|
||||
"asset_filename_is_offline": "Offline ang asset {filename}",
|
||||
"asset_uploading": "Ina-upload..."
|
||||
"asset_uploading": "Ina-upload...",
|
||||
"discord": "Discord",
|
||||
"documentation": "Dokumentasyion",
|
||||
"done": "Tapos na",
|
||||
"download": "I-download",
|
||||
"edit": "I-edit",
|
||||
"edited": "Inedit",
|
||||
"editor_close_without_save_title": "Isara ang editor?",
|
||||
"email": "Email",
|
||||
"exif": "Exif",
|
||||
"explore": "I-explore",
|
||||
"export": "I-export",
|
||||
"has_quota": "May quota",
|
||||
"hour": "Oras",
|
||||
"jobs": "Mga trabaho",
|
||||
"language": "Wika",
|
||||
"leave": "Umalis",
|
||||
"no_results": "Walang resulta"
|
||||
}
|
||||
|
||||
@@ -523,6 +523,10 @@
|
||||
"date_range": "Plage de dates",
|
||||
"day": "Jour",
|
||||
"deduplicate_all": "Dédupliquer tout",
|
||||
"deduplication_criteria_1": "Taille de l'image en octets",
|
||||
"deduplication_criteria_2": "Nombre de données EXIF",
|
||||
"deduplication_info": "Info de déduplication",
|
||||
"deduplication_info_description": "Pour présélectionner automatiquement les médias et supprimer les doublons en masse, nous examinons :",
|
||||
"default_locale": "Région par défaut",
|
||||
"default_locale_description": "Afficher les dates et nombres en fonction des paramètres de votre navigateur",
|
||||
"delete": "Supprimer",
|
||||
|
||||
36
i18n/he.json
36
i18n/he.json
@@ -131,7 +131,7 @@
|
||||
"machine_learning_smart_search_description": "חפש תמונות באופן סמנטי באמצעות הטמעות של CLIP",
|
||||
"machine_learning_smart_search_enabled": "אפשר חיפוש חכם",
|
||||
"machine_learning_smart_search_enabled_description": "אם מושבת, תמונות לא יקודדו לחיפוש חכם.",
|
||||
"machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתן יותר מכתובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.",
|
||||
"machine_learning_url_description": "כתובת האתר של שרת למידת המכונה. אם ניתנת יותר מכתובת אחת, כל שרת ינסה בתורו עד אשר יענה בחיוב, בסדר התחלתי.",
|
||||
"manage_concurrency": "נהל בו-זמניות",
|
||||
"manage_log_settings": "נהל הגדרות רישום ביומן",
|
||||
"map_dark_style": "עיצוב כהה",
|
||||
@@ -289,8 +289,8 @@
|
||||
"transcoding_constant_rate_factor": "גורם קצב קבוע (-crf)",
|
||||
"transcoding_constant_rate_factor_description": "רמת איכות וידאו. ערכים אופייניים הם הערך 23 עבור H.264, הערך 28 עבור HEVC, הערך 31 עבור VP9, והערך 35 עבור AV1. נמוך יותר הוא טוב יותר, אבל מייצר קבצים גדולים יותר.",
|
||||
"transcoding_disabled_description": "אין להמיר את הקידוד של שום סרטון, עלול לגרום לכך שהניגון לא יפעל במכשירים מסוימים",
|
||||
"transcoding_encoding_options": "אפשרויות הקידוד",
|
||||
"transcoding_encoding_options_description": "הגדר מקודדים, רזולוציה, איכות ואפשרויות נוספות עבור הסרטונים המקודדים",
|
||||
"transcoding_encoding_options": "אפשרויות קידוד",
|
||||
"transcoding_encoding_options_description": "הגדר מקודדים, רזולוציה, איכות ואפשרויות אחרות עבור הסרטונים המקודדים",
|
||||
"transcoding_hardware_acceleration": "האצת חומרה",
|
||||
"transcoding_hardware_acceleration_description": "ניסיוני; המרה הרבה יותר מהירה, אבל תהיה באיכות נמוכה יותר באותו קצב סיביות",
|
||||
"transcoding_hardware_decoding": "פענוח חומרה",
|
||||
@@ -304,7 +304,7 @@
|
||||
"transcoding_max_keyframe_interval_description": "מגדיר את מרחק הפריימים המרבי בין תמונות מפתח. ערכים נמוכים גורעים את יעילות הדחיסה, אך משפרים את זמני החיפוש ועשויים לשפר את האיכות בסצנות עם תנועה מהירה. 0 מגדיר ערך זה באופן אוטומטי.",
|
||||
"transcoding_optimal_description": "סרטונים גבוהים מרזולוציית היעד או לא בפורמט מקובל",
|
||||
"transcoding_policy": "מדיניות המרה",
|
||||
"transcoding_policy_description": "הגדר מתי וידאו יעבור המרה",
|
||||
"transcoding_policy_description": "הגדר מתי סרטון יעבור המרת קידוד",
|
||||
"transcoding_preferred_hardware_device": "מכשיר חומרה מועדף",
|
||||
"transcoding_preferred_hardware_device_description": "חל רק על VAAPI ו-QSV. מגדיר את צומת ה-dri המשמש להמרת קידוד של חומרה.",
|
||||
"transcoding_preset_preset": "הגדרות קבועות מראש (-preset)",
|
||||
@@ -313,7 +313,7 @@
|
||||
"transcoding_reference_frames_description": "מספר הפריימים לייחוס בעת דחיסה של פריים נתון. ערכים גבוהים יותר משפרים את יעילות הדחיסה, אך מאטים את הקידוד. 0 מגדיר את הערך זה באופן אוטומטי.",
|
||||
"transcoding_required_description": "רק סרטונים שאינם בפורמט מקובל",
|
||||
"transcoding_settings": "הגדרות המרת קידוד סרטונים",
|
||||
"transcoding_settings_description": "נהל אילו סרטונים לעבד וכיצד לעבד אותם",
|
||||
"transcoding_settings_description": "נהל אילו סרטונים להמיר וכיצד לעבד אותם",
|
||||
"transcoding_target_resolution": "רזולוציה יעד",
|
||||
"transcoding_target_resolution_description": "רזולוציות גבוהות יותר יכולות לשמר פרטים רבים יותר אך לוקחות זמן רב יותר לקידוד, יש להן גדלי קבצים גדולים יותר, ויכולות להפחית את תגובתיות היישום.",
|
||||
"transcoding_temporal_aq": "Temporal AQ",
|
||||
@@ -326,7 +326,7 @@
|
||||
"transcoding_transcode_policy_description": "מדיניות לגבי מתי יש להמיר קידוד של סרטון. תמיד יומר הקידוד של סרטוני HDR (למעט אם המרת קידוד מושבתת).",
|
||||
"transcoding_two_pass_encoding": "קידוד בשני מעברים",
|
||||
"transcoding_two_pass_encoding_setting_description": "המר קידוד בשני מעברים כדי לייצר סרטונים מקודדים טוב יותר. כאשר קצב סיביות מרבי מופעל (נדרש כדי שזה יעבוד עם H.264 ו-HEVC), מצב זה משתמש בטווח קצב סיביות המבוסס על קצב הסיביות המרבי ומתעלם מ-CRF. עבור VP9, ניתן להשתמש ב-CRF אם קצב סיביות מרבי מושבת.",
|
||||
"transcoding_video_codec": "מקודדי וידאו",
|
||||
"transcoding_video_codec": "מקודד סרטון",
|
||||
"transcoding_video_codec_description": "ל-VP9 יש יעילות גבוהה ותאימות רשת, אבל לוקח יותר זמן להמיר את הקידוד עבורו. HEVC מתפקד באופן דומה, אך בעל תאימות רשת נמוכה יותר. H.264 תואם באופן נרחב ומהיר להמיר את קידודו, אבל הוא מייצר קבצים גדולים בהרבה. AV1 הוא הקידוד היעיל ביותר אך לוקה בתמיכה במכשירים ישנים יותר.",
|
||||
"trash_enabled_description": "הפעל את תכונות האשפה",
|
||||
"trash_number_of_days": "מספר הימים",
|
||||
@@ -522,12 +522,16 @@
|
||||
"date_of_birth_saved": "תאריך לידה נשמר בהצלחה",
|
||||
"date_range": "טווח תאריכים",
|
||||
"day": "יום",
|
||||
"deduplicate_all": "בטל כפילויות של הכל",
|
||||
"default_locale": "אזור שפה ברירת מחדל",
|
||||
"default_locale_description": "עצב תאריכים ומספרים על סמך אזור השפה של הדפדפן שלך",
|
||||
"deduplicate_all": "ביטול כל הכפילויות",
|
||||
"deduplication_criteria_1": "גודל תמונה בבתים",
|
||||
"deduplication_criteria_2": "ספירת נתוני EXIF",
|
||||
"deduplication_info": "מידע על ביטול כפילויות",
|
||||
"deduplication_info_description": "כדי לבחור מראש נכסים באופן אוטומטי ולהסיר כפילויות בכמות גדולה, אנו מסתכלים על:",
|
||||
"default_locale": "שפת ברירת מחדל",
|
||||
"default_locale_description": "פורמט תאריכים ומספרים מבוסס שפת הדפדפן שלך",
|
||||
"delete": "מחק",
|
||||
"delete_album": "מחק אלבום",
|
||||
"delete_api_key_prompt": "האם את/ה בטוח/ה שברצונך למחוק מפתח API זה?",
|
||||
"delete_api_key_prompt": "האם את/ה בטוח/ה שברצונך למחוק מפתח ה-API הזה?",
|
||||
"delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק לצמיתות את הכפילויות האלה?",
|
||||
"delete_key": "מחק מפתח",
|
||||
"delete_library": "מחק ספרייה",
|
||||
@@ -544,13 +548,13 @@
|
||||
"direction": "כיוון",
|
||||
"disabled": "מושבת",
|
||||
"disallow_edits": "אל תאפשר עריכות",
|
||||
"discord": "דיסקורד",
|
||||
"discover": "גלה",
|
||||
"dismiss_all_errors": "התעלם מכל השגיאות",
|
||||
"dismiss_error": "התעלם מהשגיאה",
|
||||
"display_options": "הצג אפשרויות",
|
||||
"discord": "Discord",
|
||||
"discover": "גילוי",
|
||||
"dismiss_all_errors": "התעלמות מכל השגיאות",
|
||||
"dismiss_error": "התעלמות מהשגיאה",
|
||||
"display_options": "הצגת אפשרויות",
|
||||
"display_order": "סדר תצוגה",
|
||||
"display_original_photos": "הצג תמונות מקוריות",
|
||||
"display_original_photos": "הצגת תמונות מקוריות",
|
||||
"display_original_photos_setting_description": "העדף להציג את התמונה המקורית בעת צפיית נכס במקום תמונות ממוזערות כאשר הנכס המקורי תומך בתצוגה בדפדפן. זה עלול לגרום לתמונות להיות מוצגות באיטיות.",
|
||||
"do_not_show_again": "אל תציג את ההודעה הזאת שוב",
|
||||
"documentation": "תיעוד",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user