mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
Compare commits
265 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c7bd28864 | |||
| 832ed4d015 | |||
| 238895cad9 | |||
| e2ec04e86c | |||
| 6050526360 | |||
| bfd76570c5 | |||
| 37e6a49652 | |||
| 36caeb34ec | |||
| 87713c7f2f | |||
| 2039c129f2 | |||
| 52b00b0bad | |||
| 21af184045 | |||
| 1fcc2b704b | |||
| 7de73dc176 | |||
| fe2bf0c6dd | |||
| d4a97f2d25 | |||
| bd58db4fcc | |||
| 7f43c6a3a3 | |||
| 87175ee56c | |||
| 13587bf13c | |||
| f09769a2f3 | |||
| bfdff12ee0 | |||
| eb6dca6a31 | |||
| c2e3739a58 | |||
| f6bd514cdc | |||
| d93ab7707e | |||
| 6bb47c802f | |||
| 90a69e2ba6 | |||
| 6580394cfe | |||
| 42ff3b705d | |||
| 0f00053bb1 | |||
| c5c59ed040 | |||
| 576b1eb999 | |||
| 24189702da | |||
| ad0d01005e | |||
| 3e6d053f93 | |||
| 1bb3fd985f | |||
| f72aa54a1f | |||
| dafe9d7966 | |||
| 7acda0572d | |||
| 98bc9f6a6e | |||
| 63a3b405c3 | |||
| 0058df798d | |||
| 97100a4362 | |||
| af39384efb | |||
| 01712cf0a7 | |||
| 2015f95ff5 | |||
| d4f29ab6ac | |||
| 3decc864b5 | |||
| eca0e60db8 | |||
| 8cff5883b5 | |||
| 3d320d9751 | |||
| b9e0e65bdb | |||
| 88e5e8d6ea | |||
| ee107c98d5 | |||
| affe0ac5ee | |||
| f1d8ab8aae | |||
| c0898b96ca | |||
| 5e9bda7fab | |||
| b60e9c6771 | |||
| b554664791 | |||
| 97c62136b7 | |||
| c1051c7ed2 | |||
| 65bd0a9320 | |||
| bf32864644 | |||
| 7ef7ecec5b | |||
| bc4abd18e4 | |||
| b74cfd4424 | |||
| 7dc84f56c0 | |||
| 92634f923b | |||
| 96b6165bd3 | |||
| 2624f3884f | |||
| f9b7ce9407 | |||
| 013ea37a0d | |||
| b2b4385271 | |||
| 081c75bb21 | |||
| da337578fb | |||
| acf4109171 | |||
| 66601a1fdc | |||
| 02ff077367 | |||
| 94bb6c1a5e | |||
| fe9e5afcf4 | |||
| 5e89efba64 | |||
| 5a457d72c9 | |||
| 45ccdb37fb | |||
| 9263e2f2e1 | |||
| a3ee615c5b | |||
| 39cfad7136 | |||
| 350056dd1a | |||
| f0835d06f8 | |||
| 03b70cf029 | |||
| 4bfb8b36c2 | |||
| dfacde5af8 | |||
| 317afe9e3b | |||
| 1fb5f13237 | |||
| 793a7054fb | |||
| 3a874dd441 | |||
| 3dc7dc93d8 | |||
| 70397dc5a6 | |||
| a16d233a0c | |||
| bb0872afef | |||
| b9ca68f6e4 | |||
| 837305da7e | |||
| e20fb44142 | |||
| c2786978cd | |||
| 312bb91a4f | |||
| c1934b904c | |||
| 47752d158a | |||
| 6267322b9c | |||
| 93c3cd49f3 | |||
| f52825ab08 | |||
| d74dc74f92 | |||
| 539a39ae49 | |||
| f68cd424a7 | |||
| 20c0cc7e73 | |||
| be1b9a5f67 | |||
| d9011c0829 | |||
| f909648bce | |||
| c78b1d8ab4 | |||
| 94a34436a3 | |||
| 0eef15a3ab | |||
| 6982896549 | |||
| 2c812a2561 | |||
| 0b1188e42e | |||
| be20cd2bf9 | |||
| b8591cb591 | |||
| 384d3a0984 | |||
| 03af669856 | |||
| b0e4850d76 | |||
| 36ebcaf00c | |||
| 7a86f2b7b9 | |||
| 55f2b3b6a0 | |||
| fd5e8d6521 | |||
| 6798d5df32 | |||
| 9d33853544 | |||
| a46e46452c | |||
| dbf30b77bf | |||
| 8afca348ff | |||
| 2070f775d6 | |||
| a456a05052 | |||
| b7eff33f90 | |||
| 18c0228f1b | |||
| 2f8be45fe0 | |||
| 41968fdcac | |||
| 79c392ceba | |||
| 8fbeb64c59 | |||
| 7d181f0686 | |||
| 2172dde7dc | |||
| fce220b1d7 | |||
| 2a47c35eb7 | |||
| 6aadb7b5bd | |||
| 88bce52042 | |||
| d046f16860 | |||
| 88815a0345 | |||
| 57212f29bf | |||
| 95fa8fbdab | |||
| 687b7cad6f | |||
| ac2ebcee37 | |||
| 3356e81c85 | |||
| 9c642bd6fc | |||
| 9da0cb3cf4 | |||
| 4ff6cca4da | |||
| 2b7ae4981f | |||
| e63df4121a | |||
| 03b4ab2935 | |||
| facd3bd331 | |||
| 20ddf2e7d2 | |||
| 7f0025b3fc | |||
| 60f4dedb29 | |||
| d5d2ebd9bf | |||
| 37abbeba52 | |||
| 50557002b7 | |||
| 4aa31d38bf | |||
| 3d8df74b43 | |||
| 2ff9f95527 | |||
| a69eecf3bc | |||
| 4ffa26c969 | |||
| ac06514db5 | |||
| 792cb9148b | |||
| 8ee5d3039a | |||
| d410131312 | |||
| 5334a6254a | |||
| 79fccdbee0 | |||
| 6dd6053222 | |||
| 8454cb2631 | |||
| 603fc7401f | |||
| ed70e0febf | |||
| 5f5e3344d5 | |||
| 6da2d3d587 | |||
| 41d2d84b21 | |||
| 6ba17bb86f | |||
| e1a84d3ab6 | |||
| 7d8f843be6 | |||
| 3753b7a4d1 | |||
| 84a1fb27ca | |||
| 81780b0cc0 | |||
| 5e81a5a054 | |||
| e4e2f586b5 | |||
| a001adf14a | |||
| 136814540a | |||
| fed5cc1ae1 | |||
| 641ab51b80 | |||
| 3b47ca1c37 | |||
| 8fb2c7755d | |||
| 1ba0989e15 | |||
| daed3f0966 | |||
| 46d612ad8c | |||
| 513dead2c2 | |||
| ca006c1569 | |||
| 4e8e8304fd | |||
| d377d2e145 | |||
| 9c9feddf7d | |||
| bfcf34d8b5 | |||
| 95e57a24cb | |||
| eada662981 | |||
| 352f6ecc28 | |||
| bee49cef02 | |||
| 6d0c6a4008 | |||
| 8a975e5ea9 | |||
| d39e7da10d | |||
| bc400d68ac | |||
| d7f038ec60 | |||
| 26957f37ce | |||
| 3254d31cd2 | |||
| 7b269d1638 | |||
| b5bed02300 | |||
| 5553910236 | |||
| 8d67c1f820 | |||
| ed0ec30917 | |||
| 2b0f6c9202 | |||
| 55ab8c65b6 | |||
| 781d568f29 | |||
| 6a361dae72 | |||
| 64766c8c06 | |||
| 6a63e814a5 | |||
| 6441c3b77c | |||
| b03a649e74 | |||
| 2903b2653b | |||
| 9ba9a22c40 | |||
| f1882c2926 | |||
| 4278789083 | |||
| 921c8a8de3 | |||
| afec61addc | |||
| a1a03efbcd | |||
| 1d0e5cf18d | |||
| de9ec95db1 | |||
| 7f784952eb | |||
| 3d6c7ba353 | |||
| 3be97db118 | |||
| 8f3a99ffbc | |||
| e6d114af10 | |||
| 4e28811f09 | |||
| 4987032e62 | |||
| 572bad8ede | |||
| 95c1f0efeb | |||
| fbe631fe91 | |||
| 2143a0c935 | |||
| 136bd1e2eb | |||
| 564065a3ed | |||
| 9bcce59719 | |||
| cd86a83c33 | |||
| f29c06799f | |||
| 6fcf651d76 | |||
| 196307bca5 | |||
| 776b9cbad5 |
@@ -6,6 +6,12 @@ mobile/openapi/**/*.dart linguist-generated=true
|
|||||||
mobile/lib/**/*.g.dart -diff -merge
|
mobile/lib/**/*.g.dart -diff -merge
|
||||||
mobile/lib/**/*.g.dart linguist-generated=true
|
mobile/lib/**/*.g.dart linguist-generated=true
|
||||||
|
|
||||||
|
mobile/android/**/*.g.kt -diff -merge
|
||||||
|
mobile/android/**/*.g.kt linguist-generated=true
|
||||||
|
|
||||||
|
mobile/ios/**/*.g.swift -diff -merge
|
||||||
|
mobile/ios/**/*.g.swift linguist-generated=true
|
||||||
|
|
||||||
mobile/lib/**/*.drift.dart -diff -merge
|
mobile/lib/**/*.drift.dart -diff -merge
|
||||||
mobile/lib/**/*.drift.dart linguist-generated=true
|
mobile/lib/**/*.drift.dart linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
24.14.1
|
24.15.0
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
gh api graphql \
|
gh api graphql \
|
||||||
-f prId="$NODE_ID" \
|
-f prId="$NODE_ID" \
|
||||||
-f body="This PR has been automatically closed as the description doesn't follow our template. After you edit it to match the template, the PR will automatically be reopened." \
|
-f body="This PR has been automatically closed as the description doesn't follow [our template](https://github.com/immich-app/immich/blob/main/.github/pull_request_template.md). After you edit it to match the template, the PR will automatically be reopened." \
|
||||||
-f query='
|
-f query='
|
||||||
mutation CommentAndClosePR($prId: ID!, $body: String!) {
|
mutation CommentAndClosePR($prId: ID!, $body: String!) {
|
||||||
addComment(input: {
|
addComment(input: {
|
||||||
|
|||||||
@@ -51,14 +51,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -79,9 +79,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -103,7 +103,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Restore Gradle Cache
|
- name: Restore Gradle Cache
|
||||||
id: cache-gradle-restore
|
id: cache-gradle-restore
|
||||||
uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
~/.gradle/caches
|
~/.gradle/caches
|
||||||
@@ -121,7 +121,7 @@ jobs:
|
|||||||
cache: true
|
cache: true
|
||||||
|
|
||||||
- name: Setup Android SDK
|
- name: Setup Android SDK
|
||||||
uses: android-actions/setup-android@9fc6c4e9069bf8d3d10b2204b1fb8f6ef7065407 # v3.2.2
|
uses: android-actions/setup-android@40fd30fb8d7440372e1316f5d1809ec01dcd3699 # v4.0.1
|
||||||
with:
|
with:
|
||||||
packages: ''
|
packages: ''
|
||||||
|
|
||||||
@@ -153,14 +153,14 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Publish Android Artifact
|
- name: Publish Android Artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
path: mobile/build/app/outputs/flutter-apk/*.apk
|
path: mobile/build/app/outputs/flutter-apk/*.apk
|
||||||
|
|
||||||
- name: Save Gradle Cache
|
- name: Save Gradle Cache
|
||||||
id: cache-gradle-save
|
id: cache-gradle-save
|
||||||
uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
|
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
|
||||||
if: github.ref == 'refs/heads/main'
|
if: github.ref == 'refs/heads/main'
|
||||||
with:
|
with:
|
||||||
path: |
|
path: |
|
||||||
@@ -210,7 +210,7 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Setup Ruby
|
- name: Setup Ruby
|
||||||
uses: ruby/setup-ruby@c515ec17f69368147deb311832da000dd229d338 # v1.297.0
|
uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
|
||||||
with:
|
with:
|
||||||
ruby-version: '3.3'
|
ruby-version: '3.3'
|
||||||
bundler-cache: true
|
bundler-cache: true
|
||||||
@@ -291,7 +291,7 @@ jobs:
|
|||||||
security delete-keychain build.keychain || true
|
security delete-keychain build.keychain || true
|
||||||
|
|
||||||
- name: Upload IPA artifact
|
- name: Upload IPA artifact
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: ios-release-ipa
|
name: ios-release-ipa
|
||||||
path: mobile/ios/Runner.ipa
|
path: mobile/ios/Runner.ipa
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Check for breaking API changes
|
- name: Check for breaking API changes
|
||||||
uses: oasdiff/oasdiff-action/breaking@1f38ea5ea0b4a2e4e49901c3bcdf4386a05e9ea1 # v0.0.37
|
uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
|
||||||
with:
|
with:
|
||||||
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
|
||||||
revision: open-api/immich-openapi-specs.json
|
revision: open-api/immich-openapi-specs.json
|
||||||
|
|||||||
@@ -31,9 +31,9 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
@@ -71,9 +71,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -89,7 +89,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -115,7 +115,7 @@ jobs:
|
|||||||
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
type=raw,value=latest,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
- name: Build and push image
|
- name: Build and push image
|
||||||
uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0
|
uses: docker/build-push-action@bcafcacb16a39f128d818304e6c9c0c18556b85f # v7.1.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ jobs:
|
|||||||
needs: [get_body, should_run]
|
needs: [get_body, should_run]
|
||||||
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
if: ${{ needs.should_run.outputs.should_run == 'true' }}
|
||||||
container:
|
container:
|
||||||
image: ghcr.io/immich-app/mdq:main@sha256:df7188ba88abb0800d73cc97d3633280f0c0c3d4c441d678225067bf154150fb
|
image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
|
||||||
outputs:
|
outputs:
|
||||||
checked: ${{ steps.get_checkbox.outputs.checked }}
|
checked: ${{ steps.get_checkbox.outputs.checked }}
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -44,9 +44,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
@@ -57,7 +57,7 @@ jobs:
|
|||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/init@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
languages: ${{ matrix.language }}
|
languages: ${{ matrix.language }}
|
||||||
# If you wish to specify custom queries, you can do so here or in a config file.
|
# If you wish to specify custom queries, you can do so here or in a config file.
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
|
||||||
# If this step fails, then you should remove it and run the build manually (see below)
|
# If this step fails, then you should remove it and run the build manually (see below)
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/autobuild@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
|
|
||||||
# ℹ️ Command-line programs to run using the OS shell.
|
# ℹ️ Command-line programs to run using the OS shell.
|
||||||
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
|
||||||
@@ -83,6 +83,6 @@ jobs:
|
|||||||
# ./location_of_script_within_repo/buildscript.sh
|
# ./location_of_script_within_repo/buildscript.sh
|
||||||
|
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@38697555549f1db7851b81482ff19f1fa5c4fedc # v4.34.1
|
uses: github/codeql-action/analyze@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4.35.2
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
@@ -23,14 +23,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -60,7 +60,7 @@ jobs:
|
|||||||
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
suffix: ['', '-cuda', '-rocm', '-openvino', '-armnn', '-rknn']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -90,7 +90,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4.0.0
|
uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -132,7 +132,7 @@ jobs:
|
|||||||
suffixes: '-rocm'
|
suffixes: '-rocm'
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
runner-mapping: '{"linux/amd64": "pokedex-large"}'
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -155,7 +155,7 @@ jobs:
|
|||||||
name: Build and Push Server
|
name: Build and Push Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
|
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
@@ -178,7 +178,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|
||||||
@@ -189,6 +189,6 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|||||||
@@ -21,14 +21,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -54,9 +54,9 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
run: pnpm build
|
run: pnpm build
|
||||||
|
|
||||||
- name: Upload build output
|
- name: Upload build output
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
with:
|
with:
|
||||||
name: docs-build-output
|
name: docs-build-output
|
||||||
path: docs/build/
|
path: docs/build/
|
||||||
|
|||||||
@@ -20,16 +20,16 @@ jobs:
|
|||||||
artifact: ${{ steps.get-artifact.outputs.result }}
|
artifact: ${{ steps.get-artifact.outputs.result }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
|
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
|
||||||
run: echo 'The triggering workflow did not succeed' && exit 1
|
run: echo 'The triggering workflow did not succeed' && exit 1
|
||||||
- name: Get artifact
|
- name: Get artifact
|
||||||
id: get-artifact
|
id: get-artifact
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -48,7 +48,7 @@ jobs:
|
|||||||
return { found: true, id: matchArtifact.id };
|
return { found: true, id: matchArtifact.id };
|
||||||
- name: Determine deploy parameters
|
- name: Determine deploy parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
env:
|
env:
|
||||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
with:
|
with:
|
||||||
@@ -119,9 +119,9 @@ jobs:
|
|||||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -131,11 +131,13 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||||
|
with:
|
||||||
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Load parameters
|
- name: Load parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
env:
|
env:
|
||||||
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
||||||
with:
|
with:
|
||||||
@@ -147,7 +149,7 @@ jobs:
|
|||||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
env:
|
env:
|
||||||
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
||||||
with:
|
with:
|
||||||
@@ -211,7 +213,7 @@ jobs:
|
|||||||
run: 'mise run //deployment:tf apply'
|
run: 'mise run //deployment:tf apply'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
||||||
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -29,7 +29,9 @@ jobs:
|
|||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Setup Mise
|
- name: Setup Mise
|
||||||
uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
|
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||||
|
with:
|
||||||
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
|
||||||
- name: Destroy Docs Subdomain
|
- name: Destroy Docs Subdomain
|
||||||
env:
|
env:
|
||||||
@@ -42,7 +44,7 @@ jobs:
|
|||||||
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
run: 'mise run //deployment:tf destroy -- -refresh=false'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
uses: actions-cool/maintain-one-comment@909842216bc8e8658364c572ec52100f4c2cc50a # v3.3.0
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
number: ${{ github.event.number }}
|
number: ${{ github.event.number }}
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: 'Checkout'
|
- name: 'Checkout'
|
||||||
@@ -29,10 +29,10 @@ jobs:
|
|||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -42,13 +42,13 @@ jobs:
|
|||||||
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
|
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
|
||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
|
||||||
with:
|
with:
|
||||||
default_author: github_actions
|
default_author: github_actions
|
||||||
message: 'chore: fix formatting'
|
message: 'chore: fix formatting'
|
||||||
|
|
||||||
- name: Remove label
|
- name: Remove label
|
||||||
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
workflow_call:
|
||||||
secrets:
|
secrets:
|
||||||
PUSH_O_MATIC_APP_ID:
|
PUSH_O_MATIC_APP_CLIENT_ID:
|
||||||
required: true
|
required: true
|
||||||
PUSH_O_MATIC_APP_KEY:
|
PUSH_O_MATIC_APP_KEY:
|
||||||
required: true
|
required: true
|
||||||
@@ -31,9 +31,9 @@ jobs:
|
|||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate_token
|
id: generate_token
|
||||||
if: ${{ inputs.skip != true }}
|
if: ${{ inputs.skip != true }}
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Find translation PR
|
- name: Find translation PR
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Require PR to have a changelog label
|
- name: Require PR to have a changelog label
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
secrets:
|
secrets:
|
||||||
PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
|
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
|
||||||
|
|
||||||
@@ -50,9 +50,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -63,13 +63,13 @@ jobs:
|
|||||||
ref: main
|
ref: main
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@26f6d4f2c533a43e6b5da0b4a5dd983f98f7b49a # v6.0.4
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -86,7 +86,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Commit and tag
|
- name: Commit and tag
|
||||||
id: push-tag
|
id: push-tag
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
uses: EndBug/add-and-commit@290ea2c423ad77ca9c62ae0f5b224379612c0321 # v10.0.0
|
||||||
with:
|
with:
|
||||||
default_author: github_actions
|
default_author: github_actions
|
||||||
message: 'chore: version ${{ steps.output.outputs.version }}'
|
message: 'chore: version ${{ steps.output.outputs.version }}'
|
||||||
@@ -124,9 +124,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
|
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -142,7 +142,7 @@ jobs:
|
|||||||
github-token: ${{ steps.generate-token.outputs.token }}
|
github-token: ${{ steps.generate-token.outputs.token }}
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2.6.2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ needs.bump_version.outputs.version }}
|
tag_name: ${{ needs.bump_version.outputs.version }}
|
||||||
|
|||||||
@@ -14,12 +14,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
message-id: 'preview-status'
|
message-id: 'preview-status'
|
||||||
@@ -32,12 +32,12 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
|
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
script: |
|
script: |
|
||||||
@@ -48,14 +48,14 @@ jobs:
|
|||||||
name: 'preview'
|
name: 'preview'
|
||||||
})
|
})
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||||
if: ${{ github.event.pull_request.head.repo.fork }}
|
if: ${{ github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
message-id: 'preview-status'
|
message-id: 'preview-status'
|
||||||
message: 'PRs from forks cannot have preview environments.'
|
message: 'PRs from forks cannot have preview environments.'
|
||||||
|
|
||||||
- uses: mshick/add-pr-comment@ffd016c7e151d97d69d21a843022fd4cd5b96fe5 # v3.9.0
|
- uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
|
|||||||
@@ -19,9 +19,9 @@ jobs:
|
|||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
- uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
node-version-file: './open-api/typescript-sdk/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|||||||
@@ -20,14 +20,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -49,9 +49,9 @@ jobs:
|
|||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
|||||||
+79
-70
@@ -17,14 +17,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -34,13 +34,17 @@ jobs:
|
|||||||
- 'web/**'
|
- 'web/**'
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
- 'open-api/typescript-sdk/**'
|
- 'open-api/typescript-sdk/**'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
server:
|
server:
|
||||||
- 'server/**'
|
- 'server/**'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
cli:
|
cli:
|
||||||
- 'cli/**'
|
- 'cli/**'
|
||||||
- 'open-api/typescript-sdk/**'
|
- 'open-api/typescript-sdk/**'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
e2e:
|
e2e:
|
||||||
- 'e2e/**'
|
- 'e2e/**'
|
||||||
|
- 'pnpm-lock.yaml'
|
||||||
mobile:
|
mobile:
|
||||||
- 'mobile/**'
|
- 'mobile/**'
|
||||||
machine-learning:
|
machine-learning:
|
||||||
@@ -63,9 +67,9 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -77,7 +81,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -108,9 +112,9 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -121,7 +125,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -155,9 +159,9 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -168,7 +172,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -197,9 +201,9 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -210,7 +214,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -241,9 +245,9 @@ jobs:
|
|||||||
working-directory: ./web
|
working-directory: ./web
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -254,7 +258,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -279,9 +283,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -292,7 +296,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -327,9 +331,9 @@ jobs:
|
|||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -340,7 +344,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -373,9 +377,9 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -387,11 +391,15 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
cache-dependency-path: '**/pnpm-lock.yaml'
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
- name: Setup Mise
|
||||||
|
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||||
|
with:
|
||||||
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
- name: Run pnpm install
|
- name: Run pnpm install
|
||||||
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
|
||||||
- name: Run medium tests
|
- name: Run medium tests
|
||||||
@@ -412,9 +420,9 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -426,7 +434,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -464,7 +472,7 @@ jobs:
|
|||||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
- name: Archive Docker logs
|
- name: Archive Docker logs
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: e2e-server-docker-logs-${{ matrix.runner }}
|
name: e2e-server-docker-logs-${{ matrix.runner }}
|
||||||
@@ -484,9 +492,9 @@ jobs:
|
|||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -498,7 +506,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -522,7 +530,7 @@ jobs:
|
|||||||
run: pnpm test:web
|
run: pnpm test:web
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive e2e test (web) results
|
- name: Archive e2e test (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-web-test-results-${{ matrix.runner }}
|
name: e2e-web-test-results-${{ matrix.runner }}
|
||||||
@@ -533,7 +541,7 @@ jobs:
|
|||||||
run: pnpm test:web:ui
|
run: pnpm test:web:ui
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive ui test (web) results
|
- name: Archive ui test (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-ui-test-results-${{ matrix.runner }}
|
name: e2e-ui-test-results-${{ matrix.runner }}
|
||||||
@@ -544,7 +552,7 @@ jobs:
|
|||||||
run: pnpm test:web:maintenance
|
run: pnpm test:web:maintenance
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
- name: Archive maintenance tests (web) results
|
- name: Archive maintenance tests (web) results
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: success() || failure()
|
if: success() || failure()
|
||||||
with:
|
with:
|
||||||
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
|
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
|
||||||
@@ -554,7 +562,7 @@ jobs:
|
|||||||
run: docker compose logs --no-color > docker-compose-logs.txt
|
run: docker compose logs --no-color > docker-compose-logs.txt
|
||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
- name: Archive Docker logs
|
- name: Archive Docker logs
|
||||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
name: e2e-web-docker-logs-${{ matrix.runner }}
|
name: e2e-web-docker-logs-${{ matrix.runner }}
|
||||||
@@ -566,7 +574,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
@@ -578,9 +586,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -605,39 +613,40 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
env:
|
||||||
|
MISE_CEILING_PATHS: ${{ github.workspace }}
|
||||||
|
MISE_TRUSTED_CONFIG_PATHS: ${{ github.workspace }}/machine-learning/mise.toml
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
token: ${{ steps.token.outputs.token }}
|
token: ${{ steps.token.outputs.token }}
|
||||||
- name: Install uv
|
|
||||||
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
|
- name: Setup Mise
|
||||||
|
uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
python-version: 3.11
|
github_token: ${{ steps.token.outputs.token }}
|
||||||
|
working_directory: ./machine-learning
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: mise run install --extra cpu
|
||||||
uv sync --extra cpu
|
- name: Lint code
|
||||||
- name: Lint with ruff
|
run: mise run lint --output-format=github
|
||||||
run: |
|
- name: Format code
|
||||||
uv run ruff check --output-format=github immich_ml
|
run: mise run format
|
||||||
- name: Format with ruff
|
- name: Run type checking
|
||||||
run: |
|
run: mise run check
|
||||||
uv run ruff format --check immich_ml
|
|
||||||
- name: Run mypy type checking
|
|
||||||
run: |
|
|
||||||
uv run mypy --strict immich_ml/
|
|
||||||
- name: Run tests and coverage
|
- name: Run tests and coverage
|
||||||
run: |
|
run: mise run test
|
||||||
uv run pytest --cov=immich_ml --cov-report term-missing
|
|
||||||
github-files-formatting:
|
github-files-formatting:
|
||||||
name: .github Files Formatting
|
name: .github Files Formatting
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -650,9 +659,9 @@ jobs:
|
|||||||
working-directory: ./.github
|
working-directory: ./.github
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -663,7 +672,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './.github/.nvmrc'
|
node-version-file: './.github/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -680,9 +689,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||||
@@ -701,9 +710,9 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -714,7 +723,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
@@ -763,9 +772,9 @@ jobs:
|
|||||||
working-directory: ./server
|
working-directory: ./server
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
@@ -776,7 +785,7 @@ jobs:
|
|||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
cache: 'pnpm'
|
cache: 'pnpm'
|
||||||
|
|||||||
@@ -24,14 +24,14 @@ jobs:
|
|||||||
should_run: ${{ steps.check.outputs.should_run }}
|
should_run: ${{ steps.check.outputs.should_run }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Check what should run
|
- name: Check what should run
|
||||||
id: check
|
id: check
|
||||||
uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
|
uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
|
||||||
with:
|
with:
|
||||||
github-token: ${{ steps.token.outputs.token }}
|
github-token: ${{ steps.token.outputs.token }}
|
||||||
filters: |
|
filters: |
|
||||||
@@ -47,9 +47,9 @@ jobs:
|
|||||||
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
|
||||||
steps:
|
steps:
|
||||||
- id: token
|
- id: token
|
||||||
uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
|
uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Bot review status
|
- name: Bot review status
|
||||||
@@ -68,6 +68,6 @@ jobs:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
|
- uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
|
||||||
with:
|
with:
|
||||||
needs: ${{ toJSON(needs) }}
|
needs: ${{ toJSON(needs) }}
|
||||||
|
|||||||
@@ -28,3 +28,5 @@ vite.config.js.timestamp-*
|
|||||||
.pnpm-store
|
.pnpm-store
|
||||||
.devcontainer/library
|
.devcontainer/library
|
||||||
.devcontainer/.env*
|
.devcontainer/.env*
|
||||||
|
*.tsbuildinfo
|
||||||
|
*.tsbuildInfo
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
[submodule "mobile/.isar"]
|
|
||||||
path = mobile/.isar
|
|
||||||
url = https://github.com/isar/isar
|
|
||||||
[submodule "e2e/test-assets"]
|
[submodule "e2e/test-assets"]
|
||||||
path = e2e/test-assets
|
path = e2e/test-assets
|
||||||
url = https://github.com/immich-app/test-assets
|
url = https://github.com/immich-app/test-assets
|
||||||
|
|||||||
Vendored
+5
-13
@@ -13,10 +13,6 @@
|
|||||||
"editor.wordBasedSuggestions": "off"
|
"editor.wordBasedSuggestions": "off"
|
||||||
},
|
},
|
||||||
"[javascript]": {
|
"[javascript]": {
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
@@ -29,18 +25,14 @@
|
|||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
"[svelte]": {
|
"[svelte]": {
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true,
|
||||||
|
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||||
|
},
|
||||||
|
"svelte.plugin.svelte.compilerWarnings": {
|
||||||
|
"state_referenced_locally": "ignore"
|
||||||
},
|
},
|
||||||
"[typescript]": {
|
"[typescript]": {
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true
|
"editor.formatOnSave": true
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
24.14.1
|
24.15.0
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.6.3",
|
"version": "2.7.5",
|
||||||
"description": "Command Line Interface (CLI) for Immich",
|
"description": "Command Line Interface (CLI) for Immich",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"exports": "./dist/index.js",
|
"exports": "./dist/index.js",
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
"@types/micromatch": "^4.0.9",
|
"@types/micromatch": "^4.0.9",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.2",
|
||||||
"@vitest/coverage-v8": "^4.0.0",
|
"@vitest/coverage-v8": "^4.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -28,13 +28,13 @@
|
|||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^63.0.0",
|
"eslint-plugin-unicorn": "^64.0.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"prettier-plugin-organize-imports": "^4.0.0",
|
"prettier-plugin-organize-imports": "^4.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^6.0.0",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.0",
|
"vite": "^8.0.0",
|
||||||
"vitest": "^4.0.0",
|
"vitest": "^4.0.0",
|
||||||
"vitest-fetch-mock": "^0.4.0",
|
"vitest-fetch-mock": "^0.4.0",
|
||||||
@@ -68,6 +68,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.14.1"
|
"node": "24.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from 'node:path';
|
|||||||
import { setTimeout as sleep } from 'node:timers/promises';
|
import { setTimeout as sleep } from 'node:timers/promises';
|
||||||
import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
import { describe, expect, it, MockedFunction, vi } from 'vitest';
|
||||||
|
|
||||||
import { Action, checkBulkUpload, defaults, getSupportedMediaTypes, Reason } from '@immich/sdk';
|
import { AssetRejectReason, AssetUploadAction, checkBulkUpload, defaults, getSupportedMediaTypes } from '@immich/sdk';
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -120,7 +120,7 @@ describe('checkForDuplicates', () => {
|
|||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
action: Action.Accept,
|
action: AssetUploadAction.Accept,
|
||||||
id: testFilePath,
|
id: testFilePath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -144,10 +144,10 @@ describe('checkForDuplicates', () => {
|
|||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
action: Action.Reject,
|
action: AssetUploadAction.Reject,
|
||||||
id: testFilePath,
|
id: testFilePath,
|
||||||
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
assetId: 'fc5621b1-86f6-44a1-9905-403e607df9f5',
|
||||||
reason: Reason.Duplicate,
|
reason: AssetRejectReason.Duplicate,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -167,7 +167,7 @@ describe('checkForDuplicates', () => {
|
|||||||
vi.mocked(checkBulkUpload).mockResolvedValue({
|
vi.mocked(checkBulkUpload).mockResolvedValue({
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
action: Action.Accept,
|
action: AssetUploadAction.Accept,
|
||||||
id: testFilePath,
|
id: testFilePath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@@ -187,7 +187,7 @@ describe('checkForDuplicates', () => {
|
|||||||
mocked.mockResolvedValue({
|
mocked.mockResolvedValue({
|
||||||
results: [
|
results: [
|
||||||
{
|
{
|
||||||
action: Action.Accept,
|
action: AssetUploadAction.Accept,
|
||||||
id: testFilePath,
|
id: testFilePath,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import {
|
import {
|
||||||
Action,
|
|
||||||
AssetBulkUploadCheckItem,
|
AssetBulkUploadCheckItem,
|
||||||
AssetBulkUploadCheckResult,
|
AssetBulkUploadCheckResult,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
AssetMediaStatus,
|
AssetMediaStatus,
|
||||||
|
AssetUploadAction,
|
||||||
Permission,
|
Permission,
|
||||||
addAssetsToAlbum,
|
addAssetsToAlbum,
|
||||||
checkBulkUpload,
|
checkBulkUpload,
|
||||||
@@ -234,7 +234,7 @@ export const checkForDuplicates = async (files: string[], { concurrency, skipHas
|
|||||||
const results = response.results as AssetBulkUploadCheckResults;
|
const results = response.results as AssetBulkUploadCheckResults;
|
||||||
|
|
||||||
for (const { id: filepath, assetId, action } of results) {
|
for (const { id: filepath, assetId, action } of results) {
|
||||||
if (action === Action.Accept) {
|
if (action === AssetUploadAction.Accept) {
|
||||||
newFiles.push(filepath);
|
newFiles.push(filepath);
|
||||||
} else {
|
} else {
|
||||||
// rejects are always duplicates
|
// rejects are always duplicates
|
||||||
@@ -404,8 +404,6 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
|||||||
const { baseUrl, headers } = defaults;
|
const { baseUrl, headers } = defaults;
|
||||||
|
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('deviceAssetId', `${basename(input)}-${stats.size}`.replaceAll(/\s+/g, ''));
|
|
||||||
formData.append('deviceId', 'CLI');
|
|
||||||
formData.append('fileCreatedAt', stats.mtime.toISOString());
|
formData.append('fileCreatedAt', stats.mtime.toISOString());
|
||||||
formData.append('fileModifiedAt', stats.mtime.toISOString());
|
formData.append('fileModifiedAt', stats.mtime.toISOString());
|
||||||
formData.append('fileSize', String(stats.size));
|
formData.append('fileSize', String(stats.size));
|
||||||
|
|||||||
+6
-2
@@ -15,8 +15,12 @@
|
|||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"baseUrl": "./",
|
"rootDir": "./src",
|
||||||
|
"paths": {
|
||||||
|
"src/*": ["./src/*"],
|
||||||
|
},
|
||||||
|
"tsBuildInfoFile": "./dist/tsconfig.tsbuildinfo",
|
||||||
"types": ["vitest/globals"]
|
"types": ["vitest/globals"]
|
||||||
},
|
},
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules", "vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[tools]
|
[tools]
|
||||||
terragrunt = "0.99.4"
|
terragrunt = "1.0.3"
|
||||||
opentofu = "1.11.5"
|
opentofu = "1.11.6"
|
||||||
|
|
||||||
[tasks."tg:fmt"]
|
[tasks."tg:fmt"]
|
||||||
run = "terragrunt hclfmt"
|
run = "terragrunt hclfmt"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ services:
|
|||||||
- /tmp
|
- /tmp
|
||||||
volumes:
|
volumes:
|
||||||
- ..:/usr/src/app
|
- ..:/usr/src/app
|
||||||
|
# - ../../ui:/usr/src/ui
|
||||||
- pnpm_cache:/buildcache/pnpm_cache
|
- pnpm_cache:/buildcache/pnpm_cache
|
||||||
- server_node_modules:/usr/src/app/server/node_modules
|
- server_node_modules:/usr/src/app/server/node_modules
|
||||||
- web_node_modules:/usr/src/app/web/node_modules
|
- web_node_modules:/usr/src/app/web/node_modules
|
||||||
@@ -156,7 +157,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
|
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
|
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
@@ -85,7 +85,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:4a61322ac1103a0e3aea2a61ef1718422a48fa046441f299d71e660a3bc71ae9
|
image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -97,7 +97,7 @@ services:
|
|||||||
command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.4.2-ubuntu@sha256:78839fe49e1425c02416fa8072591533a72bd9598e563b54a07d78f9e27fb5d3
|
image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
|
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
||||||
user: '1000:1000'
|
user: '1000:1000'
|
||||||
security_opt:
|
security_opt:
|
||||||
- no-new-privileges:true
|
- no-new-privileges:true
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich_redis
|
container_name: immich_redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
|
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
restart: always
|
restart: always
|
||||||
|
|||||||
+1
-1
@@ -1 +1 @@
|
|||||||
24.14.1
|
24.15.0
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ The provided restore process ensures your database is never in a broken state by
|
|||||||
|
|
||||||
## Filesystem
|
## Filesystem
|
||||||
|
|
||||||
Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
|
Immich does not handle filesystem backups for you. You have to arrange these yourself! Immich stores two types of content in the filesystem: (a) original, unmodified assets (photos and videos), and (b) generated content. We recommend backing up the entire contents of `UPLOAD_LOCATION`, but only the original content is critical, which is stored in the following folders:
|
||||||
|
|
||||||
1. `UPLOAD_LOCATION/library`
|
1. `UPLOAD_LOCATION/library`
|
||||||
2. `UPLOAD_LOCATION/upload`
|
2. `UPLOAD_LOCATION/upload`
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ Before enabling OAuth in Immich, a new client application needs to be configured
|
|||||||
- `https://immich.example.com/auth/login`
|
- `https://immich.example.com/auth/login`
|
||||||
- `https://immich.example.com/user-settings`
|
- `https://immich.example.com/user-settings`
|
||||||
|
|
||||||
|
3. Configure Backchannel logout URL
|
||||||
|
|
||||||
|
If the authentication server supports it, the **Backchannel logout URL** can be specified, and it is of the form: `http://DOMAIN:PORT/api/oauth/backchannel-logout`.
|
||||||
|
|
||||||
## Enable OAuth
|
## Enable OAuth
|
||||||
|
|
||||||
Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
|
Once you have a new OAuth client application configured, Immich can be configured using the Administration Settings page, available on the web (Administration -> Settings).
|
||||||
@@ -63,6 +67,8 @@ Once you have a new OAuth client application configured, Immich can be configure
|
|||||||
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
| `scope` | string | openid email profile | Full list of scopes to send with the request (space delimited) |
|
||||||
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
| `id_token_signed_response_alg` | string | RS256 | The algorithm used to sign the id token (examples: RS256, HS256) |
|
||||||
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
|
| `userinfo_signed_response_alg` | string | none | The algorithm used to sign the userinfo response (examples: RS256, HS256) |
|
||||||
|
| `prompt` | string | (empty) | Prompt parameter for authorization url (examples: select_account, login, consent) |
|
||||||
|
| `end_session_endpoint` | URL | (empty) | Http(s) alternative end session endpoint (logout URI) |
|
||||||
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
|
| Request timeout | string | 30,000 (30 seconds) | Number of milliseconds to wait for http requests to complete before giving up |
|
||||||
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
| Storage Label Claim | string | preferred_username | Claim mapping for the user's storage label**¹** |
|
||||||
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
| Role Claim | string | immich_role | Claim mapping for the user's role. (should return "user" or "admin")**¹** |
|
||||||
@@ -181,6 +187,7 @@ Configuration of OAuth in Immich System Settings
|
|||||||
| Scope | openid email profile immich_scope |
|
| Scope | openid email profile immich_scope |
|
||||||
| ID Token Signed Response Algorithm | RS256 |
|
| ID Token Signed Response Algorithm | RS256 |
|
||||||
| Userinfo Signed Response Algorithm | RS256 |
|
| Userinfo Signed Response Algorithm | RS256 |
|
||||||
|
| End Session Endpoint | https://auth.example.com/logout?rd=https://immich.example.com/ |
|
||||||
| Storage Label Claim | uid |
|
| Storage Label Claim | uid |
|
||||||
| Storage Quota Claim | immich_quota |
|
| Storage Quota Claim | immich_quota |
|
||||||
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
| Default Storage Quota (GiB) | 0 (empty for unlimited quota) |
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ VectorChord is the successor extension to pgvecto.rs, allowing for higher perfor
|
|||||||
|
|
||||||
### Migrating from pgvecto.rs
|
### Migrating from pgvecto.rs
|
||||||
|
|
||||||
Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so.
|
Support for pgvecto.rs has been dropped as of 3.0, hence all users currently using pgvecto.rs should migrate to VectorChord. There are two primary approaches to do so.
|
||||||
|
|
||||||
The easiest option is to have both extensions installed during the migration:
|
The easiest option is to have both extensions installed during the migration:
|
||||||
|
|
||||||
|
|||||||
@@ -80,9 +80,9 @@ To see local changes to `@immich/ui` in Immich, do the following:
|
|||||||
|
|
||||||
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
|
1. Install `@immich/ui` as a sibling to `immich/`, for example `/home/user/immich` and `/home/user/ui`
|
||||||
2. Build the `@immich/ui` project via `pnpm run build`
|
2. Build the `@immich/ui` project via `pnpm run build`
|
||||||
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yml` file (`../../ui:/usr/src/ui`)
|
||||||
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
4. Uncomment the corresponding alias in the `web/vite.config.ts` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui/packages/ui')`)
|
||||||
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
5. Uncomment the import statement in `web/src/app.css` file `@import '../../../ui/packages/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
||||||
6. Start up the stack via `make dev`
|
6. Start up the stack via `make dev`
|
||||||
7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`)
|
7. After making changes in `@immich/ui`, rebuild it (`pnpm run build`)
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,8 @@ Some basic examples:
|
|||||||
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
- `**/Raw/**` will exclude all files in any directory named `Raw`
|
||||||
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
|
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
|
||||||
|
|
||||||
|
Note that `*` is a wildcard matching zero or more characters (i.e., withinin a filename or single directory name). `**` matches zero or more subdirectories, recursively. It also includes any/all files within a subdirectory, i.e., when used at the end of a pattern. For example, `**/exclude_me/**` will exclude all files in any directory named `exclude_me`, as well as all files in any subdirectories of `exclude_me`, recursively.
|
||||||
|
|
||||||
Special characters such as @ should be escaped, for instance:
|
Special characters such as @ should be escaped, for instance:
|
||||||
|
|
||||||
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
|
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
|
|||||||
|
|
||||||
#### ROCm
|
#### ROCm
|
||||||
|
|
||||||
|
- On Linux, The [AMDGPU driver module](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) needs to be installed on the server and, if secure boot is used, the signing key of DKMS [needs to be enrolled in UEFI BIOS](https://wiki.debian.org/SecureBoot)
|
||||||
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
|
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=<a supported version, e.g. 10.3.0>`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
|
||||||
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
|
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
|
||||||
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
|
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ You can search the following types of content:
|
|||||||
| People | Faces that are recognized in your photos/videos. |
|
| People | Faces that are recognized in your photos/videos. |
|
||||||
| Contextual | Content of the photos and videos. |
|
| Contextual | Content of the photos and videos. |
|
||||||
| File name or extension | Full or partial file's name, or file's extension |
|
| File name or extension | Full or partial file's name, or file's extension |
|
||||||
|
| Full path or folder | Full or partial folder names from the original path. |
|
||||||
| Description | Description added to assets. |
|
| Description | Description added to assets. |
|
||||||
| Optical Character Recognition (OCR) | Text in images |
|
| Optical Character Recognition (OCR) | Text in images |
|
||||||
| Locations | Cities, states, and countries from reverse geocoding. |
|
| Locations | Cities, states, and countries from reverse geocoding. |
|
||||||
@@ -26,10 +27,16 @@ You can search the following types of content:
|
|||||||
| Time frame | Start and end date of a specific time bucket |
|
| Time frame | Start and end date of a specific time bucket |
|
||||||
| Media type | Image or video or both |
|
| Media type | Image or video or both |
|
||||||
| Display options | In Archive, in Favorites or Not in any album |
|
| Display options | In Archive, in Favorites or Not in any album |
|
||||||
| Start rating | User-assigned start rating |
|
| Star rating | User-assigned star rating |
|
||||||
|
|
||||||
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
|
<img src={require('./img/advanced-search-filters.webp').default} width="70%" title='Advanced search filters' />
|
||||||
|
|
||||||
|
### Full path or folder
|
||||||
|
|
||||||
|
Use this mode when you know a folder name or part of the original asset path.
|
||||||
|
|
||||||
|
Example: for /John/Projects/3D_Printing/2026-07-01/IMG_0001.jpg, searches like Projects, 3D, Printing, or 2026 match the asset.
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
|
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
| `JPEG 2000` | `.jp2` | :white_check_mark: | |
|
||||||
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
| `JPEG` | `.jpeg` `.jpg` `.jpe` `.insp` | :white_check_mark: | |
|
||||||
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
| `JPEG XL` | `.jxl` | :white_check_mark: | |
|
||||||
|
| `MPO` | `.mpo` | :white_check_mark: | Multi-Picture |
|
||||||
| `PNG` | `.png` | :white_check_mark: | |
|
| `PNG` | `.png` | :white_check_mark: | |
|
||||||
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
| `PSD` | `.psd` | :white_check_mark: | Adobe Photoshop |
|
||||||
| `RAW` | `.raw` | :white_check_mark: | |
|
| `RAW` | `.raw` | :white_check_mark: | |
|
||||||
@@ -28,17 +29,17 @@ For the full list, refer to the [Immich source code](https://github.com/immich-a
|
|||||||
|
|
||||||
## Video formats
|
## Video formats
|
||||||
|
|
||||||
| Format | Extension(s) | Supported? | Notes |
|
| Format | Extension(s) | Supported? | Notes |
|
||||||
| :---------- | :-------------------- | :----------------: | :---- |
|
| :---------- | :-------------------------- | :----------------: | :---- |
|
||||||
| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | |
|
| `3GPP` | `.3gp` `.3gpp` | :white_check_mark: | |
|
||||||
| `AVI` | `.avi` | :white_check_mark: | |
|
| `AVI` | `.avi` | :white_check_mark: | |
|
||||||
| `FLV` | `.flv` | :white_check_mark: | |
|
| `FLV` | `.flv` | :white_check_mark: | |
|
||||||
| `M4V` | `.m4v` | :white_check_mark: | |
|
| `M4V` | `.m4v` | :white_check_mark: | |
|
||||||
| `MATROSKA` | `.mkv` | :white_check_mark: | |
|
| `MATROSKA` | `.mkv` | :white_check_mark: | |
|
||||||
| `MP2T` | `.mts` `.m2ts` `.m2t` | :white_check_mark: | |
|
| `MP2T` | `.mts` `.m2ts` `.m2t` `.ts` | :white_check_mark: | |
|
||||||
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
|
| `MP4` | `.mp4` `.insv` | :white_check_mark: | |
|
||||||
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
|
| `MPEG` | `.mpg` `.mpe` `.mpeg` | :white_check_mark: | |
|
||||||
| `MXF` | `.mxf` | :white_check_mark: | |
|
| `MXF` | `.mxf` | :white_check_mark: | |
|
||||||
| `QUICKTIME` | `.mov` | :white_check_mark: | |
|
| `QUICKTIME` | `.mov` | :white_check_mark: | |
|
||||||
| `WEBM` | `.webm` | :white_check_mark: | |
|
| `WEBM` | `.webm` | :white_check_mark: | |
|
||||||
| `WMV` | `.wmv` | :white_check_mark: | |
|
| `WMV` | `.wmv` | :white_check_mark: | |
|
||||||
|
|||||||
@@ -20,8 +20,6 @@ def upload(file):
|
|||||||
}
|
}
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
'deviceAssetId': f'{file}-{stats.st_mtime}',
|
|
||||||
'deviceId': 'python',
|
|
||||||
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
|
'fileCreatedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||||
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
|
'fileModifiedAt': datetime.fromtimestamp(stats.st_mtime),
|
||||||
'isFavorite': 'false',
|
'isFavorite': 'false',
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
|
|||||||
### Cons
|
### Cons
|
||||||
|
|
||||||
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
|
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
|
||||||
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
|
- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use.
|
||||||
- Tailscale needs to be installed and running on both server-side and client-side.
|
- Tailscale needs to be installed and running on both server-side and client-side.
|
||||||
|
|
||||||
## Option 3: Reverse Proxy
|
## Option 3: Reverse Proxy
|
||||||
|
|||||||
@@ -193,6 +193,7 @@ The default configuration looks like this:
|
|||||||
"defaultStorageQuota": null,
|
"defaultStorageQuota": null,
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
"issuerUrl": "",
|
"issuerUrl": "",
|
||||||
|
"endSessionEndpoint": "",
|
||||||
"mobileOverrideEnabled": false,
|
"mobileOverrideEnabled": false,
|
||||||
"mobileRedirectUri": "",
|
"mobileRedirectUri": "",
|
||||||
"profileSigningAlgorithm": "none",
|
"profileSigningAlgorithm": "none",
|
||||||
|
|||||||
@@ -29,29 +29,31 @@ These environment variables are used by the `docker-compose.yml` file and do **N
|
|||||||
|
|
||||||
## General
|
## General
|
||||||
|
|
||||||
| Variable | Description | Default | Containers | Workers |
|
| Variable | Description | Default | Containers | Workers |
|
||||||
| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
|
||||||
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
| `TZ` | Timezone | <sup>\*1</sup> | server | microservices |
|
||||||
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
|
||||||
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
|
||||||
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**<sup>\*2</sup>⚠️ | `/data` | server | api, microservices |
|
||||||
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
|
||||||
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api, microservices |
|
| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`<sup>\*3</sup>. | `false` | server | api |
|
||||||
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
|
||||||
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
|
||||||
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
|
||||||
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
|
||||||
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
|
||||||
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
|
||||||
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
|
||||||
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
|
||||||
|
|
||||||
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
|
||||||
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
|
||||||
|
|
||||||
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
|
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
|
||||||
|
|
||||||
|
\*3: The [default configuration](https://helmetjs.github.io/#content-security-policy) sets `upgrade-insecure-requests`, which tells the browser to upgrade all requests to HTTPS. This breaks on HTTP-only deployments. If you cannot use HTTPS, you should use a custom helmet config file with `"upgrade-insecure-requests": null`.
|
||||||
|
|
||||||
## Workers
|
## Workers
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
@@ -81,7 +83,7 @@ Information on the current workers can be found [here](/administration/jobs-work
|
|||||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
| `DB_SSL_MODE` | Database SSL mode | | server |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`]) | | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
|
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])<sup>\*3</sup> | `SSD` | database |
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ Immich requires [**Docker**](https://docs.docker.com/get-started/get-docker/) wi
|
|||||||
The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
|
The Compose plugin will be installed by both Docker Engine and Desktop by following the linked installation guides; it can also be [separately installed](https://docs.docker.com/compose/install/).
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/compose/migrate/) and is no longer supported by Immich.
|
Immich requires the command `docker compose`; the similarly named `docker-compose` is [deprecated](https://docs.docker.com/retired/#docker-compose-v1-replaced-by-compose-v2) and is no longer supported by Immich.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Special requirements for Windows users
|
### Special requirements for Windows users
|
||||||
|
|||||||
@@ -130,7 +130,3 @@ These storage mediums have different performance characteristics. As a result, t
|
|||||||
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
|
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
|
||||||
|
|
||||||
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
|
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
|
||||||
|
|
||||||
#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
|
|
||||||
|
|
||||||
VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
|
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ You can read more about the differences between storage template engine on and o
|
|||||||
|
|
||||||
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.
|
The admin user can set the template by using the template builder in the `Administration -> Settings -> Storage Template`. Immich provides a set of variables that you can use in constructing the template, along with additional custom text. If the template produces [multiple files with the same filename, they won't be overwritten](https://github.com/immich-app/immich/discussions/3324) as a sequence number is appended to the filename.
|
||||||
|
|
||||||
|
Date and time variables in storage templates are rendered in the server's local timezone.
|
||||||
|
|
||||||
```bash title="Default template"
|
```bash title="Default template"
|
||||||
Year/Year-Month-Day/Filename.Extension
|
Year/Year-Month-Day/Filename.Extension
|
||||||
```
|
```
|
||||||
|
|||||||
+11
-11
@@ -17,10 +17,10 @@
|
|||||||
"write-heading-ids": "docusaurus write-heading-ids"
|
"write-heading-ids": "docusaurus write-heading-ids"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@docusaurus/core": "~3.9.0",
|
"@docusaurus/core": "~3.10.0",
|
||||||
"@docusaurus/preset-classic": "~3.9.0",
|
"@docusaurus/preset-classic": "~3.10.0",
|
||||||
"@docusaurus/theme-common": "~3.9.0",
|
"@docusaurus/theme-common": "~3.10.0",
|
||||||
"@docusaurus/theme-mermaid": "~3.9.0",
|
"@docusaurus/theme-mermaid": "~3.10.0",
|
||||||
"@mdi/js": "^7.3.67",
|
"@mdi/js": "^7.3.67",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mdx-js/react": "^3.0.0",
|
"@mdx-js/react": "^3.0.0",
|
||||||
@@ -30,17 +30,17 @@
|
|||||||
"postcss": "^8.4.25",
|
"postcss": "^8.4.25",
|
||||||
"prism-react-renderer": "^2.3.1",
|
"prism-react-renderer": "^2.3.1",
|
||||||
"raw-loader": "^4.0.2",
|
"raw-loader": "^4.0.2",
|
||||||
"react": "^18.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^18.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.2.4",
|
||||||
"url": "^0.11.0"
|
"url": "^0.11.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@docusaurus/module-type-aliases": "~3.9.0",
|
"@docusaurus/module-type-aliases": "~3.10.0",
|
||||||
"@docusaurus/tsconfig": "^3.7.0",
|
"@docusaurus/tsconfig": "^3.10.0",
|
||||||
"@docusaurus/types": "^3.7.0",
|
"@docusaurus/types": "^3.10.0",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.7.4",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^6.0.0"
|
||||||
},
|
},
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
@@ -58,6 +58,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.14.1"
|
"node": "24.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
@@ -1,4 +1,8 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"label": "v2.7.5",
|
||||||
|
"url": "https://docs.v2.7.5.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v2.6.3",
|
"label": "v2.6.3",
|
||||||
"url": "https://docs.v2.6.3.archive.immich.app"
|
"url": "https://docs.v2.6.3.archive.immich.app"
|
||||||
|
|||||||
+1
-5
@@ -1,8 +1,4 @@
|
|||||||
{
|
{
|
||||||
// This file is not used in compilation. It is here just for a nice editor experience.
|
// This file is not used in compilation. It is here just for a nice editor experience.
|
||||||
"extends": "@docusaurus/tsconfig",
|
"extends": "@docusaurus/tsconfig"
|
||||||
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
import { exportJWK, generateKeyPair } from 'jose';
|
import {
|
||||||
|
calculateJwkThumbprint,
|
||||||
|
exportJWK,
|
||||||
|
importPKCS8,
|
||||||
|
importSPKI,
|
||||||
|
SignJWT,
|
||||||
|
} from 'jose';
|
||||||
import Provider from 'oidc-provider';
|
import Provider from 'oidc-provider';
|
||||||
|
import { PRIVATE_KEY_PEM, PUBLIC_KEY_PEM } from './test-keys';
|
||||||
|
|
||||||
export enum OAuthClient {
|
export enum OAuthClient {
|
||||||
DEFAULT = 'client-default',
|
DEFAULT = 'client-default',
|
||||||
@@ -44,6 +51,29 @@ const claims = [
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const privateKey = await importPKCS8(PRIVATE_KEY_PEM, 'RS256', {
|
||||||
|
extractable: true,
|
||||||
|
});
|
||||||
|
const publicKey = await importSPKI(PUBLIC_KEY_PEM, 'RS256', {
|
||||||
|
extractable: true,
|
||||||
|
});
|
||||||
|
const kid = await calculateJwkThumbprint(await exportJWK(publicKey));
|
||||||
|
|
||||||
|
export async function generateLogoutToken(iss: string, sub: string) {
|
||||||
|
return await new SignJWT({
|
||||||
|
iss: iss,
|
||||||
|
aud: OAuthClient.DEFAULT,
|
||||||
|
iat: Math.floor(Date.now() / 1000),
|
||||||
|
jti: crypto.randomUUID(),
|
||||||
|
sub: sub,
|
||||||
|
events: {
|
||||||
|
'http://schemas.openid.net/event/backchannel-logout': {},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.setProtectedHeader({ alg: 'RS256', typ: 'logout+jwt', kid: kid })
|
||||||
|
.sign(privateKey);
|
||||||
|
}
|
||||||
|
|
||||||
const withDefaultClaims = (sub: string) => ({
|
const withDefaultClaims = (sub: string) => ({
|
||||||
sub,
|
sub,
|
||||||
email: `${sub}@immich.app`,
|
email: `${sub}@immich.app`,
|
||||||
@@ -66,8 +96,6 @@ const getClaims = (sub: string, use?: string) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setup = async () => {
|
const setup = async () => {
|
||||||
const { privateKey, publicKey } = await generateKeyPair('RS256');
|
|
||||||
|
|
||||||
const redirectUris = [
|
const redirectUris = [
|
||||||
'http://127.0.0.1:2285/auth/login',
|
'http://127.0.0.1:2285/auth/login',
|
||||||
'https://photos.immich.app/oauth/mobile-redirect',
|
'https://photos.immich.app/oauth/mobile-redirect',
|
||||||
|
|||||||
@@ -7,9 +7,10 @@
|
|||||||
"start": "tsx startup.ts"
|
"start": "tsx startup.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"jose": "^5.6.3",
|
"jose": "^6.0.0",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^9.0.0",
|
||||||
"oidc-provider": "^9.0.0",
|
"oidc-provider": "^9.0.0",
|
||||||
"tsx": "^4.20.6"
|
"tsx": "^4.20.6"
|
||||||
}
|
},
|
||||||
|
"packageManager": "pnpm@10.33.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
export const PRIVATE_KEY_PEM = `-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCVj5C7hzN3E2HO
|
||||||
|
TcJ+DN/e2NSTQFj4rPylz4J8xjm8Es7l0k2kK5EEGvUNVGZbw7s055c+6kwP9eqg
|
||||||
|
B5XFE7+26Fcq1sou6Tbm310kU4dnMW5l2CgwrhaGyb1pNysao0AMLT60dFYqtUwn
|
||||||
|
ha9ceCsa+ZU1JrknVf3rONtppBvhWoI7CO9XX1keVQ0unHPzCWUjpXTzC8OGEbmB
|
||||||
|
2w7ZIUf8OfJkd5RZ4OtIpML71W9n13aDxT50x2/EW/pFLFtQ/oaleOKHpvlRXDRX
|
||||||
|
W86G4moUJym3gHMXMUj2aOcFG2UJnpLruKz3i5qZwYiTRlBP6O9EIQNCVtYxchuN
|
||||||
|
V1CCcBU1AgMBAAECggEAJLfXMu8Nx89ynPVyyUMMaFfoEpHC9iR0L5obQVpiPMYK
|
||||||
|
VRqVVLecdftPS9s7eQ58BNBRzdC0ZVu841aRYs3HLNbsZZhPkYZQpAxU//Dg5okY
|
||||||
|
fzj7Hv5yidt4HN9+Pd8z/3lRMnj4WapifLaBt8xJ2ujJBMBRxzJBsXDnT0+Kx7+y
|
||||||
|
bYDeuVfyUTEikaK3QZTbuRF3D3eiuN16GG+hv8UqTF2eYbPxdiLjYpTSHa4mH88C
|
||||||
|
qfJz2Xt4SEzmyeo3G+MO17wDFOwtEe8ojlJfULHnHJSFdUwTfYIFM1bg5/fJ9MOS
|
||||||
|
/fO3TSG+wkQqjQa6eoGssAzP87fL2XNLzlDtGY/7uQKBgQDHuJHOtf1EjOvNYiP7
|
||||||
|
EN+8QGs41ghzt9CQRQxWbHpusR3IW3P83KMXwYmrlG70oOUXBRGSB/ESXUofXc5W
|
||||||
|
pu5+Y55S44aUnu/a9yOBttYW0dtHZSL0zFT+PlVASwUzFZ2zcH1KXlUkSpfL5OAD
|
||||||
|
PyDDTnBZ2AWh45fRO9wLo6PPuQKBgQC/tI03RqU3mOjqukKbquYeIpXHfRU5Z0DM
|
||||||
|
u9ru1THYEl6fmkMXycxo/mvW3awyFuyKy/VodqIgKnFgumEqCHZh6OAMm/LC7TfA
|
||||||
|
l9tjFSs/MyOqQVD4kbX+z6Oq4c4GccDoXfsQ3gzECoBapegi/F+6/25y+/C8ghXb
|
||||||
|
J/Jg1GQXXQKBgQDFgWbfzuVZZyrBfu4qGLPJDMN7/114YizknwPma3xf/tN/EcGQ
|
||||||
|
K/k1QvWMMkvPq1UiAKcxjJ0AFjV482FcG9T6NDWbrtmmG88C8Sex3Ue2ZW2+GuwI
|
||||||
|
vhDHJIlV/Vp0/Elp7DJa2xLDwuh+gCZvz3vs6KL+ljxrrhCyn8mp0PfsMQKBgFFZ
|
||||||
|
KnuETOO0zVGdzFoGQTQUdP58A5+iQwsdxB+I9Ge+E80iRso3ZbhADj7VPhbbR3D2
|
||||||
|
b6LuhImluQrUzBpsEOAnU7vGCVPSGdBuIDiBaSKebsn2gYeZPWNtdQQ0YZq2dqek
|
||||||
|
Cb/0mfIuipzsvf7qnSza62F7q4IyqVegMegI+Jg5AoGATM3NMy7JZeKzSkm+3ohU
|
||||||
|
3xZOwgqKV9SH+0OeYWpuBxT7D7FlrKKI4NJ3XN3hg2f/DJAF6dH11CPe7pk94yol
|
||||||
|
HMbh+PQUQ6GYvAzxIOvagWboQ3lzeyubNMpyFjfOrIE/WOQCUBZ9tIwCHIarIuyi
|
||||||
|
QRuNOj3+U8T/n1Ww352HBdw=
|
||||||
|
-----END PRIVATE KEY-----`;
|
||||||
|
|
||||||
|
export const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlY+Qu4czdxNhzk3Cfgzf
|
||||||
|
3tjUk0BY+Kz8pc+CfMY5vBLO5dJNpCuRBBr1DVRmW8O7NOeXPupMD/XqoAeVxRO/
|
||||||
|
tuhXKtbKLuk25t9dJFOHZzFuZdgoMK4Whsm9aTcrGqNADC0+tHRWKrVMJ4WvXHgr
|
||||||
|
GvmVNSa5J1X96zjbaaQb4VqCOwjvV19ZHlUNLpxz8wllI6V08wvDhhG5gdsO2SFH
|
||||||
|
/DnyZHeUWeDrSKTC+9VvZ9d2g8U+dMdvxFv6RSxbUP6GpXjih6b5UVw0V1vOhuJq
|
||||||
|
FCcpt4BzFzFI9mjnBRtlCZ6S67is94uamcGIk0ZQT+jvRCEDQlbWMXIbjVdQgnAV
|
||||||
|
NQIDAQAB
|
||||||
|
-----END PUBLIC KEY-----`;
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
24.14.1
|
24.15.0
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ services:
|
|||||||
|
|
||||||
redis:
|
redis:
|
||||||
container_name: immich-e2e-redis
|
container_name: immich-e2e-redis
|
||||||
image: docker.io/valkey/valkey:9@sha256:3eeb09785cd61ec8e3be35f8804c8892080f3ca21934d628abc24ee4ed1698f6
|
image: docker.io/valkey/valkey:9@sha256:3b55fbaa0cd93cf0d9d961f405e4dfcc70efe325e2d84da207a0a8e6d8fde4f9
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: redis-cli ping || exit 1
|
test: redis-cli ping || exit 1
|
||||||
|
|
||||||
|
|||||||
+6
-6
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "2.6.3",
|
"version": "2.7.5",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -32,15 +32,15 @@
|
|||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@socket.io/component-emitter": "^3.1.2",
|
"@socket.io/component-emitter": "^3.1.2",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^24.12.0",
|
"@types/node": "^24.12.2",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.15.1",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^7.0.0",
|
||||||
"dotenv": "^17.2.3",
|
"dotenv": "^17.2.3",
|
||||||
"eslint": "^10.0.0",
|
"eslint": "^10.0.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"eslint-config-prettier": "^10.1.8",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^63.0.0",
|
"eslint-plugin-unicorn": "^64.0.0",
|
||||||
"exiftool-vendored": "^35.0.0",
|
"exiftool-vendored": "^35.0.0",
|
||||||
"globals": "^17.0.0",
|
"globals": "^17.0.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
@@ -51,13 +51,13 @@
|
|||||||
"sharp": "^0.34.5",
|
"sharp": "^0.34.5",
|
||||||
"socket.io-client": "^4.7.4",
|
"socket.io-client": "^4.7.4",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^6.0.0",
|
||||||
"typescript-eslint": "^8.28.0",
|
"typescript-eslint": "^8.28.0",
|
||||||
"utimes": "^5.2.1",
|
"utimes": "^5.2.1",
|
||||||
"vite-tsconfig-paths": "^6.1.1",
|
"vite-tsconfig-paths": "^6.1.1",
|
||||||
"vitest": "^4.0.0"
|
"vitest": "^4.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "24.14.1"
|
"node": "24.15.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-42
@@ -2,82 +2,44 @@ import { expect } from 'vitest';
|
|||||||
|
|
||||||
export const errorDto = {
|
export const errorDto = {
|
||||||
unauthorized: {
|
unauthorized: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Authentication required',
|
message: 'Authentication required',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
unauthorizedWithMessage: (message: string) => ({
|
unauthorizedWithMessage: (message: string) => ({
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message,
|
message,
|
||||||
correlationId: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
forbidden: {
|
forbidden: {
|
||||||
error: 'Forbidden',
|
|
||||||
statusCode: 403,
|
|
||||||
message: expect.any(String),
|
message: expect.any(String),
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
missingPermission: (permission: string) => ({
|
missingPermission: (permission: string) => ({
|
||||||
error: 'Forbidden',
|
|
||||||
statusCode: 403,
|
|
||||||
message: `Missing required permission: ${permission}`,
|
message: `Missing required permission: ${permission}`,
|
||||||
correlationId: expect.any(String),
|
|
||||||
}),
|
}),
|
||||||
wrongPassword: {
|
wrongPassword: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'Wrong password',
|
message: 'Wrong password',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
invalidToken: {
|
invalidToken: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Invalid user token',
|
message: 'Invalid user token',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
invalidShareKey: {
|
invalidShareKey: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Invalid share key',
|
message: 'Invalid share key',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
passwordRequired: {
|
passwordRequired: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Password required',
|
message: 'Password required',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
badRequest: (message: any = null) => ({
|
badRequest: (message: any = null) => ({
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: message ?? expect.anything(),
|
message: message ?? expect.anything(),
|
||||||
correlationId: expect.any(String),
|
}),
|
||||||
|
validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray<string | number>; message: string }>) => ({
|
||||||
|
message: 'Validation failed',
|
||||||
|
errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
|
||||||
}),
|
}),
|
||||||
noPermission: {
|
noPermission: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: expect.stringContaining('Not found or no'),
|
message: expect.stringContaining('Not found or no'),
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
incorrectLogin: {
|
incorrectLogin: {
|
||||||
error: 'Unauthorized',
|
|
||||||
statusCode: 401,
|
|
||||||
message: 'Incorrect email or password',
|
message: 'Incorrect email or password',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
alreadyHasAdmin: {
|
alreadyHasAdmin: {
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: 'The server already has an admin',
|
message: 'The server already has an admin',
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
|
||||||
invalidEmail: {
|
|
||||||
error: 'Bad Request',
|
|
||||||
statusCode: 400,
|
|
||||||
message: ['email must be an email'],
|
|
||||||
correlationId: expect.any(String),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -130,12 +130,11 @@ describe('/albums', () => {
|
|||||||
describe('GET /albums', () => {
|
describe('GET /albums', () => {
|
||||||
it("should not show other users' favorites", async () => {
|
it("should not show other users' favorites", async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${user1Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining({ isFavorite: false })],
|
|
||||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||||
lastModifiedAssetTimestamp: expect.any(String),
|
lastModifiedAssetTimestamp: expect.any(String),
|
||||||
startDate: expect.any(String),
|
startDate: expect.any(String),
|
||||||
@@ -147,7 +146,7 @@ describe('/albums', () => {
|
|||||||
|
|
||||||
it('should not return shared albums with a deleted owner', async () => {
|
it('should not return shared albums with a deleted owner', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/albums?shared=true')
|
.get('/albums?isShared=true')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -155,23 +154,31 @@ describe('/albums', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedLink,
|
albumName: user1SharedLink,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedEditorUser,
|
albumName: user1SharedEditorUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedViewerUser,
|
albumName: user1SharedViewerUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user2.userId,
|
|
||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@@ -181,82 +188,164 @@ describe('/albums', () => {
|
|||||||
it('should return the album collection including owned and shared', async () => {
|
it('should return the album collection including owned and shared', async () => {
|
||||||
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
|
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(4);
|
expect(body).toHaveLength(5);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedEditorUser,
|
albumName: user1SharedEditorUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedViewerUser,
|
albumName: user1SharedViewerUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedLink,
|
albumName: user1SharedLink,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
|
expect.objectContaining({
|
||||||
|
albumName: user2SharedUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||||
|
]),
|
||||||
|
shared: true,
|
||||||
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection filtered by shared', async () => {
|
it('should return the album collection filtered by isShared', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/albums?shared=true')
|
.get('/albums?isShared=true')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(4);
|
expect(body).toHaveLength(4);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedEditorUser,
|
albumName: user1SharedEditorUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedViewerUser,
|
albumName: user1SharedViewerUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1SharedLink,
|
albumName: user1SharedLink,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user2.userId,
|
|
||||||
albumName: user2SharedUser,
|
albumName: user2SharedUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||||
|
]),
|
||||||
shared: true,
|
shared: true,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection filtered by NOT shared', async () => {
|
it('should return the album collection filtered by NOT isShared', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/albums?shared=false')
|
.get('/albums?isShared=false')
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(1);
|
expect(body).toHaveLength(1);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: user1NotShared,
|
albumName: user1NotShared,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) },
|
||||||
|
]),
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return only owned albums when filtered by isOwned=true', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?isOwned=true')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(4);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ albumName: user1SharedEditorUser }),
|
||||||
|
expect.objectContaining({ albumName: user1SharedViewerUser }),
|
||||||
|
expect.objectContaining({ albumName: user1SharedLink }),
|
||||||
|
expect.objectContaining({ albumName: user1NotShared }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?isOwned=false')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(1);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({
|
||||||
|
albumName: user2SharedUser,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
|
||||||
|
]),
|
||||||
|
}),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?isOwned=true&isShared=true')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(3);
|
||||||
|
expect(body).toEqual(
|
||||||
|
expect.arrayContaining([
|
||||||
|
expect.objectContaining({ albumName: user1SharedEditorUser }),
|
||||||
|
expect.objectContaining({ albumName: user1SharedViewerUser }),
|
||||||
|
expect.objectContaining({ albumName: user1SharedLink }),
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?isOwned=false&isShared=false')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return the album collection filtered by assetId', async () => {
|
it('should return the album collection filtered by assetId', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums?assetId=${user1Asset2.id}`)
|
.get(`/albums?assetId=${user1Asset2.id}`)
|
||||||
@@ -265,17 +354,17 @@ describe('/albums', () => {
|
|||||||
expect(body).toHaveLength(2);
|
expect(body).toHaveLength(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection filtered by assetId and ignores shared=true', async () => {
|
it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums?shared=true&assetId=${user1Asset1.id}`)
|
.get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(5);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the album collection filtered by assetId and ignores shared=false', async () => {
|
it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums?shared=false&assetId=${user1Asset1.id}`)
|
.get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toHaveLength(5);
|
expect(body).toHaveLength(5);
|
||||||
@@ -287,13 +376,17 @@ describe('/albums', () => {
|
|||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user4.userId,
|
|
||||||
albumName: user4DeletedAsset,
|
albumName: user4DeletedAsset,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
||||||
|
]),
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
ownerId: user4.userId,
|
|
||||||
albumName: user4Empty,
|
albumName: user4Empty,
|
||||||
|
albumUsers: expect.arrayContaining([
|
||||||
|
{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user4.userId }) },
|
||||||
|
]),
|
||||||
shared: false,
|
shared: false,
|
||||||
}),
|
}),
|
||||||
]),
|
]),
|
||||||
@@ -304,13 +397,12 @@ describe('/albums', () => {
|
|||||||
describe('GET /albums/:id', () => {
|
describe('GET /albums/:id', () => {
|
||||||
it('should return album info for own album', async () => {
|
it('should return album info for own album', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user1Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${user1Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
|
||||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||||
lastModifiedAssetTimestamp: expect.any(String),
|
lastModifiedAssetTimestamp: expect.any(String),
|
||||||
startDate: expect.any(String),
|
startDate: expect.any(String),
|
||||||
@@ -322,7 +414,7 @@ describe('/albums', () => {
|
|||||||
|
|
||||||
it('should return album info for shared album (editor)', async () => {
|
it('should return album info for shared album (editor)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user2Albums[0].id}?withoutAssets=false`)
|
.get(`/albums/${user2Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
@@ -331,14 +423,14 @@ describe('/albums', () => {
|
|||||||
|
|
||||||
it('should return album info for shared album (viewer)', async () => {
|
it('should return album info for shared album (viewer)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user1Albums[3].id}?withoutAssets=false`)
|
.get(`/albums/${user1Albums[3].id}`)
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
.set('Authorization', `Bearer ${user2.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ id: user1Albums[3].id });
|
expect(body).toMatchObject({ id: user1Albums[3].id });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return album info with assets when withoutAssets is undefined', async () => {
|
it('should return album info', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user1Albums[0].id}`)
|
.get(`/albums/${user1Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
@@ -346,25 +438,6 @@ describe('/albums', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
...user1Albums[0],
|
...user1Albums[0],
|
||||||
assets: [expect.objectContaining({ id: user1Albums[0].assets[0].id })],
|
|
||||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
|
||||||
lastModifiedAssetTimestamp: expect.any(String),
|
|
||||||
startDate: expect.any(String),
|
|
||||||
endDate: expect.any(String),
|
|
||||||
albumUsers: expect.any(Array),
|
|
||||||
shared: true,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return album info without assets when withoutAssets is true', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/albums/${user1Albums[0].id}?withoutAssets=true`)
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({
|
|
||||||
...user1Albums[0],
|
|
||||||
assets: [],
|
|
||||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||||
assetCount: 1,
|
assetCount: 1,
|
||||||
lastModifiedAssetTimestamp: expect.any(String),
|
lastModifiedAssetTimestamp: expect.any(String),
|
||||||
@@ -379,21 +452,21 @@ describe('/albums', () => {
|
|||||||
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
await utils.deleteAssets(user1.accessToken, [user1Asset2.id]);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/albums/${user2Albums[0].id}?withoutAssets=true`)
|
.get(`/albums/${user2Albums[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual(
|
||||||
...user2Albums[0],
|
expect.objectContaining({
|
||||||
assets: [],
|
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
||||||
contributorCounts: [{ userId: user1.userId, assetCount: 1 }],
|
assetCount: 1,
|
||||||
assetCount: 1,
|
lastModifiedAssetTimestamp: expect.any(String),
|
||||||
lastModifiedAssetTimestamp: expect.any(String),
|
endDate: expect.any(String),
|
||||||
endDate: expect.any(String),
|
startDate: expect.any(String),
|
||||||
startDate: expect.any(String),
|
albumUsers: expect.any(Array),
|
||||||
albumUsers: expect.any(Array),
|
shared: true,
|
||||||
shared: true,
|
}),
|
||||||
});
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -419,16 +492,13 @@ describe('/albums', () => {
|
|||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String),
|
updatedAt: expect.any(String),
|
||||||
ownerId: user1.userId,
|
|
||||||
albumName: 'New album',
|
albumName: 'New album',
|
||||||
description: '',
|
description: '',
|
||||||
albumThumbnailAssetId: null,
|
albumThumbnailAssetId: null,
|
||||||
shared: false,
|
shared: false,
|
||||||
albumUsers: [],
|
albumUsers: [{ role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user1.userId }) }],
|
||||||
hasSharedLink: false,
|
hasSharedLink: false,
|
||||||
assets: [],
|
|
||||||
assetCount: 0,
|
assetCount: 0,
|
||||||
owner: expect.objectContaining({ email: user1.userEmail }),
|
|
||||||
isActivityEnabled: true,
|
isActivityEnabled: true,
|
||||||
order: AssetOrder.Desc,
|
order: AssetOrder.Desc,
|
||||||
});
|
});
|
||||||
@@ -644,11 +714,11 @@ describe('/albums', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
albumUsers: [
|
albumUsers: expect.arrayContaining([
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
user: expect.objectContaining({ id: user2.userId }),
|
user: expect.objectContaining({ id: user2.userId }),
|
||||||
}),
|
}),
|
||||||
],
|
]),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -660,7 +730,7 @@ describe('/albums', () => {
|
|||||||
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
|
.send({ albumUsers: [{ userId: user1.userId, role: AlbumUserRole.Editor }] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('Cannot be shared with owner'));
|
expect(body).toEqual(errorDto.badRequest('User already added'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be able to add existing user to shared album', async () => {
|
it('should not be able to add existing user to shared album', async () => {
|
||||||
@@ -686,7 +756,7 @@ describe('/albums', () => {
|
|||||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
|
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.put(`/albums/${album.id}/user/${user2.userId}`)
|
.put(`/albums/${album.id}/user/${user2.userId}`)
|
||||||
@@ -701,7 +771,10 @@ describe('/albums', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
albumUsers: [expect.objectContaining({ role: AlbumUserRole.Editor })],
|
albumUsers: [
|
||||||
|
expect.objectContaining({ role: AlbumUserRole.Owner }),
|
||||||
|
expect.objectContaining({ role: AlbumUserRole.Editor }),
|
||||||
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -712,7 +785,7 @@ describe('/albums', () => {
|
|||||||
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
albumUsers: [{ userId: user2.userId, role: AlbumUserRole.Viewer }],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(album.albumUsers[0].role).toEqual(AlbumUserRole.Viewer);
|
expect(album.albumUsers[1].role).toEqual(AlbumUserRole.Viewer);
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/albums/${album.id}/user/${user2.userId}`)
|
.put(`/albums/${album.id}/user/${user2.userId}`)
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
AssetMediaStatus,
|
AssetMediaStatus,
|
||||||
AssetResponseDto,
|
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
@@ -19,7 +18,7 @@ import { Socket } from 'socket.io-client';
|
|||||||
import { createUserDto, uuidDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { makeRandomImage } from 'src/generators';
|
import { makeRandomImage } from 'src/generators';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
import { app, asBearerAuth, tempDir, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
@@ -95,8 +94,8 @@ describe('/asset', () => {
|
|||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
utils.createAsset(user1.accessToken, {
|
utils.createAsset(user1.accessToken, {
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
fileCreatedAt: yesterday.toISO(),
|
fileCreatedAt: yesterday.toUTC().toISO(),
|
||||||
fileModifiedAt: yesterday.toISO(),
|
fileModifiedAt: yesterday.toUTC().toISO(),
|
||||||
assetData: { filename: 'example.mp4' },
|
assetData: { filename: 'example.mp4' },
|
||||||
}),
|
}),
|
||||||
utils.createAsset(user1.accessToken),
|
utils.createAsset(user1.accessToken),
|
||||||
@@ -380,62 +379,12 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/random', () => {
|
|
||||||
beforeAll(async () => {
|
|
||||||
await Promise.all([
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
utils.createAsset(user1.accessToken),
|
|
||||||
]);
|
|
||||||
|
|
||||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/assets/random')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const assets: AssetResponseDto[] = body;
|
|
||||||
expect(assets.length).toBe(1);
|
|
||||||
expect(assets[0].ownerId).toBe(user1.userId);
|
|
||||||
});
|
|
||||||
|
|
||||||
it.each(TEN_TIMES)('should return 2 random assets', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/assets/random?count=2')
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const assets: AssetResponseDto[] = body;
|
|
||||||
expect(assets.length).toBe(2);
|
|
||||||
|
|
||||||
for (const asset of assets) {
|
|
||||||
expect(asset.ownerId).toBe(user1.userId);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it.skip('should return 1 asset if there are 10 assets in the database but user 2 only has 1', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/assets/random')
|
|
||||||
.set('Authorization', `Bearer ${user2.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('PUT /assets/:id', () => {
|
describe('PUT /assets/:id', () => {
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/assets/${user2Assets[0].id}`)
|
.put(`/assets/${user2Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
});
|
});
|
||||||
@@ -1142,8 +1091,6 @@ describe('/asset', () => {
|
|||||||
const { body, status } = await request(app)
|
const { body, status } = await request(app)
|
||||||
.post('/assets')
|
.post('/assets')
|
||||||
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
|
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
|
||||||
.field('deviceAssetId', 'example-image')
|
|
||||||
.field('deviceId', 'e2e')
|
|
||||||
.field('fileCreatedAt', new Date().toISOString())
|
.field('fileCreatedAt', new Date().toISOString())
|
||||||
.field('fileModifiedAt', new Date().toISOString())
|
.field('fileModifiedAt', new Date().toISOString())
|
||||||
.attach('assetData', makeRandomImage(), 'example.jpg');
|
.attach('assetData', makeRandomImage(), 'example.jpg');
|
||||||
@@ -1160,8 +1107,6 @@ describe('/asset', () => {
|
|||||||
const { body, status } = await request(app)
|
const { body, status } = await request(app)
|
||||||
.post('/assets')
|
.post('/assets')
|
||||||
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
|
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
|
||||||
.field('deviceAssetId', 'example-image')
|
|
||||||
.field('deviceId', 'e2e')
|
|
||||||
.field('fileCreatedAt', new Date().toISOString())
|
.field('fileCreatedAt', new Date().toISOString())
|
||||||
.field('fileModifiedAt', new Date().toISOString())
|
.field('fileModifiedAt', new Date().toISOString())
|
||||||
.attach('assetData', randomBytes(2014), 'example.jpg');
|
.attach('assetData', randomBytes(2014), 'example.jpg');
|
||||||
@@ -1215,29 +1160,4 @@ describe('/asset', () => {
|
|||||||
expect(video.checksum).toStrictEqual(checksum);
|
expect(video.checksum).toStrictEqual(checksum);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /assets/exist', () => {
|
|
||||||
it('ignores invalid deviceAssetIds', async () => {
|
|
||||||
const response = await utils.checkExistingAssets(user1.accessToken, {
|
|
||||||
deviceId: 'test-assets-exist',
|
|
||||||
deviceAssetIds: ['invalid', 'INVALID'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.existingIds).toHaveLength(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('returns the IDs of existing assets', async () => {
|
|
||||||
await utils.createAsset(user1.accessToken, {
|
|
||||||
deviceId: 'test-assets-exist',
|
|
||||||
deviceAssetId: 'test-asset-0',
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await utils.checkExistingAssets(user1.accessToken, {
|
|
||||||
deviceId: 'test-assets-exist',
|
|
||||||
deviceAssetIds: ['test-asset-0'],
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(response.existingIds).toEqual(['test-asset-0']);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -110,7 +110,9 @@ describe('/libraries', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not create an external library with duplicate exclusion patterns', async () => {
|
it('should not create an external library with duplicate exclusion patterns', async () => {
|
||||||
@@ -125,7 +127,9 @@ describe('/libraries', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +161,9 @@ describe('/libraries', () => {
|
|||||||
.send({ name: '' });
|
.send({ name: '' });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['name should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the import paths', async () => {
|
it('should change the import paths', async () => {
|
||||||
@@ -181,7 +187,9 @@ describe('/libraries', () => {
|
|||||||
.send({ importPaths: [''] });
|
.send({ importPaths: [''] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['each value in importPaths should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject duplicate import paths', async () => {
|
it('should reject duplicate import paths', async () => {
|
||||||
@@ -191,7 +199,9 @@ describe('/libraries', () => {
|
|||||||
.send({ importPaths: ['/path', '/path'] });
|
.send({ importPaths: ['/path', '/path'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(["All importPaths's elements must be unique"]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the exclusion pattern', async () => {
|
it('should change the exclusion pattern', async () => {
|
||||||
@@ -215,7 +225,9 @@ describe('/libraries', () => {
|
|||||||
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(["All exclusionPatterns's elements must be unique"]));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject an empty exclusion pattern', async () => {
|
it('should reject an empty exclusion pattern', async () => {
|
||||||
@@ -225,7 +237,9 @@ describe('/libraries', () => {
|
|||||||
.send({ exclusionPatterns: [''] });
|
.send({ exclusionPatterns: [''] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['each value in exclusionPatterns should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lon=123')
|
.get('/map/reverse-geocode?lon=123')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lat is not a number', async () => {
|
it('should throw an error if a lat is not a number', async () => {
|
||||||
@@ -117,7 +119,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=abc&lon=123.456')
|
.get('/map/reverse-geocode?lat=abc&lon=123.456')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lat is out of range', async () => {
|
it('should throw an error if a lat is out of range', async () => {
|
||||||
@@ -125,7 +129,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=91&lon=123.456')
|
.get('/map/reverse-geocode?lat=91&lon=123.456')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['lat must be a number between -90 and 90']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw an error if a lon is not provided', async () => {
|
it('should throw an error if a lon is not provided', async () => {
|
||||||
@@ -133,7 +139,9 @@ describe('/map', () => {
|
|||||||
.get('/map/reverse-geocode?lat=75')
|
.get('/map/reverse-geocode?lat=75')
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['lon must be a number between -180 and 180']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
const reverseGeocodeTestCases = [
|
const reverseGeocodeTestCases = [
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { OAuthClient, OAuthUser } from '@immich/e2e-auth-server';
|
import { OAuthClient, OAuthUser, generateLogoutToken } from '@immich/e2e-auth-server';
|
||||||
import {
|
import {
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SystemConfigOAuthDto,
|
SystemConfigOAuthDto,
|
||||||
getConfigDefaults,
|
getConfigDefaults,
|
||||||
getMyUser,
|
getMyUser,
|
||||||
|
getSessions,
|
||||||
startOAuth,
|
startOAuth,
|
||||||
updateConfig,
|
updateConfig,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@@ -76,6 +77,7 @@ const setupOAuth = async (token: string, dto: Partial<SystemConfigOAuthDto>) =>
|
|||||||
...defaults.oauth,
|
...defaults.oauth,
|
||||||
buttonText: 'Login with Immich',
|
buttonText: 'Login with Immich',
|
||||||
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
|
issuerUrl: `${authServer.internal}/.well-known/openid-configuration`,
|
||||||
|
allowInsecureRequests: true,
|
||||||
...dto,
|
...dto,
|
||||||
};
|
};
|
||||||
await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options);
|
await updateConfig({ systemConfigDto: { ...defaults, oauth: merged } }, options);
|
||||||
@@ -87,21 +89,27 @@ describe(`/oauth`, () => {
|
|||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
|
|
||||||
await setupOAuth(admin.accessToken, {
|
|
||||||
enabled: true,
|
|
||||||
clientId: OAuthClient.DEFAULT,
|
|
||||||
clientSecret: OAuthClient.DEFAULT,
|
|
||||||
buttonText: 'Login with Immich',
|
|
||||||
storageLabelClaim: 'immich_username',
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /oauth/authorize', () => {
|
describe('POST /oauth/authorize', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupOAuth(admin.accessToken, {
|
||||||
|
enabled: true,
|
||||||
|
clientId: OAuthClient.DEFAULT,
|
||||||
|
clientSecret: OAuthClient.DEFAULT,
|
||||||
|
buttonText: 'Login with Immich',
|
||||||
|
storageLabelClaim: 'immich_username',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it(`should throw an error if a redirect uri is not provided`, async () => {
|
it(`should throw an error if a redirect uri is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
const { status, body } = await request(app).post('/oauth/authorize').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['redirectUri must be a string', 'redirectUri should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return a redirect uri', async () => {
|
it('should return a redirect uri', async () => {
|
||||||
@@ -117,19 +125,60 @@ describe(`/oauth`, () => {
|
|||||||
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
|
expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
|
||||||
expect(params.get('state')).toBeDefined();
|
expect(params.get('state')).toBeDefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not include the prompt parameter when not configured', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/oauth/authorize')
|
||||||
|
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
const params = new URL(body.url).searchParams;
|
||||||
|
expect(params.get('prompt')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should include the prompt parameter when configured', async () => {
|
||||||
|
await setupOAuth(admin.accessToken, {
|
||||||
|
enabled: true,
|
||||||
|
clientId: OAuthClient.DEFAULT,
|
||||||
|
clientSecret: OAuthClient.DEFAULT,
|
||||||
|
prompt: 'select_account',
|
||||||
|
});
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/oauth/authorize')
|
||||||
|
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||||
|
expect(status).toBe(201);
|
||||||
|
|
||||||
|
const params = new URL(body.url).searchParams;
|
||||||
|
expect(params.get('prompt')).toBe('select_account');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /oauth/callback', () => {
|
describe('POST /oauth/callback', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupOAuth(admin.accessToken, {
|
||||||
|
enabled: true,
|
||||||
|
clientId: OAuthClient.DEFAULT,
|
||||||
|
clientSecret: OAuthClient.DEFAULT,
|
||||||
|
buttonText: 'Login with Immich',
|
||||||
|
storageLabelClaim: 'immich_username',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it(`should throw an error if a url is not provided`, async () => {
|
it(`should throw an error if a url is not provided`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send({});
|
const { status, body } = await request(app).post('/oauth/callback').send({});
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['url must be a string', 'url should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if the url is empty`, async () => {
|
it(`should throw an error if the url is empty`, async () => {
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
|
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['url should not be empty']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it(`should throw an error if the state is not provided`, async () => {
|
it(`should throw an error if the state is not provided`, async () => {
|
||||||
@@ -158,10 +207,9 @@ describe(`/oauth`, () => {
|
|||||||
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
|
it(`should throw an error if the codeVerifier doesn't match the challenge`, async () => {
|
||||||
const callbackParams = await loginWithOAuth('oauth-auto-register');
|
const callbackParams = await loginWithOAuth('oauth-auto-register');
|
||||||
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
|
const { codeVerifier } = await loginWithOAuth('oauth-auto-register');
|
||||||
const { status, body } = await request(app)
|
const { status } = await request(app)
|
||||||
.post('/oauth/callback')
|
.post('/oauth/callback')
|
||||||
.send({ ...callbackParams, codeVerifier });
|
.send({ ...callbackParams, codeVerifier });
|
||||||
console.log(body);
|
|
||||||
expect(status).toBeGreaterThanOrEqual(400);
|
expect(status).toBeGreaterThanOrEqual(400);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -258,7 +306,7 @@ describe(`/oauth`, () => {
|
|||||||
accessToken: expect.any(String),
|
accessToken: expect.any(String),
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
name: 'OAuth User',
|
name: 'OAuth User',
|
||||||
userEmail: 'oauth-RS256-token@immich.app',
|
userEmail: 'oauth-rs256-token@immich.app',
|
||||||
userId: expect.any(String),
|
userId: expect.any(String),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -292,9 +340,7 @@ describe(`/oauth`, () => {
|
|||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
expect(status).toBe(500);
|
expect(status).toBe(500);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
error: 'Internal Server Error',
|
|
||||||
message: 'Failed to finish oauth',
|
message: 'Failed to finish oauth',
|
||||||
statusCode: 500,
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -313,7 +359,7 @@ describe(`/oauth`, () => {
|
|||||||
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
|
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
|
||||||
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
|
expect(body).toEqual(errorDto.badRequest('OAuth authentication failed'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should link to an existing user by email', async () => {
|
it('should link to an existing user by email', async () => {
|
||||||
@@ -333,6 +379,54 @@ describe(`/oauth`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`POST /oauth/backchannel-logout`, () => {
|
||||||
|
it(`should throw an error if the logout_token is not provided`, async () => {
|
||||||
|
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should throw an error if an invalid logout token is provided`, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/oauth/backchannel-logout')
|
||||||
|
.send({ logout_token: 'invalid token' });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest('Error backchannel logout: token validation failed'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should logout user if a valid logout token is provided`, async () => {
|
||||||
|
await setupOAuth(admin.accessToken, {
|
||||||
|
enabled: true,
|
||||||
|
clientId: OAuthClient.DEFAULT,
|
||||||
|
clientSecret: OAuthClient.DEFAULT,
|
||||||
|
autoRegister: true,
|
||||||
|
signingAlgorithm: 'RS256',
|
||||||
|
buttonText: 'Login with Immich',
|
||||||
|
});
|
||||||
|
|
||||||
|
const callbackParams = await loginWithOAuth('backchannel-logout-user');
|
||||||
|
const { status: callbackStatus, body: callbackBody } = await request(app)
|
||||||
|
.post('/oauth/callback')
|
||||||
|
.send(callbackParams);
|
||||||
|
expect(callbackStatus).toBe(201);
|
||||||
|
|
||||||
|
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).resolves.toHaveLength(1);
|
||||||
|
|
||||||
|
const logoutToken = await generateLogoutToken('http://0.0.0.0:2286', 'backchannel-logout-user');
|
||||||
|
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({ logout_token: logoutToken });
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toMatchObject({});
|
||||||
|
|
||||||
|
await expect(getSessions({ headers: asBearerAuth(callbackBody.accessToken) })).rejects.toMatchObject({
|
||||||
|
status: 401,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('mobile redirect override', () => {
|
describe('mobile redirect override', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await setupOAuth(admin.accessToken, {
|
await setupOAuth(admin.accessToken, {
|
||||||
@@ -399,4 +493,22 @@ describe(`/oauth`, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('allowInsecureRequests: false', () => {
|
||||||
|
beforeAll(async () => {
|
||||||
|
await setupOAuth(admin.accessToken, {
|
||||||
|
enabled: true,
|
||||||
|
clientId: OAuthClient.DEFAULT,
|
||||||
|
clientSecret: OAuthClient.DEFAULT,
|
||||||
|
allowInsecureRequests: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject OAuth discovery over HTTP', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.post('/oauth/authorize')
|
||||||
|
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
|
||||||
|
expect(status).toBe(500);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ describe('/search', () => {
|
|||||||
const bytes = await readFile(join(testAssetDir, filename));
|
const bytes = await readFile(join(testAssetDir, filename));
|
||||||
assets.push(
|
assets.push(
|
||||||
await utils.createAsset(admin.accessToken, {
|
await utils.createAsset(admin.accessToken, {
|
||||||
deviceAssetId: `test-${filename}`,
|
|
||||||
assetData: { bytes, filename },
|
assetData: { bytes, filename },
|
||||||
...dto,
|
...dto,
|
||||||
}),
|
}),
|
||||||
@@ -458,7 +457,7 @@ describe('/search', () => {
|
|||||||
expect(Array.isArray(body)).toBe(true);
|
expect(Array.isArray(body)).toBe(true);
|
||||||
if (Array.isArray(body)) {
|
if (Array.isArray(body)) {
|
||||||
expect(body.length).toBeGreaterThan(10);
|
expect(body.length).toBeGreaterThan(10);
|
||||||
expect(body[0].name).toEqual(name);
|
expect(body[0].name).toEqual(expect.stringContaining(name));
|
||||||
expect(body[0].admin2name).toEqual(name);
|
expect(body[0].admin2name).toEqual(name);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -207,16 +207,6 @@ describe('/server', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /server/theme', () => {
|
|
||||||
it('should respond with the server theme', async () => {
|
|
||||||
const { status, body } = await request(app).get('/server/theme');
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body).toEqual({
|
|
||||||
customCss: '',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('GET /server/license', () => {
|
describe('GET /server/license', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/server/license');
|
const { status, body } = await request(app).get('/server/license');
|
||||||
|
|||||||
@@ -243,9 +243,21 @@ describe('/shared-links', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should get data for correct password protected link', async () => {
|
it('should get data for correct password protected link', async () => {
|
||||||
|
const response = await request(app)
|
||||||
|
.post('/shared-links/login')
|
||||||
|
.send({ password: 'foo' })
|
||||||
|
.query({ key: linkWithPassword.key });
|
||||||
|
|
||||||
|
expect(response.status).toBe(201);
|
||||||
|
|
||||||
|
const cookies = response.get('Set-Cookie') ?? [];
|
||||||
|
expect(cookies).toHaveLength(1);
|
||||||
|
expect(cookies[0]).toContain('immich_shared_link_token');
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/shared-links/me')
|
.get('/shared-links/me')
|
||||||
.query({ key: linkWithPassword.key, password: 'foo' });
|
.query({ key: linkWithPassword.key })
|
||||||
|
.set('Cookie', cookies);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
@@ -329,7 +341,9 @@ describe('/shared-links', () => {
|
|||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require an asset/album id', async () => {
|
it('should require an asset/album id', async () => {
|
||||||
|
|||||||
@@ -41,7 +41,9 @@ describe('/stacks', () => {
|
|||||||
.send({ assetIds: [asset.id] });
|
.send({ assetIds: [asset.id] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require a valid id', async () => {
|
it('should require a valid id', async () => {
|
||||||
@@ -51,7 +53,12 @@ describe('/stacks', () => {
|
|||||||
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
|
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['assetIds', 0], message: 'Invalid UUID' },
|
||||||
|
{ path: ['assetIds', 1], message: 'Invalid UUID' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ describe('/tags', () => {
|
|||||||
.get(`/tags/${uuidDto.invalid}`)
|
.get(`/tags/${uuidDto.invalid}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get tag details', async () => {
|
it('should get tag details', async () => {
|
||||||
@@ -427,7 +427,7 @@ describe('/tags', () => {
|
|||||||
.delete(`/tags/${uuidDto.invalid}`)
|
.delete(`/tags/${uuidDto.invalid}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete a tag', async () => {
|
it('should delete a tag', async () => {
|
||||||
|
|||||||
@@ -108,14 +108,20 @@ describe('/admin/users', () => {
|
|||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
|
for (const [key, message] of [
|
||||||
|
['password', 'Invalid input: expected string, received null'],
|
||||||
|
['email', 'Invalid input: expected email, received object'],
|
||||||
|
['name', 'Invalid input: expected string, received null'],
|
||||||
|
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
|
||||||
|
['notify', 'Invalid input: expected boolean, received null'],
|
||||||
|
] as const) {
|
||||||
it(`should not allow null ${key}`, async () => {
|
it(`should not allow null ${key}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/admin/users`)
|
.post(`/admin/users`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ ...createUserDto.user1, [key]: null });
|
.send({ ...createUserDto.user1, [key]: null });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,14 +159,19 @@ describe('/admin/users', () => {
|
|||||||
expect(body).toEqual(errorDto.forbidden);
|
expect(body).toEqual(errorDto.forbidden);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
|
for (const [key, message] of [
|
||||||
|
['password', 'Invalid input: expected string, received null'],
|
||||||
|
['email', 'Invalid input: expected email, received object'],
|
||||||
|
['name', 'Invalid input: expected string, received null'],
|
||||||
|
['shouldChangePassword', 'Invalid input: expected boolean, received null'],
|
||||||
|
] as const) {
|
||||||
it(`should not allow null ${key}`, async () => {
|
it(`should not allow null ${key}`, async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/admin/users/${uuidDto.notFound}`)
|
.put(`/admin/users/${uuidDto.notFound}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`)
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
.send({ [key]: null });
|
.send({ [key]: null });
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest());
|
expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,7 +298,8 @@ describe('/admin/users', () => {
|
|||||||
it('should delete user', async () => {
|
it('should delete user', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/admin/users/${userToDelete.userId}`)
|
.delete(`/admin/users/${userToDelete.userId}`)
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({});
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ describe('/users', () => {
|
|||||||
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
|
expect(body).toMatchObject(errorDto.badRequest('Email is not available'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update my email', async () => {
|
it('should update my email', async () => {
|
||||||
@@ -178,7 +178,11 @@ describe('/users', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['download.archiveSize must be an integer number']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update download archive size', async () => {
|
it('should update download archive size', async () => {
|
||||||
@@ -204,7 +208,11 @@ describe('/users', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value']));
|
expect(body).toEqual(
|
||||||
|
errorDto.validationError([
|
||||||
|
{ path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
|
||||||
|
]),
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update download include embedded videos', async () => {
|
it('should update download include embedded videos', async () => {
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
|
||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
import { readFile } from 'node:fs/promises';
|
||||||
|
import { basename, join } from 'node:path';
|
||||||
import type { Socket } from 'socket.io-client';
|
import type { Socket } from 'socket.io-client';
|
||||||
import { utils } from 'src/utils';
|
import { testAssetDir, utils } from 'src/utils';
|
||||||
|
|
||||||
test.describe('Detail Panel', () => {
|
test.describe('Detail Panel', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
@@ -83,4 +85,42 @@ test.describe('Detail Panel', () => {
|
|||||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
||||||
await expect(textarea).toHaveValue('new description');
|
await expect(textarea).toHaveValue('new description');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test.describe('Date editor', () => {
|
||||||
|
test('displays inferred asset timezone', async ({ context, page }) => {
|
||||||
|
const test = {
|
||||||
|
filepath: 'metadata/dates/datetimeoriginal-gps.jpg',
|
||||||
|
expected: {
|
||||||
|
dateTime: '2025-12-01T11:30',
|
||||||
|
// Test with a timezone which is NOT the first among timezones with the same offset
|
||||||
|
// This is to check that the editor does not simply fall back to the first available timezone with that offset
|
||||||
|
// America/Denver (-07:00) is not the first among timezones with offset -07:00
|
||||||
|
timeZoneWithOffset: 'America/Denver (-07:00)',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const asset = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
|
bytes: await readFile(join(testAssetDir, test.filepath)),
|
||||||
|
filename: basename(test.filepath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: asset.id });
|
||||||
|
|
||||||
|
// asset viewer -> detail panel -> date editor
|
||||||
|
await utils.setAuthCookies(context, admin.accessToken);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await page.waitForSelector('#immich-asset-viewer');
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Info' }).click();
|
||||||
|
await page.getByTestId('detail-panel-edit-date-button').click();
|
||||||
|
await page.waitForSelector('[role="dialog"]');
|
||||||
|
|
||||||
|
const datetime = page.locator('#datetime');
|
||||||
|
await expect(datetime).toHaveValue(test.expected.dateTime);
|
||||||
|
const timezone = page.getByRole('combobox', { name: 'Timezone' });
|
||||||
|
await expect(timezone).toHaveValue(test.expected.timeZoneWithOffset);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ test.describe('Duplicates Utility', () => {
|
|||||||
|
|
||||||
test.beforeEach(async ({ context }) => {
|
test.beforeEach(async ({ context }) => {
|
||||||
[firstAsset, secondAsset] = await Promise.all([
|
[firstAsset, secondAsset] = await Promise.all([
|
||||||
utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-a' }),
|
utils.createAsset(admin.accessToken, {}),
|
||||||
utils.createAsset(admin.accessToken, { deviceAssetId: 'duplicate-b' }),
|
utils.createAsset(admin.accessToken, {}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await updateAssets(
|
await updateAssets(
|
||||||
|
|||||||
@@ -77,18 +77,4 @@ test.describe('Photo Viewer', () => {
|
|||||||
});
|
});
|
||||||
expect(tagAtCenter).toBe('IMG');
|
expect(tagAtCenter).toBe('IMG');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('reloads photo when checksum changes', async ({ page }) => {
|
|
||||||
await page.goto(`/photos/${asset.id}`);
|
|
||||||
|
|
||||||
const preview = page.getByTestId('preview').filter({ visible: true });
|
|
||||||
await expect(preview).toHaveAttribute('src', /.+/);
|
|
||||||
const initialSrc = await preview.getAttribute('src');
|
|
||||||
|
|
||||||
const websocketEvent = utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id });
|
|
||||||
await utils.replaceAsset(admin.accessToken, asset.id);
|
|
||||||
await websocketEvent;
|
|
||||||
|
|
||||||
await expect(preview).not.toHaveAttribute('src', initialSrc!);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -32,8 +32,12 @@ export function generateThumbhash(rng: SeededRandom): string {
|
|||||||
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
|
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateDuration(rng: SeededRandom): string {
|
export function generateDuration(rng: SeededRandom): number {
|
||||||
return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
|
return (
|
||||||
|
rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
|
||||||
|
1000 +
|
||||||
|
rng.nextInt(0, 1000)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function generateUUID(): string {
|
export function generateUUID(): string {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
AlbumUserRole,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
UserAvatarColor,
|
UserAvatarColor,
|
||||||
@@ -315,11 +316,9 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: asset.id,
|
id: asset.id,
|
||||||
deviceAssetId: `device-${asset.id}`,
|
|
||||||
ownerId: asset.ownerId,
|
ownerId: asset.ownerId,
|
||||||
owner: owner || defaultOwner,
|
owner: owner || defaultOwner,
|
||||||
libraryId: `library-${asset.ownerId}`,
|
libraryId: `library-${asset.ownerId}`,
|
||||||
deviceId: `device-${asset.ownerId}`,
|
|
||||||
type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image,
|
type: asset.isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image,
|
||||||
originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
|
originalPath: `/original/${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
|
||||||
originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
|
originalFileName: `${asset.id}.${asset.isVideo ? 'mp4' : 'jpg'}`,
|
||||||
@@ -334,7 +333,7 @@ export function toAssetResponseDto(asset: MockTimelineAsset, owner?: UserRespons
|
|||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: asset.isTrashed,
|
isTrashed: asset.isTrashed,
|
||||||
visibility: asset.visibility,
|
visibility: asset.visibility,
|
||||||
duration: asset.duration || '0:00:00.00000',
|
duration: asset.duration,
|
||||||
exifInfo,
|
exifInfo,
|
||||||
livePhotoVideoId: asset.livePhotoVideoId,
|
livePhotoVideoId: asset.livePhotoVideoId,
|
||||||
tags: [],
|
tags: [],
|
||||||
@@ -422,14 +421,11 @@ export function getAlbum(
|
|||||||
albumThumbnailAssetId: album.thumbnailAssetId,
|
albumThumbnailAssetId: album.thumbnailAssetId,
|
||||||
createdAt: album.createdAt,
|
createdAt: album.createdAt,
|
||||||
updatedAt: album.updatedAt,
|
updatedAt: album.updatedAt,
|
||||||
ownerId: albumOwner.id,
|
albumUsers: [{ user: albumOwner, role: AlbumUserRole.Owner }],
|
||||||
owner: albumOwner,
|
|
||||||
albumUsers: [], // Empty array for non-shared album
|
|
||||||
shared: false,
|
shared: false,
|
||||||
hasSharedLink: false,
|
hasSharedLink: false,
|
||||||
isActivityEnabled: true,
|
isActivityEnabled: true,
|
||||||
assetCount: albumAssets.length,
|
assetCount: albumAssets.length,
|
||||||
assets: albumAssets,
|
|
||||||
startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined,
|
startDate: albumAssets.length > 0 ? albumAssets.at(-1)?.fileCreatedAt : undefined,
|
||||||
endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
|
endDate: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
|
||||||
lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
|
lastModifiedAssetTimestamp: albumAssets.length > 0 ? albumAssets[0].fileCreatedAt : undefined,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
|
|||||||
isTrashed: boolean;
|
isTrashed: boolean;
|
||||||
isVideo: boolean;
|
isVideo: boolean;
|
||||||
isImage: boolean;
|
isImage: boolean;
|
||||||
duration: string | null;
|
duration: number | null;
|
||||||
projectionType: string | null;
|
projectionType: string | null;
|
||||||
livePhotoVideoId: string | null;
|
livePhotoVideoId: string | null;
|
||||||
city: string | null;
|
city: string | null;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
import { playwrightHost } from 'playwright.config';
|
import { playwrightHost } from 'src/../playwright.config';
|
||||||
|
|
||||||
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserId: string) => {
|
||||||
await context.addCookies([
|
await context.addCookies([
|
||||||
@@ -173,6 +173,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||||||
'.mpeg',
|
'.mpeg',
|
||||||
'.mpg',
|
'.mpg',
|
||||||
'.mts',
|
'.mts',
|
||||||
|
'.ts',
|
||||||
'.vob',
|
'.vob',
|
||||||
'.webm',
|
'.webm',
|
||||||
'.wmv',
|
'.wmv',
|
||||||
@@ -222,6 +223,7 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||||||
'.jp2',
|
'.jp2',
|
||||||
'.jpe',
|
'.jpe',
|
||||||
'.jxl',
|
'.jxl',
|
||||||
|
'.mpo',
|
||||||
'.svg',
|
'.svg',
|
||||||
'.tif',
|
'.tif',
|
||||||
'.tiff',
|
'.tiff',
|
||||||
@@ -238,7 +240,8 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
await context.route('**/api/albums*', async (route, request) => {
|
await context.route('**/api/albums*', async (route, request) => {
|
||||||
if (request.url().endsWith('albums?shared=true') || request.url().endsWith('albums')) {
|
const url = request.url();
|
||||||
|
if (url.endsWith('albums?isShared=true') || url.endsWith('albums?isOwned=true') || url.endsWith('albums')) {
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
id: assetId,
|
id: assetId,
|
||||||
deviceAssetId: `device-${assetId}`,
|
|
||||||
ownerId,
|
ownerId,
|
||||||
owner: {
|
owner: {
|
||||||
id: ownerId,
|
id: ownerId,
|
||||||
@@ -27,7 +26,6 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
|||||||
avatarColor: 'blue' as never,
|
avatarColor: 'blue' as never,
|
||||||
},
|
},
|
||||||
libraryId: `library-${ownerId}`,
|
libraryId: `library-${ownerId}`,
|
||||||
deviceId: `device-${ownerId}`,
|
|
||||||
type: AssetTypeEnum.Image,
|
type: AssetTypeEnum.Image,
|
||||||
originalPath: `/original/${assetId}.jpg`,
|
originalPath: `/original/${assetId}.jpg`,
|
||||||
originalFileName: `${assetId}.jpg`,
|
originalFileName: `${assetId}.jpg`,
|
||||||
@@ -42,7 +40,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
|||||||
isArchived: false,
|
isArchived: false,
|
||||||
isTrashed: false,
|
isTrashed: false,
|
||||||
visibility: AssetVisibility.Timeline,
|
visibility: AssetVisibility.Timeline,
|
||||||
duration: '0:00:00.00000',
|
duration: null,
|
||||||
exifInfo: {
|
exifInfo: {
|
||||||
make: null,
|
make: null,
|
||||||
model: null,
|
model: null,
|
||||||
@@ -69,7 +67,7 @@ export const createMockStackAsset = (ownerId: string): AssetResponseDto => {
|
|||||||
tags: [],
|
tags: [],
|
||||||
people: [],
|
people: [],
|
||||||
unassignedFaces: [],
|
unassignedFaces: [],
|
||||||
stack: null,
|
stack: undefined,
|
||||||
isOffline: false,
|
isOffline: false,
|
||||||
hasMetadata: true,
|
hasMetadata: true,
|
||||||
duplicateId: null,
|
duplicateId: null,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
generateTimelineData,
|
generateTimelineData,
|
||||||
TimelineAssetConfig,
|
TimelineAssetConfig,
|
||||||
TimelineData,
|
TimelineData,
|
||||||
|
toAssetResponseDto,
|
||||||
} from 'src/ui/generators/timeline';
|
} from 'src/ui/generators/timeline';
|
||||||
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
import { setupBaseMockApiRoutes } from 'src/ui/mock-network/base-network';
|
||||||
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
import { setupTimelineMockApiRoutes, TimelineTestContext } from 'src/ui/mock-network/timeline-network';
|
||||||
@@ -30,6 +31,10 @@ test.describe('search gallery-viewer', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
test.beforeAll(async () => {
|
test.beforeAll(async () => {
|
||||||
|
test.fail(
|
||||||
|
process.env.PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS !== '1',
|
||||||
|
'This test requires env var: PW_EXPERIMENTAL_SERVICE_WORKER_NETWORK_EVENTS=1',
|
||||||
|
);
|
||||||
adminUserId = faker.string.uuid();
|
adminUserId = faker.string.uuid();
|
||||||
testContext.adminId = adminUserId;
|
testContext.adminId = adminUserId;
|
||||||
timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId });
|
timelineRestData = generateTimelineData({ ...createDefaultTimelineConfig(), ownerId: adminUserId });
|
||||||
@@ -44,7 +49,10 @@ test.describe('search gallery-viewer', () => {
|
|||||||
|
|
||||||
await context.route('**/api/search/metadata', async (route, request) => {
|
await context.route('**/api/search/metadata', async (route, request) => {
|
||||||
if (request.method() === 'POST') {
|
if (request.method() === 'POST') {
|
||||||
const searchAssets = assets.slice(0, 5).filter((asset) => !changes.assetDeletions.includes(asset.id));
|
const searchAssets = assets
|
||||||
|
.slice(0, 5)
|
||||||
|
.filter((asset) => !changes.assetDeletions.includes(asset.id))
|
||||||
|
.map((asset) => toAssetResponseDto(asset));
|
||||||
return route.fulfill({
|
return route.fulfill({
|
||||||
status: 200,
|
status: 200,
|
||||||
contentType: 'application/json',
|
contentType: 'application/json',
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ test.describe('Timeline', () => {
|
|||||||
await page.keyboard.down('Shift');
|
await page.keyboard.down('Shift');
|
||||||
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
|
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
|
||||||
await expect(
|
await expect(
|
||||||
thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
|
thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'),
|
||||||
).toHaveCount(3);
|
).toHaveCount(3);
|
||||||
await thumbnailUtils.selectButton(page, assets[2].id).click();
|
await thumbnailUtils.selectButton(page, assets[2].id).click();
|
||||||
await page.keyboard.up('Shift');
|
await page.keyboard.up('Shift');
|
||||||
@@ -349,7 +349,7 @@ test.describe('Timeline', () => {
|
|||||||
expect(visibleMockAssetsYearMonths).toContain(month);
|
expect(visibleMockAssetsYearMonths).toContain(month);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
test('Deep link to last photo, scroll up', async ({ page }) => {
|
test.skip('Deep link to last photo, scroll up', async ({ page }) => {
|
||||||
const lastAsset = assets.at(-1)!;
|
const lastAsset = assets.at(-1)!;
|
||||||
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
|
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
|
||||||
|
|
||||||
@@ -361,7 +361,7 @@ test.describe('Timeline', () => {
|
|||||||
|
|
||||||
await thumbnailUtils.expectInViewport(page, '14e5901f-fd7f-40c0-b186-4d7e7fc67968');
|
await thumbnailUtils.expectInViewport(page, '14e5901f-fd7f-40c0-b186-4d7e7fc67968');
|
||||||
});
|
});
|
||||||
test('Deep link to first bucket, scroll down', async ({ page }) => {
|
test.skip('Deep link to first bucket, scroll down', async ({ page }) => {
|
||||||
const lastAsset = assets.at(0)!;
|
const lastAsset = assets.at(0)!;
|
||||||
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
|
await pageUtils.deepLinkPhotosPage(page, lastAsset.id);
|
||||||
await timelineUtils.locator(page).hover();
|
await timelineUtils.locator(page).hover();
|
||||||
@@ -440,7 +440,7 @@ test.describe('Timeline', () => {
|
|||||||
await thumbnailUtils.expectInViewport(page, asset.id);
|
await thumbnailUtils.expectInViewport(page, asset.id);
|
||||||
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
await thumbnailUtils.expectSelectedDisabled(page, asset.id);
|
||||||
});
|
});
|
||||||
test('Add photos to album', async ({ page }) => {
|
test.skip('Add photos to album', async ({ page }) => {
|
||||||
const album = timelineRestData.album;
|
const album = timelineRestData.album;
|
||||||
await pageUtils.openAlbumPage(page, album.id);
|
await pageUtils.openAlbumPage(page, album.id);
|
||||||
await page.locator('nav button[aria-label="Add photos"]').click();
|
await page.locator('nav button[aria-label="Add photos"]').click();
|
||||||
@@ -752,7 +752,7 @@ test.describe('Timeline', () => {
|
|||||||
await page.getByText('Photos', { exact: true }).click();
|
await page.getByText('Photos', { exact: true }).click();
|
||||||
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
|
await thumbnailUtils.expectInViewport(page, assetToFavorite.id);
|
||||||
});
|
});
|
||||||
test('open /favorites, archive photo, unarchive photo', async ({ page }) => {
|
test.skip('open /favorites, archive photo, unarchive photo', async ({ page }) => {
|
||||||
await pageUtils.openFavorites(page);
|
await pageUtils.openFavorites(page);
|
||||||
const assetToArchive = getAsset(timelineRestData, 'ad31e29f-2069-4574-b9a9-ad86523c92cb')!;
|
const assetToArchive = getAsset(timelineRestData, 'ad31e29f-2069-4574-b9a9-ad86523c92cb')!;
|
||||||
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();
|
await thumbnailUtils.withAssetId(page, assetToArchive.id).hover();
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
CheckExistingAssetsDto,
|
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
JobCreateDto,
|
JobCreateDto,
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
UserAdminCreateDto,
|
UserAdminCreateDto,
|
||||||
UserPreferencesUpdateDto,
|
UserPreferencesUpdateDto,
|
||||||
ValidateLibraryDto,
|
ValidateLibraryDto,
|
||||||
checkExistingAssets,
|
|
||||||
createAlbum,
|
createAlbum,
|
||||||
createApiKey,
|
createApiKey,
|
||||||
createJob,
|
createJob,
|
||||||
@@ -343,8 +341,6 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
) => {
|
) => {
|
||||||
const _dto = {
|
const _dto = {
|
||||||
deviceAssetId: 'test-1',
|
|
||||||
deviceId: 'test',
|
|
||||||
fileCreatedAt: new Date().toISOString(),
|
fileCreatedAt: new Date().toISOString(),
|
||||||
fileModifiedAt: new Date().toISOString(),
|
fileModifiedAt: new Date().toISOString(),
|
||||||
...dto,
|
...dto,
|
||||||
@@ -375,40 +371,6 @@ export const utils = {
|
|||||||
return body as AssetMediaResponseDto;
|
return body as AssetMediaResponseDto;
|
||||||
},
|
},
|
||||||
|
|
||||||
replaceAsset: async (
|
|
||||||
accessToken: string,
|
|
||||||
assetId: string,
|
|
||||||
dto?: Partial<Omit<AssetMediaCreateDto, 'assetData'>> & { assetData?: FileData },
|
|
||||||
) => {
|
|
||||||
const _dto = {
|
|
||||||
deviceAssetId: 'test-1',
|
|
||||||
deviceId: 'test',
|
|
||||||
fileCreatedAt: new Date().toISOString(),
|
|
||||||
fileModifiedAt: new Date().toISOString(),
|
|
||||||
...dto,
|
|
||||||
};
|
|
||||||
|
|
||||||
const assetData = dto?.assetData?.bytes || makeRandomImage();
|
|
||||||
const filename = dto?.assetData?.filename || 'example.png';
|
|
||||||
|
|
||||||
if (dto?.assetData?.bytes) {
|
|
||||||
console.log(`Uploading ${filename}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const builder = request(app)
|
|
||||||
.put(`/assets/${assetId}/original`)
|
|
||||||
.attach('assetData', assetData, filename)
|
|
||||||
.set('Authorization', `Bearer ${accessToken}`);
|
|
||||||
|
|
||||||
for (const [key, value] of Object.entries(_dto)) {
|
|
||||||
void builder.field(key, String(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
const { body } = await builder;
|
|
||||||
|
|
||||||
return body as AssetMediaResponseDto;
|
|
||||||
},
|
|
||||||
|
|
||||||
createImageFile: (path: string) => {
|
createImageFile: (path: string) => {
|
||||||
if (!existsSync(dirname(path))) {
|
if (!existsSync(dirname(path))) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
@@ -450,9 +412,6 @@ export const utils = {
|
|||||||
|
|
||||||
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
getAssetInfo: (accessToken: string, id: string) => getAssetInfo({ id }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|
||||||
checkExistingAssets: (accessToken: string, checkExistingAssetsDto: CheckExistingAssetsDto) =>
|
|
||||||
checkExistingAssets({ checkExistingAssetsDto }, { headers: asBearerAuth(accessToken) }),
|
|
||||||
|
|
||||||
searchAssets: async (accessToken: string, dto: MetadataSearchDto) => {
|
searchAssets: async (accessToken: string, dto: MetadataSearchDto) => {
|
||||||
return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
return searchAssets({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
|
||||||
},
|
},
|
||||||
|
|||||||
+1
-1
Submodule e2e/test-assets updated: 163c251744...6742055402
+3
-1
@@ -14,8 +14,10 @@
|
|||||||
"outDir": "./dist",
|
"outDir": "./dist",
|
||||||
"incremental": true,
|
"incremental": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
"paths": {
|
||||||
|
"src/*": ["./src/*"]
|
||||||
|
},
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"baseUrl": "./"
|
|
||||||
},
|
},
|
||||||
"include": ["src/**/*.ts", "vitest*.config.ts"],
|
"include": ["src/**/*.ts", "vitest*.config.ts"],
|
||||||
"exclude": ["dist", "node_modules"]
|
"exclude": ["dist", "node_modules"]
|
||||||
|
|||||||
+17
-8
@@ -3,7 +3,7 @@
|
|||||||
"account": "حساب",
|
"account": "حساب",
|
||||||
"account_settings": "إعدادات الحساب",
|
"account_settings": "إعدادات الحساب",
|
||||||
"acknowledge": "أُدرك ذلك",
|
"acknowledge": "أُدرك ذلك",
|
||||||
"action": "عملية",
|
"action": "إجراء",
|
||||||
"action_common_update": "تحديث",
|
"action_common_update": "تحديث",
|
||||||
"action_description": "مجموعة من الفعاليات التي ستنفذ على الأصول التي تم تصفيتها",
|
"action_description": "مجموعة من الفعاليات التي ستنفذ على الأصول التي تم تصفيتها",
|
||||||
"actions": "عمليات",
|
"actions": "عمليات",
|
||||||
@@ -61,8 +61,8 @@
|
|||||||
"backup_onboarding_1_description": "نسخة خارج الموقع في موقع آخر.",
|
"backup_onboarding_1_description": "نسخة خارج الموقع في موقع آخر.",
|
||||||
"backup_onboarding_2_description": "نسخ محلية على أجهزة مختلفة. يشمل ذلك الملفات الرئيسية ونسخة احتياطية محلية منها.",
|
"backup_onboarding_2_description": "نسخ محلية على أجهزة مختلفة. يشمل ذلك الملفات الرئيسية ونسخة احتياطية محلية منها.",
|
||||||
"backup_onboarding_3_description": "إجمالي نُسخ بياناتك، بما في ذلك الملفات الأصلية. يشمل ذلك نسخةً واحدةً خارج الموقع ونسختين محليتين.",
|
"backup_onboarding_3_description": "إجمالي نُسخ بياناتك، بما في ذلك الملفات الأصلية. يشمل ذلك نسخةً واحدةً خارج الموقع ونسختين محليتين.",
|
||||||
"backup_onboarding_description": "يُنصح باتباع <backblaze-link>استراتيجية النسخ الاحتياطي 3-2-1</backblaze-link> لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.",
|
"backup_onboarding_description": "يُنصح باتباع <backblaze-link>استراتيجية النسخ الاحتياطي 3-2- 1</backblaze-link> لحماية بياناتك. احتفظ بنسخ احتياطية من صورك/فيديوهاتك المحمّلة، بالإضافة إلى قاعدة بيانات Immich، لضمان حل نسخ احتياطي شامل.",
|
||||||
"backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ Immich، يرجى الرجوع إلى <link> التعليمات </link>.",
|
"backup_onboarding_footer": "لمزيد من المعلومات حول النسخ الاحتياطي لـ <link>Immich</link>، يرجى الرجوع إلى <link>الوثائق</link>.",
|
||||||
"backup_onboarding_parts_title": "يتضمن النسخ الاحتياطي 3-2-1 ما يلي:",
|
"backup_onboarding_parts_title": "يتضمن النسخ الاحتياطي 3-2-1 ما يلي:",
|
||||||
"backup_onboarding_title": "النسخ الاحتياطية",
|
"backup_onboarding_title": "النسخ الاحتياطية",
|
||||||
"backup_settings": "إعدادات تفريغ قاعدة البيانات",
|
"backup_settings": "إعدادات تفريغ قاعدة البيانات",
|
||||||
@@ -333,7 +333,7 @@
|
|||||||
"storage_template_migration_description": "قم بتطبيق القالب الحالي <link>{template}</link> على المحتويات التي تم رفعها سابقًا",
|
"storage_template_migration_description": "قم بتطبيق القالب الحالي <link>{template}</link> على المحتويات التي تم رفعها سابقًا",
|
||||||
"storage_template_migration_info": "تغييرات النموذج الخزني ستغير جميع الصيغ الى احرف صغيرة. تغييرات النموذج ستنطبق فقط على المحتويات الجديدة. لتطبيق النموذج على المحتويات التي تم رفعها سابقًا، قم بتشغيل <link>{job}</link>.",
|
"storage_template_migration_info": "تغييرات النموذج الخزني ستغير جميع الصيغ الى احرف صغيرة. تغييرات النموذج ستنطبق فقط على المحتويات الجديدة. لتطبيق النموذج على المحتويات التي تم رفعها سابقًا، قم بتشغيل <link>{job}</link>.",
|
||||||
"storage_template_migration_job": "وظيفة تهجير قالب التخزين",
|
"storage_template_migration_job": "وظيفة تهجير قالب التخزين",
|
||||||
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و<implications-link>implications</implications-link>",
|
"storage_template_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <template-link>Storage Template</template-link> و <implications-link>implications</implications-link>.",
|
||||||
"storage_template_onboarding_description_v2": "عند التفعيل. هذه الخاصية ستقوم بالترتيب التلقائي للملفات بناء على نموذج معرف من قبل المستخدم. رجاء اطلع على <link>التوثيق</link>.",
|
"storage_template_onboarding_description_v2": "عند التفعيل. هذه الخاصية ستقوم بالترتيب التلقائي للملفات بناء على نموذج معرف من قبل المستخدم. رجاء اطلع على <link>التوثيق</link>.",
|
||||||
"storage_template_path_length": "الحد التقريبي لطول المسار: <b>{length, number}</b>/{limit, number}",
|
"storage_template_path_length": "الحد التقريبي لطول المسار: <b>{length, number}</b>/{limit, number}",
|
||||||
"storage_template_settings": "قالب التخزين",
|
"storage_template_settings": "قالب التخزين",
|
||||||
@@ -372,7 +372,7 @@
|
|||||||
"transcoding_audio_codec": "كود الصوت",
|
"transcoding_audio_codec": "كود الصوت",
|
||||||
"transcoding_audio_codec_description": "Opus هو الخيار ذو أعلى جودة، ولكنه يتمتع بتوافق أقل مع الأجهزة أو البرمجيات القديمة.",
|
"transcoding_audio_codec_description": "Opus هو الخيار ذو أعلى جودة، ولكنه يتمتع بتوافق أقل مع الأجهزة أو البرمجيات القديمة.",
|
||||||
"transcoding_bitrate_description": "مقاطع الفيديو التي يتجاوز معدل البت أقصى قيمة أو التي لا تكون في تنسيق مقبول",
|
"transcoding_bitrate_description": "مقاطع الفيديو التي يتجاوز معدل البت أقصى قيمة أو التي لا تكون في تنسيق مقبول",
|
||||||
"transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg لل<h264-link>H.264 codec</h264-link>, <hevc-link>HEVC codec</hevc-link> and <vp9-link>VP9 codec</vp9-link>.",
|
"transcoding_codecs_learn_more": "لمعرفة المزيد حول المصطلحات المستخدمة هنا، يرجى الرجوع إلى وثائق FFmpeg لـ <h264-link>H.264 codec</h264-link>، و <hevc-link>HEVC codec</hevc-link> و <vp9-link>VP9 codec</vp9-link>.",
|
||||||
"transcoding_constant_quality_mode": "وضع الجودة الثابتة",
|
"transcoding_constant_quality_mode": "وضع الجودة الثابتة",
|
||||||
"transcoding_constant_quality_mode_description": "ICQ أفضل من CQP، ولكن بعض أجهزة عتاد التسريع لا تدعم هذا الوضع. تعيين هذا الخيار يسجعل الأفضلية للوضع المحدد عند استخدام الترميز بناءً على الجودة. يتم تجاهله بواسطة NVENC لأنه لا يدعم ICQ.",
|
"transcoding_constant_quality_mode_description": "ICQ أفضل من CQP، ولكن بعض أجهزة عتاد التسريع لا تدعم هذا الوضع. تعيين هذا الخيار يسجعل الأفضلية للوضع المحدد عند استخدام الترميز بناءً على الجودة. يتم تجاهله بواسطة NVENC لأنه لا يدعم ICQ.",
|
||||||
"transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)",
|
"transcoding_constant_rate_factor": "عامل معدل الجودة الثابت (-crf)",
|
||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "إنشاء رابط للمشاركة",
|
"create_link_to_share": "إنشاء رابط للمشاركة",
|
||||||
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
|
"create_link_to_share_description": "السماح لأي شخص لديه الرابط بمشاهدة الصورة (الصور) المحددة",
|
||||||
"create_new": "انشاء جديد",
|
"create_new": "انشاء جديد",
|
||||||
|
"create_new_face": "إنشاء وجه جديد",
|
||||||
"create_new_person": "إنشاء شخص جديد",
|
"create_new_person": "إنشاء شخص جديد",
|
||||||
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
|
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
|
||||||
"create_new_user": "إنشاء مستخدم جديد",
|
"create_new_user": "إنشاء مستخدم جديد",
|
||||||
|
"create_person": "إنشاء شخص",
|
||||||
|
"create_person_subtitle": "أضف اسماً للوجه المحدد لإنشاء الشخص الجديد والإشارة إليه",
|
||||||
"create_shared_album_page_share_add_assets": "إضافة الأصول",
|
"create_shared_album_page_share_add_assets": "إضافة الأصول",
|
||||||
"create_shared_album_page_share_select_photos": "حدد الصور",
|
"create_shared_album_page_share_select_photos": "حدد الصور",
|
||||||
"create_shared_link": "انشاء رابط مشترك",
|
"create_shared_link": "انشاء رابط مشترك",
|
||||||
@@ -892,6 +895,8 @@
|
|||||||
"day": "يوم",
|
"day": "يوم",
|
||||||
"days": "ايام",
|
"days": "ايام",
|
||||||
"deduplicate_all": "إلغاء تكرار الكل",
|
"deduplicate_all": "إلغاء تكرار الكل",
|
||||||
|
"default_locale": "الإعدادات المحلية الافتراضية",
|
||||||
|
"default_locale_description": "تنسيق التواريخ والأرقام بناءً على الإعدادات المحلية للمتصفح",
|
||||||
"delete": "حذف",
|
"delete": "حذف",
|
||||||
"delete_action_confirmation_message": "هل انت متأكد من حذف هذا الملف؟ هذا سؤدي الى نقل الملف الى سلة مهملات الخادم وسيتم اشعارك ان كنت تريد حذفه على الجهاز",
|
"delete_action_confirmation_message": "هل انت متأكد من حذف هذا الملف؟ هذا سؤدي الى نقل الملف الى سلة مهملات الخادم وسيتم اشعارك ان كنت تريد حذفه على الجهاز",
|
||||||
"delete_action_prompt": "تم حذف {count}",
|
"delete_action_prompt": "تم حذف {count}",
|
||||||
@@ -1384,9 +1389,11 @@
|
|||||||
"library_page_sort_title": "عنوان الألبوم",
|
"library_page_sort_title": "عنوان الألبوم",
|
||||||
"licenses": "رُخَص",
|
"licenses": "رُخَص",
|
||||||
"light": "المضيئ",
|
"light": "المضيئ",
|
||||||
|
"light_theme": "التبديل إلى المظهر الفاتح",
|
||||||
"like": "اعجاب",
|
"like": "اعجاب",
|
||||||
"like_deleted": "تم حذف الإعجاب",
|
"like_deleted": "تم حذف الإعجاب",
|
||||||
"link_motion_video": "رابط فيديو الحركة",
|
"link_motion_video": "رابط فيديو الحركة",
|
||||||
|
"link_to_docs": "لمزيد من المعلومات، يُرجى الرجوع إلى <link>الوثائق</link>.",
|
||||||
"link_to_oauth": "الربط مع OAuth",
|
"link_to_oauth": "الربط مع OAuth",
|
||||||
"linked_oauth_account": "حساب مرتبط بـ OAuth",
|
"linked_oauth_account": "حساب مرتبط بـ OAuth",
|
||||||
"list": "قائمة",
|
"list": "قائمة",
|
||||||
@@ -2210,6 +2217,7 @@
|
|||||||
"tag": "العلامة",
|
"tag": "العلامة",
|
||||||
"tag_assets": "أصول العلامة",
|
"tag_assets": "أصول العلامة",
|
||||||
"tag_created": "تم إنشاء العلامة: {tag}",
|
"tag_created": "تم إنشاء العلامة: {tag}",
|
||||||
|
"tag_face": "علِّم الوجه",
|
||||||
"tag_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب مواضيع العلامات المنطقية",
|
"tag_feature_description": "تصفح الصور ومقاطع الفيديو المجمعة حسب مواضيع العلامات المنطقية",
|
||||||
"tag_not_found_question": "لا يمكن العثور على علامة؟ <link>قم بإنشاء علامة جديدة.</link>",
|
"tag_not_found_question": "لا يمكن العثور على علامة؟ <link>قم بإنشاء علامة جديدة.</link>",
|
||||||
"tag_people": "علِّم الأشخاص",
|
"tag_people": "علِّم الأشخاص",
|
||||||
@@ -2384,13 +2392,14 @@
|
|||||||
"view_name": "عرض",
|
"view_name": "عرض",
|
||||||
"view_next_asset": "عرض المحتوى التالي",
|
"view_next_asset": "عرض المحتوى التالي",
|
||||||
"view_previous_asset": "عرض المحتوى السابق",
|
"view_previous_asset": "عرض المحتوى السابق",
|
||||||
"view_qr_code": "عرض رمز الاستجابة السريعة",
|
"view_qr_code": "عرض رمز الاستجابة السريعة",
|
||||||
"view_similar_photos": "عرض صور مشابهة",
|
"view_similar_photos": "عرض صور مشابهة",
|
||||||
"view_stack": "عرض التكديس",
|
"view_stack": "عرض التكديس",
|
||||||
"view_user": "عرض المستخدم",
|
"view_user": "عرض المستخدم",
|
||||||
"viewer_remove_from_stack": "حذف من الكومه أو المجموعة",
|
"viewer_remove_from_stack": "حذف من الكومه أو المجموعة",
|
||||||
"viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي",
|
"viewer_stack_use_as_main_asset": "استخدم كأصل رئيسي",
|
||||||
"viewer_unstack": "فك الكومه",
|
"viewer_unstack": "فك الكومه",
|
||||||
|
"visibility": "إمكانية الرؤية",
|
||||||
"visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}",
|
"visibility_changed": "الرؤية تغيرت لـ {count, plural, one {شخص واحد} other {# عدة أشخاص}}",
|
||||||
"visual": "مرئي",
|
"visual": "مرئي",
|
||||||
"visual_builder": "اداة نشاء مرئية",
|
"visual_builder": "اداة نشاء مرئية",
|
||||||
@@ -2402,14 +2411,14 @@
|
|||||||
"welcome_to_immich": "مرحباً بك في Immich",
|
"welcome_to_immich": "مرحباً بك في Immich",
|
||||||
"width": "عُرض",
|
"width": "عُرض",
|
||||||
"wifi_name": "اسم شبكة Wi-Fi",
|
"wifi_name": "اسم شبكة Wi-Fi",
|
||||||
"workflow_delete_prompt": "هل أنت متأكد من حذف سير العمل هذا؟",
|
"workflow_delete_prompt": "متأكد من حذف سير العمل هذا؟",
|
||||||
"workflow_deleted": "تم حذف سير العمل",
|
"workflow_deleted": "تم حذف سير العمل",
|
||||||
"workflow_description": "وصف سير العمل",
|
"workflow_description": "وصف سير العمل",
|
||||||
"workflow_info": "معلومات سير العمل",
|
"workflow_info": "معلومات سير العمل",
|
||||||
"workflow_json": "ملف JSON لسير العمل",
|
"workflow_json": "ملف JSON لسير العمل",
|
||||||
"workflow_json_help": "قم بتعديل إعدادات سير العمل بصيغة JSON. ستتم مزامنة التغييرات مع أداة الإنشاء المرئية.",
|
"workflow_json_help": "قم بتعديل إعدادات سير العمل بصيغة JSON. ستتم مزامنة التغييرات مع أداة الإنشاء المرئية.",
|
||||||
"workflow_name": "اسم سير العمل",
|
"workflow_name": "اسم سير العمل",
|
||||||
"workflow_navigation_prompt": "هل انت متاكد من المغادرة بدون حفظ التغييرات؟",
|
"workflow_navigation_prompt": "متاكد من المغادرة بدون حفظ التغييرات؟",
|
||||||
"workflow_summary": "ملخص سير العمل",
|
"workflow_summary": "ملخص سير العمل",
|
||||||
"workflow_update_success": "تم تحديث سير العمل بنجاح",
|
"workflow_update_success": "تم تحديث سير العمل بنجاح",
|
||||||
"workflow_updated": "تم تحديث سير العمل",
|
"workflow_updated": "تم تحديث سير العمل",
|
||||||
|
|||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Създаване на линк за споделяне",
|
"create_link_to_share": "Създаване на линк за споделяне",
|
||||||
"create_link_to_share_description": "Позволете на всеки, който има линк, да види избраната(ите) снимка(и)",
|
"create_link_to_share_description": "Позволете на всеки, който има линк, да види избраната(ите) снимка(и)",
|
||||||
"create_new": "СЪЗДАЙ НОВ",
|
"create_new": "СЪЗДАЙ НОВ",
|
||||||
|
"create_new_face": "Създай ново лице",
|
||||||
"create_new_person": "Създаване на ново лице",
|
"create_new_person": "Създаване на ново лице",
|
||||||
"create_new_person_hint": "Присвойте избраните файлове на нов човек",
|
"create_new_person_hint": "Присвойте избраните файлове на нов човек",
|
||||||
"create_new_user": "Създаване на нов потребител",
|
"create_new_user": "Създаване на нов потребител",
|
||||||
|
"create_person": "Създай човек",
|
||||||
|
"create_person_subtitle": "Добави име към избраното лице за да създадеш и да сложиш етикет на новия човек",
|
||||||
"create_shared_album_page_share_add_assets": "ДОБАВИ ОБЕКТИ",
|
"create_shared_album_page_share_add_assets": "ДОБАВИ ОБЕКТИ",
|
||||||
"create_shared_album_page_share_select_photos": "Избери снимки",
|
"create_shared_album_page_share_select_photos": "Избери снимки",
|
||||||
"create_shared_link": "Създай линк за споделяне",
|
"create_shared_link": "Създай линк за споделяне",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Таг",
|
"tag": "Таг",
|
||||||
"tag_assets": "Тагни елементи",
|
"tag_assets": "Тагни елементи",
|
||||||
"tag_created": "Създаден етикет: {tag}",
|
"tag_created": "Създаден етикет: {tag}",
|
||||||
|
"tag_face": "Отбележи лице",
|
||||||
"tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове",
|
"tag_feature_description": "Разглеждане на снимки и видеоклипове, групирани по теми с логически тагове",
|
||||||
"tag_not_found_question": "Не можете да намерите етикет? <link>Създайте нов етикет.</link>",
|
"tag_not_found_question": "Не можете да намерите етикет? <link>Създайте нов етикет.</link>",
|
||||||
"tag_people": "Отбележи Хора",
|
"tag_people": "Отбележи Хора",
|
||||||
|
|||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Crear enllaç per compartir",
|
"create_link_to_share": "Crear enllaç per compartir",
|
||||||
"create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades",
|
"create_link_to_share_description": "Deixa que qualsevol persona amb l'enllaç vegi les fotos seleccionades",
|
||||||
"create_new": "CREAR NOU",
|
"create_new": "CREAR NOU",
|
||||||
|
"create_new_face": "Crea una nova cara",
|
||||||
"create_new_person": "Crea una nova persona",
|
"create_new_person": "Crea una nova persona",
|
||||||
"create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
|
"create_new_person_hint": "Assigna els elements seleccionats a una persona nova",
|
||||||
"create_new_user": "Crea un usuari nou",
|
"create_new_user": "Crea un usuari nou",
|
||||||
|
"create_person": "Crea una persona",
|
||||||
|
"create_person_subtitle": "Afegeix un nom a la cara seleccionada per crear i etiquetar la nova persona",
|
||||||
"create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS",
|
"create_shared_album_page_share_add_assets": "AFEGEIX ELEMENTS",
|
||||||
"create_shared_album_page_share_select_photos": "Escull fotografies",
|
"create_shared_album_page_share_select_photos": "Escull fotografies",
|
||||||
"create_shared_link": "Crea un enllaç compartit",
|
"create_shared_link": "Crea un enllaç compartit",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"tag_assets": "Etiquetar actius",
|
"tag_assets": "Etiquetar actius",
|
||||||
"tag_created": "Etiqueta creada: {tag}",
|
"tag_created": "Etiqueta creada: {tag}",
|
||||||
|
"tag_face": "Etiqueta una cara",
|
||||||
"tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques",
|
"tag_feature_description": "Exploreu fotos i vídeos agrupats per temes d'etiquetes lògiques",
|
||||||
"tag_not_found_question": "No trobeu una etiqueta? <link>Crear una nova etiqueta.</link>",
|
"tag_not_found_question": "No trobeu una etiqueta? <link>Crear una nova etiqueta.</link>",
|
||||||
"tag_people": "Etiquetar personas",
|
"tag_people": "Etiquetar personas",
|
||||||
|
|||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Opret link for at dele",
|
"create_link_to_share": "Opret link for at dele",
|
||||||
"create_link_to_share_description": "Tillad alle med linket at se de(t) valgte billede(r)",
|
"create_link_to_share_description": "Tillad alle med linket at se de(t) valgte billede(r)",
|
||||||
"create_new": "OPRET NY",
|
"create_new": "OPRET NY",
|
||||||
|
"create_new_face": "Opret nyt ansigt",
|
||||||
"create_new_person": "Opret ny person",
|
"create_new_person": "Opret ny person",
|
||||||
"create_new_person_hint": "Tildel valgte aktiver til en ny person",
|
"create_new_person_hint": "Tildel valgte aktiver til en ny person",
|
||||||
"create_new_user": "Opret ny bruger",
|
"create_new_user": "Opret ny bruger",
|
||||||
|
"create_person": "Opret person",
|
||||||
|
"create_person_subtitle": "Tilføj et navn til det valgte ansigt for at oprette og tagge den nye person",
|
||||||
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
|
"create_shared_album_page_share_add_assets": "TILFØJ ELEMENT",
|
||||||
"create_shared_album_page_share_select_photos": "Vælg Billeder",
|
"create_shared_album_page_share_select_photos": "Vælg Billeder",
|
||||||
"create_shared_link": "Opret delt link",
|
"create_shared_link": "Opret delt link",
|
||||||
@@ -892,6 +895,7 @@
|
|||||||
"day": "Dag",
|
"day": "Dag",
|
||||||
"days": "Dage",
|
"days": "Dage",
|
||||||
"deduplicate_all": "Dedubliker alle",
|
"deduplicate_all": "Dedubliker alle",
|
||||||
|
"default_locale": "Standard sprog",
|
||||||
"default_locale_description": "Formatér datoer og tal baseret på din browsers landestandard",
|
"default_locale_description": "Formatér datoer og tal baseret på din browsers landestandard",
|
||||||
"delete": "Slet",
|
"delete": "Slet",
|
||||||
"delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt",
|
"delete_action_confirmation_message": "Er du sikker på, at du vil slette dette objekt? Denne handling vil flytte objektet til serverens papirkurv, og vil spørge dig, om du vil slette den lokalt",
|
||||||
@@ -2213,6 +2217,7 @@
|
|||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"tag_assets": "Tag mediefiler",
|
"tag_assets": "Tag mediefiler",
|
||||||
"tag_created": "Oprettet tag: {tag}",
|
"tag_created": "Oprettet tag: {tag}",
|
||||||
|
"tag_face": "Tag ansigt",
|
||||||
"tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner",
|
"tag_feature_description": "Gennemse billeder og videoer grupperet efter logiske tag-emner",
|
||||||
"tag_not_found_question": "Kan du ikke finde et tag? <link>Opret et nyt tag.</link>",
|
"tag_not_found_question": "Kan du ikke finde et tag? <link>Opret et nyt tag.</link>",
|
||||||
"tag_people": "Tag personer",
|
"tag_people": "Tag personer",
|
||||||
|
|||||||
+8
-4
@@ -541,7 +541,7 @@
|
|||||||
"app_settings": "App-Einstellungen",
|
"app_settings": "App-Einstellungen",
|
||||||
"app_stores": "App Stores",
|
"app_stores": "App Stores",
|
||||||
"app_update_available": "App Update verfügbar",
|
"app_update_available": "App Update verfügbar",
|
||||||
"appears_in": "Erscheint in",
|
"appears_in": "Enthalten in",
|
||||||
"apply_count": "Anwenden ({count, number})",
|
"apply_count": "Anwenden ({count, number})",
|
||||||
"archive": "Archiv",
|
"archive": "Archiv",
|
||||||
"archive_action_prompt": "{count} zum Archiv hinzugefügt",
|
"archive_action_prompt": "{count} zum Archiv hinzugefügt",
|
||||||
@@ -812,8 +812,8 @@
|
|||||||
"confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?",
|
"confirm_keep_this_delete_others": "Alle anderen Dateien im Stapel bis auf diese werden gelöscht. Bist du sicher, dass du fortfahren möchten?",
|
||||||
"confirm_new_pin_code": "Neuen PIN-Code bestätigen",
|
"confirm_new_pin_code": "Neuen PIN-Code bestätigen",
|
||||||
"confirm_password": "Passwort bestätigen",
|
"confirm_password": "Passwort bestätigen",
|
||||||
"confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} markieren?",
|
"confirm_tag_face": "Wollen Sie dieses Gesicht mit {name} taggen?",
|
||||||
"confirm_tag_face_unnamed": "Möchten Sie dieses Gesicht markieren?",
|
"confirm_tag_face_unnamed": "Möchten Sie dieses Gesicht taggen?",
|
||||||
"connected_device": "Verbundenes Gerät",
|
"connected_device": "Verbundenes Gerät",
|
||||||
"connected_to": "Verbunden mit",
|
"connected_to": "Verbunden mit",
|
||||||
"contain": "Vollständig",
|
"contain": "Vollständig",
|
||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Link zum Teilen erstellen",
|
"create_link_to_share": "Link zum Teilen erstellen",
|
||||||
"create_link_to_share_description": "Lass jeden mit dem Link die ausgewählten Fotos sehen",
|
"create_link_to_share_description": "Lass jeden mit dem Link die ausgewählten Fotos sehen",
|
||||||
"create_new": "NEUES ERSTELLEN",
|
"create_new": "NEUES ERSTELLEN",
|
||||||
|
"create_new_face": "Neues Gesicht erstellen",
|
||||||
"create_new_person": "Neue Person anlegen",
|
"create_new_person": "Neue Person anlegen",
|
||||||
"create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen",
|
"create_new_person_hint": "Ausgewählte Dateien einer neuen Person zuweisen",
|
||||||
"create_new_user": "Neuen Nutzer erstellen",
|
"create_new_user": "Neuen Nutzer erstellen",
|
||||||
|
"create_person": "Person anlegen",
|
||||||
|
"create_person_subtitle": "Gib dem gewählten Gesicht einen Namen um die neue Person zu erstellen und zu taggen",
|
||||||
"create_shared_album_page_share_add_assets": "INHALTE HINZUFÜGEN",
|
"create_shared_album_page_share_add_assets": "INHALTE HINZUFÜGEN",
|
||||||
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
"create_shared_album_page_share_select_photos": "Fotos auswählen",
|
||||||
"create_shared_link": "Geteilten Link erstellen",
|
"create_shared_link": "Geteilten Link erstellen",
|
||||||
@@ -1035,7 +1038,7 @@
|
|||||||
"error_loading_partners": "Fehler beim Laden der Partner: {error}",
|
"error_loading_partners": "Fehler beim Laden der Partner: {error}",
|
||||||
"error_retrieving_asset_information": "Fehler beim Abruf der Dateiinformationen",
|
"error_retrieving_asset_information": "Fehler beim Abruf der Dateiinformationen",
|
||||||
"error_saving_image": "Fehler: {error}",
|
"error_saving_image": "Fehler: {error}",
|
||||||
"error_tag_face_bounding_box": "Fehler beim Markieren des Gesichts - Begrenzungen können nicht abgerufen werden",
|
"error_tag_face_bounding_box": "Fehler beim Taggen des Gesichts - Begrenzungen können nicht abgerufen werden",
|
||||||
"error_title": "Fehler - Etwas ist schief gelaufen",
|
"error_title": "Fehler - Etwas ist schief gelaufen",
|
||||||
"error_while_navigating": "Fehler beim Navigieren zur Datei",
|
"error_while_navigating": "Fehler beim Navigieren zur Datei",
|
||||||
"errors": {
|
"errors": {
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"tag_assets": "Dateien taggen",
|
"tag_assets": "Dateien taggen",
|
||||||
"tag_created": "Tag erstellt: {tag}",
|
"tag_created": "Tag erstellt: {tag}",
|
||||||
|
"tag_face": "Gesicht taggen",
|
||||||
"tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen",
|
"tag_feature_description": "Durchsuchen von Fotos und Videos, gruppiert nach logischen Tag-Themen",
|
||||||
"tag_not_found_question": "Kein Tag vorhanden? <link>Erstelle einen neuen Tag.</link>",
|
"tag_not_found_question": "Kein Tag vorhanden? <link>Erstelle einen neuen Tag.</link>",
|
||||||
"tag_people": "Personen taggen",
|
"tag_people": "Personen taggen",
|
||||||
|
|||||||
+111
-111
@@ -1,132 +1,132 @@
|
|||||||
{
|
{
|
||||||
"about": "Über",
|
"about": "Über",
|
||||||
"account": "Konto",
|
"account": "Konto",
|
||||||
"account_settings": "Konto Istelligä",
|
"account_settings": "Konto Einstellungen",
|
||||||
"acknowledge": "Bestätige",
|
"acknowledge": "Bestätigä",
|
||||||
"action": "Aktion",
|
"action": "Aktion",
|
||||||
"action_common_update": "Update",
|
"action_common_update": "Update",
|
||||||
"action_description": "Es paar Aktione, wo a de gfilterete Assets usgführt wärde sölled",
|
"action_description": "Aktionä, wo uf de gefilterti Mediä ausgführt werdä solled",
|
||||||
"actions": "Aktione",
|
"actions": "Aktionen",
|
||||||
"active": "Aktiv",
|
"active": "Aktiv",
|
||||||
"active_count": "Aktivi: {count}",
|
"active_count": "Aktiv: {count}",
|
||||||
"activity": "Aktivität",
|
"activity": "Aktivität",
|
||||||
"activity_changed": "Aktivität isch {enabled, select, true {aktiviert} other {deaktiviert}}",
|
"activity_changed": "Aktivität ist {enabled, select, true {aktiviert} other {deaktiviert}}",
|
||||||
"add": "Hinzuefüegä",
|
"add": "Hinzuefüge",
|
||||||
"add_a_description": "Beschriibig hinzuefüege",
|
"add_a_description": "Beschreibung hinzufügen",
|
||||||
"add_a_location": "Standort hinzuefüege",
|
"add_a_location": "Standort hinzuefügä",
|
||||||
"add_a_name": "Name hinzuefüege",
|
"add_a_name": "Namä hinzefügä",
|
||||||
"add_a_title": "Titel hinzuefüege",
|
"add_a_title": "Titel hinzufeügä",
|
||||||
"add_action": "Aktion hinzuefüege",
|
"add_action": "Aktion hinzuefügä",
|
||||||
"add_action_description": "Aklicke um en Aktion dure zfüehre",
|
"add_action_description": "Klick do zum e Aktion hinzuefüge",
|
||||||
"add_assets": "Assets hinzufüege",
|
"add_assets": "Mediä hinzuefüge",
|
||||||
"add_birthday": "Geburtstag hinzuefüege",
|
"add_birthday": "Geburtstag hinzuefüge",
|
||||||
"add_endpoint": "Endpunkt hinzuefüge",
|
"add_endpoint": "Endpunkt hinzuefüge",
|
||||||
"add_exclusion_pattern": "Uuschlussmuster hinzuefüege",
|
"add_exclusion_pattern": "Ausschlussmuster hinzufügen",
|
||||||
"add_filter": "Filter hinzuefüge",
|
"add_filter": "Filter hinzufügen",
|
||||||
"add_filter_description": "Klicke, um e Filterbedingig hinzuezfüege",
|
"add_filter_description": "Klicke hier um eine Filterbedingung hinzuzufügen",
|
||||||
"add_location": "Standort hinzuefüege",
|
"add_location": "Standort hinzufügen",
|
||||||
"add_more_users": "Meh Benutzer hinzuefüege",
|
"add_more_users": "Mehr Benutzer hinzufügen",
|
||||||
"add_partner": "Partner hinzuefüege",
|
"add_partner": "Partner hinzufügen",
|
||||||
"add_path": "Pfad hinzuefüege",
|
"add_path": "Pfad hinzufügen",
|
||||||
"add_photos": "Föteli hinzuefüege",
|
"add_photos": "Fotos hinzufügen",
|
||||||
"add_tag": "Tag hinzuefüege",
|
"add_tag": "Tag hinzufügen",
|
||||||
"add_to": "Hinzuefüege zu …",
|
"add_to": "Hinzufügen zu…",
|
||||||
"add_to_album": "Zum Album hinzuefüege",
|
"add_to_album": "Zu Album hinzufügen",
|
||||||
"add_to_album_bottom_sheet_added": "Zu {album} hinzuegfüegt",
|
"add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt",
|
||||||
"add_to_album_bottom_sheet_already_exists": "Scho in {album}",
|
"add_to_album_bottom_sheet_already_exists": "Bereits in {album}",
|
||||||
"add_to_album_bottom_sheet_some_local_assets": "Es hend es paar lokali Dateie nöd chöne im Album hinzuegfüegt werde",
|
"add_to_album_bottom_sheet_some_local_assets": "Einige lokale Dateien konnten nicht zum Album hinzugefügt werden",
|
||||||
"add_to_album_toggle": "Uuswahl umschalte für {album}",
|
"add_to_album_toggle": "Auswahl umschalten für {album}",
|
||||||
"add_to_albums": "Zu Albe hinzuefüege",
|
"add_to_albums": "Zu Alben hinzufügen",
|
||||||
"add_to_albums_count": "Zu Albe hinzuefüege ({count})",
|
"add_to_albums_count": "Zu Alben hinzufügen ({count})",
|
||||||
"add_to_bottom_bar": "Hinzuefüege zu",
|
"add_to_bottom_bar": "Hinzufügen zu",
|
||||||
"add_to_shared_album": "Zum teilte Album hinzuefüege",
|
"add_to_shared_album": "Zu geteiltem Album hinzufügen",
|
||||||
"add_upload_to_stack": "Upload zum Stack hinzuefüege",
|
"add_upload_to_stack": "Upload zum Stapel hinzufügen",
|
||||||
"add_url": "URL hinzuefüege",
|
"add_url": "URL hinzufügen",
|
||||||
"add_workflow_step": "Workflow-Schritt hinzuefüege",
|
"add_workflow_step": "Workflow-Schritt hinzufügen",
|
||||||
"added_to_archive": "Is Archiv verschobe",
|
"added_to_archive": "Zum Archiv hinzugefügt",
|
||||||
"added_to_favorites": "Zu dine Favoritä hinzuegfüegt",
|
"added_to_favorites": "Zu Favoriten hinzugefügt",
|
||||||
"added_to_favorites_count": "{count, number} zu Favorite hinzuegfüegt",
|
"added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt",
|
||||||
"admin": {
|
"admin": {
|
||||||
"add_exclusion_pattern_description": "Uusschlussmuster hinzuefüge. Platzhalter, wie *, **, und ? wärded understützt. Zum all Dateie i eim Verzeichnis namens „Raw\" ignoriere, „**/Raw/**“ verwände. Zum all Dateien ignorieren, wo uf „.tif“ änded, „**/*.tif“ verwände. Zum en absolute Pfad ignoriere, „/pfad/zum/ignoriere/**“ verwände.",
|
"add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw“ zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.",
|
||||||
"admin_user": "Admin Benutzer",
|
"admin_user": "Administrator",
|
||||||
"asset_offline_description": "Die Datei vonere externe Bibliothek isch nümme uf de Festplatte und isch in Papierchorb verschobe worde. Falls die Datei innerhalb vo de Bibliothek verschoben worde isch, überprüf dini Ziitleiste uf die neui entsprechendi Datei. Zum die Datei wiederherstelle, stell bitte sicher, dass Immich uf de unde stehendi Dateipfad chan zuegriife und scann d'Bibliothek.",
|
"asset_offline_description": "Diese Datei einer externen Bibliothek befindet sich nicht mehr auf der Festplatte und wurde in den Papierkorb verschoben. Falls die Datei innerhalb der Bibliothek verschoben wurde, überprüfe deine Zeitleiste auf die neue entsprechende Datei. Um diese Datei wiederherzustellen, stelle bitte sicher, dass Immich auf den unten stehenden Dateipfad zugreifen kann und scanne die Bibliothek.",
|
||||||
"authentication_settings": "Authentifizierigs Iistellige",
|
"authentication_settings": "Authentifizierungseinstellungen",
|
||||||
"authentication_settings_description": "Passwort, OAuth und anderi Authentifizierigseinstellige verwalte",
|
"authentication_settings_description": "Passwort-, OAuth- und andere Authentifizierungseinstellungen verwalten",
|
||||||
"authentication_settings_disable_all": "Bisch sicher, dass du alli Login-Methodä wotsch deaktivierä? S Login isch denn komplett deaktiviert.",
|
"authentication_settings_disable_all": "Bist du sicher, dass du alle Loginmethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.",
|
||||||
"authentication_settings_reenable": "Bruuch ein <link>Server-Befehl</link> zum reaktiviere.",
|
"authentication_settings_reenable": "Nutze einen <link>Server-Befehl</link> zur Reaktivierung.",
|
||||||
"background_task_job": "Hintergrund Ufgabä",
|
"background_task_job": "Hintergrundaufgaben",
|
||||||
"backup_database": "Datenbank-Dump aalege",
|
"backup_database": "Datenbanksicherung erstellen",
|
||||||
"backup_database_enable_description": "Datenbank-Dumps aktiviere",
|
"backup_database_enable_description": "Datenbank regelmässig sichern",
|
||||||
"backup_keep_last_amount": "Aazahl vo de vorherige Dumps, wo bhalte werde sölle",
|
"backup_keep_last_amount": "Anzahl der aufzubewahrenden früheren Sicherungen",
|
||||||
"backup_onboarding_1_description": "Offsite-Kopie i dä Cloud oder amene andere physische Standort.",
|
"backup_onboarding_1_description": "Offsite-Kopie in der Cloud oder an einem anderen physischen Ort.",
|
||||||
"backup_onboarding_2_description": "Lokali Kopie uf verschiedene Grät. Das beinhaltet d Hauptdateie und e lokali Sicherig vo dene Dateie.",
|
"backup_onboarding_2_description": "lokale Kopien auf verschiedenen Geräten. Dazu gehören die Hauptdateien und eine lokale Sicherung dieser Dateien.",
|
||||||
"backup_onboarding_3_description": "Total aazahl vo dine Dateikopie, inklusiv d Originaldateie. Das beinhaltet 1 Offsite-Kopie und 2 lokali Kopie.",
|
"backup_onboarding_3_description": "Kopien deiner Daten inklusive Originaldateien. Dies umfasst 1 Kopie an einem anderen Ort und 2 lokale Kopien.",
|
||||||
"backup_onboarding_description": "E <backblaze-link>3-2-1-Backup-Strategie</backblaze-link> wird empfohle, zum dini Dateie z schütze. Du söttsch sowohl Kopie vo dine ufgeladene Fotos/Videos wie au d Immich-Datenbank bhalte, für e rundum sauberi Backup-Lösig.",
|
"backup_onboarding_description": "Eine <backblaze-link>3-2-1 Sicherungsstrategie</backblaze-link> wird empfohlen, um deine Daten zu schützen. Du solltest sowohl Kopien deiner hochgeladenen Fotos/Videos als auch der Immich-Datenbank aufbewahren, um eine umfassende Sicherungslösung zu haben.",
|
||||||
"backup_onboarding_footer": "Für meh Infos zum Backup vo Immich lueg bitte i d <link>Dokumentation</link>.",
|
"backup_onboarding_footer": "Weitere Informationen zum Sichern von Immich findest du in der <link>Dokumentation</link>.",
|
||||||
"backup_onboarding_parts_title": "Es 3-2-1-Backup beinhaltet:",
|
"backup_onboarding_parts_title": "Eine 3-2-1-Sicherung umfasst:",
|
||||||
"backup_onboarding_title": "Backups",
|
"backup_onboarding_title": "Backups",
|
||||||
"backup_settings": "Iistellige für Datenbank-Dumps",
|
"backup_settings": "Einstellungen für Datenbanksicherung",
|
||||||
"backup_settings_description": "Datenbank-Dump-Iistellige verwalte.",
|
"backup_settings_description": "Einstellungen zur regelmässigen Sicherung der Datenbank.",
|
||||||
"cleared_jobs": "Jobs glöscht für: {job}",
|
"cleared_jobs": "Folgende Aufgaben zurückgesetzt: {job}",
|
||||||
"config_set_by_file": "D Konfiguration isch aktuell dur e Konfigurationsdatei gsetzt",
|
"config_set_by_file": "Die Konfiguration ist aktuell durch eine Konfigurationsdatei gsetzt",
|
||||||
"confirm_delete_library": "Bisch sicher, dass du d Bibliothek {library} wotsch lösche?",
|
"confirm_delete_library": "Bist du sicher, dass du die Bibliothek {library} löschen willst?",
|
||||||
"confirm_delete_library_assets": "Bisch sicher, dass du die Bibliothek wotsch lösche? Das löscht {count, plural, one {# enthaltenes Asset} other {alli # enthaltene Assets}} us Immich und chan nöd rückgängig gmacht werde. D Dateie bliibed uf em Dateträger.",
|
"confirm_delete_library_assets": "Bist du sicher, dass du diese Bibliothek löschen willst? Dies löscht {count, plural, one {# enthaltene Datei} other {alle # enthaltenen Dateien}} aus Immich und kann nicht rückgängig gemacht werden. Die Dateien bleiben auf der Festplatte erhalten.",
|
||||||
"confirm_email_below": "Zum bestätige bitte \"{email}\" une iitippe",
|
"confirm_email_below": "Zum Bestätigen, tippe unten \"{email}\" ein",
|
||||||
"confirm_reprocess_all_faces": "Bisch sicher, dass du alli Gsichter neu verarbeite wotsch? Däbii werde au benannti Persone glöscht.",
|
"confirm_reprocess_all_faces": "Bist du sicher, dass du alle Gesichter erneut verarbeiten möchtest? Dies löscht auch alle bereits benannten Personen.",
|
||||||
"confirm_user_password_reset": "Bisch sicher, dass du s Passwort für {user} möchtisch zruggsetze?",
|
"confirm_user_password_reset": "Bist du sicher, dass du das Passwort für {user} zurücksetzen möchtest?",
|
||||||
"confirm_user_pin_code_reset": "Bisch sicher, dass du de PIN-Code vo {user} möchtisch zruggsetze?",
|
"confirm_user_pin_code_reset": "Bist du sicher, dass du den PIN-Code von {user} zurücksetzen möchtest?",
|
||||||
"copy_config_to_clipboard_description": "Kopier die aktuelli Systemkonfiguration als JSON-Objekt i d'Zwüschenablage",
|
"copy_config_to_clipboard_description": "Aktuelle Systemkonfiguration als JSON-Objekt in die Zwischenablage kopieren",
|
||||||
"create_job": "Uufgabe erstelle",
|
"create_job": "Aufgabe erstellen",
|
||||||
"cron_expression": "Cron-Ziitagabe",
|
"cron_expression": "Cron-Ausdruck",
|
||||||
"cron_expression_description": "Setz s Scanintervall im Cron-Format. Hilf mit däm Format bütet z. B. der <link>Crontab Guru</link>",
|
"cron_expression_description": "Setze das Scanintervall im Cron-Format. Für mehr Informationen, siehe z. B. <link>Crontab Guru</link>",
|
||||||
"cron_expression_presets": "Vorlage für Cron-Uusdruck",
|
"cron_expression_presets": "Vorlagen für Cron-Ausdrücke",
|
||||||
"disable_login": "Login deaktiviere",
|
"disable_login": "Login deaktivieren",
|
||||||
"duplicate_detection_job_description": "Die Uufgab füehrt s maschinelle Lärne für jedi Datei us, zum Duplikat finde. Die Uufgabe berueht uf de intelligente Suechi",
|
"duplicate_detection_job_description": "Verwendet maschinelles Lernen auf den Dateien, um Duplikate zu finden. Baut auf der intelligenten Suche auf",
|
||||||
"exclusion_pattern_description": "Mit Uusschlussmuster chönnd Dateie und Ordner bim Scanne vo dinere Bibliothek ignoriert wärde. Das isch nützlich, wenn du Ordner häsch, wo Dateien drin händ, wo d nöd wotsch importiere, wie z. B. RAW-Dateie.",
|
"exclusion_pattern_description": "Mit Ausschlussmustern können Dateien und Ordner beim Scannen deiner Bibliothek ignoriert werden. Dies ist nützlich, wenn du Ordner hast, die Dateien enthalten, die du nicht importieren möchtest, wie z. B. RAW-Dateien.",
|
||||||
"export_config_as_json_description": "Lad die aktuelli Systemkonfiguration als JSON-Datei abe",
|
"export_config_as_json_description": "Aktuelle Systemkonfiguration als JSON-Datei herunterladen",
|
||||||
"external_libraries_page_description": "Externi Bibliothekssiite für Administratore",
|
"external_libraries_page_description": "Externe Bibliotheksseite für Administratoren",
|
||||||
"face_detection": "Gsichtserkennig",
|
"face_detection": "Gesichtserkennung",
|
||||||
"face_detection_description": "Die Uufgab erfasst Gsichter in Dateien dur maschinells Lerne. Bi Video wird nur d'Miniaturasicht brucht. „Aktualisiere“ verarbeitet all Dateie neu. „Zruggsetze“ setzt au no all Gsichter zrugg. „Fehlendi“ stellt nur nöd verarbeiteti Dateie in d'Warteschlange. Erfassti Gsichter wärdet zur Gsichtsidentifizierig in diWarteschlange gstellt, damit sie i bestehendi oder neui Persone z'gruppiere.",
|
"face_detection_description": "Diese Aufgabe erkennt mit maschinellem Lernen Gesichter in Dateien. Bei Videos wird nur das Vorschaubild verwendet. „Aktualisieren“ verarbeitet alle Dateien neu. „Zurücksetzen“ setzt zusätzlich alle Gesichter zurück. „Fehlende“ fügt nur nicht verarbeitete Dateien in die Warteschlange ein. Erfasste Gesichter werden zur Gesichtsidentifizierung in die Warteschlange eingefügt, um sie in bestehende oder neue Personen zu gruppieren.",
|
||||||
"facial_recognition_job_description": "Die Uufgabe gruppiert im Anschluss an d'Gsichtserfassig die erfasste Gsichter zu Persone. „Zruggsetze“ gruppiert alli Gsichter neu und mit „Fehlendi“ werdet Gsichter ohni Zuordnig i d'Warteschlange gstellt.",
|
"facial_recognition_job_description": "Diese Aufgabe gruppiert im Anschluss an die Gesichtserkennung die erkannten Gesichter zu Personen. „Zurücksetzen“ gruppiert alle Gesichter neu, während „Fehlende“ Gesichter ohne Zuordnung in die Warteschlange stellt.",
|
||||||
"failed_job_command": "Befehl {command} hät für d'Uufgabe {job} nöd funktioniert",
|
"failed_job_command": "Befehl {command} ist für Aufgabe {job} fehlgeschlagen",
|
||||||
"force_delete_user_warning": "WARNIG: Die Aktion löscht dä Benutzer und all sini Dateie. Das chann nöd rückgängig gmacht wärde und d'Dateie chönnd nöd wiederhergstellt wärde.",
|
"force_delete_user_warning": "WARNUNG: Diese Aktion löscht sofort den Benutzer und all seine Dateien. Dies kann nicht rückgängig gemacht werden und die Dateien können nicht wiederhergestellt werden.",
|
||||||
"image_format": "Format",
|
"image_format": "Format",
|
||||||
"image_format_description": "WebP erzeugt chlineri Dateie we JPEG, isch aber es bitz langsamer i de Erstellig.",
|
"image_format_description": "WebP erzeugt kleinere Dateien als JPEG, ist aber etwas langsamer in der Erstellung.",
|
||||||
"image_fullsize_description": "Hochuflösends Bild mit glöschte Metadate, wo bim Zoome brucht wird",
|
"image_fullsize_description": "Hochauflösendes Bild mit entfernten Metadaten, das beim Zoomen verwendet wird",
|
||||||
"image_fullsize_enabled": "Hochuflösendi Vorschaubilder aktiviere",
|
"image_fullsize_enabled": "Hochauflösende Vorschaubilder aktivieren",
|
||||||
"image_fullsize_enabled_description": "Generiere hochauflösende Vorschaubilder in Originalauflösung für nicht web-kompatibel Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.",
|
"image_fullsize_enabled_description": "Generiere Vorschaubilder in Originalauflösung für nicht web-kompatible Formate. Wenn \"Eingebettete Vorschau bevorzugen\" aktiviert ist, werden eingebettete Vorschaubilder direkt verwendet. Hat keinen Einfluss auf web-kompatible Formate wie JPEG.",
|
||||||
"image_fullsize_quality_description": "Qualität der hochauflösenden Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien.",
|
"image_fullsize_quality_description": "Qualität der hochauflösenden Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien.",
|
||||||
"image_fullsize_title": "Hochauflösende Vorschaueinstellungen",
|
"image_fullsize_title": "Hochauflösende Vorschaueinstellungen",
|
||||||
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
|
"image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen",
|
||||||
"image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.",
|
"image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.",
|
||||||
"image_prefer_wide_gamut": "Breites Spektrum bevorzugen",
|
"image_prefer_wide_gamut": "Breites Spektrum bevorzugen",
|
||||||
"image_prefer_wide_gamut_setting_description": "Bruuch Display P3 für Vorschaubildli. Das erhaltet d'Vitalität von Bildli mit grossem Farbruum besser. Uf alte Grät mit alte Browser chann das aber andersch uusgseh. sRGB-Bildli wärdet als sRGB bhalte zum Farbänderige vermiide.",
|
"image_prefer_wide_gamut_setting_description": "Display P3 (DCI-P3) für Vorschaubilder verwenden. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.",
|
||||||
"image_preview_description": "Mittelgrossi Bildli ohni Metadate, bruuchts für Einzelaasichte und fürs maschinelle Lärne",
|
"image_preview_description": "Mittelgrosses Bild mit entfernten Metadaten, das bei der Betrachtung einer einzelnen Datei und für maschinelles Lernen verwendet wird",
|
||||||
"image_preview_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere. Z tüffi Wert chönnd s maschinelle Lärne beiträchtige.",
|
"image_preview_quality_description": "Vorschauqualität von 1-100. Ein höherer Wert ist besser, erzeugt dadurch aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen. Ein niedriger Wert kann dafür aber die Qualität des maschinellen Lernens beeinträchtigen.",
|
||||||
"image_preview_title": "Vorschauiistellige",
|
"image_preview_title": "Vorschaueinstellungen",
|
||||||
"image_progressive": "Fortlaufend",
|
"image_progressive": "Fortlaufend",
|
||||||
"image_progressive_description": "Codier fortlaufendi JPEG-Bildi: Sie wärdet bim Lade aufbauend aazeiget. Das hät kei Würkig uf WebP-Bildi.",
|
"image_progressive_description": "JPEG-Bilder schrittweise kodieren, um ein stufenweises Laden zu ermöglichen. Dies hat keine Auswirkungen auf WebP-Bilder.",
|
||||||
"image_quality": "Qualität",
|
"image_quality": "Qualität",
|
||||||
"image_resolution": "Uuflösig",
|
"image_resolution": "Auflösung",
|
||||||
"image_resolution_description": "Höcheri Uuflösig erhaltet meh Detail, gaht aber länger zum codiere, macht grösseri Dateie und chan d'App Schuppdizität reduziere.",
|
"image_resolution_description": "Höhere Auflösungen können mehr Details erhalten, benötigen aber mehr Zeit für die Kodierung, haben grössere Dateigrössen und können die Reaktionsfähigkeit der App beeinträchtigen.",
|
||||||
"image_settings": "Bild-Iistellige",
|
"image_settings": "Bildeinstellungen",
|
||||||
"image_settings_description": "Qualität und Uuflösig von erstellte Bildli verwalte",
|
"image_settings_description": "Qualität und Auflösung der generierten Bilder verwalten",
|
||||||
"image_thumbnail_description": "Chlini Vorschaubildli ohni Metadate, bruuchts für Aasichte mit Gruppe vo Föteli wie i de Hauptziitachse",
|
"image_thumbnail_description": "Kleines Vorschaubild mit entfernten Metadaten, die bei der Anzeige von Sammlungen von Fotos wie der Zeitleiste verwendet wird",
|
||||||
"image_thumbnail_quality_description": "Vorschauqualität vo 1-100. Höcher isch besser, git aber grösseri Dateie und chan d'App Schwuppdizität reduziere.",
|
"image_thumbnail_quality_description": "Qualität der Vorschaubilder von 1-100. Höher ist besser, erzeugt aber grössere Dateien und kann die Reaktionsfähigkeit der App beeinträchtigen.",
|
||||||
"image_thumbnail_title": "Iistellige für Vorschaubildli",
|
"image_thumbnail_title": "Einstellungen für Vorschaubilder",
|
||||||
"import_config_from_json_description": "Systemkonfiguration importiere durs Ufelade vonere JSON-Datei",
|
"import_config_from_json_description": "Systemkonfiguration von hochgeladener JSON-Konfigurationsdatei importieren",
|
||||||
"job_concurrency": "{job} Näbeläufigkeit",
|
"job_concurrency": "{job} (Anzahl gleichzeitig laufende Prozesse)",
|
||||||
"job_created": "Uufgab erstellt",
|
"job_created": "Aufgabe erstellt",
|
||||||
"job_not_concurrency_safe": "Die Uufgabe ist nöd für Paralleluusführig gmacht.",
|
"job_not_concurrency_safe": "Diese Aufgabe kann nicht mehrmals parallel laufen gelassen werden.",
|
||||||
"job_settings": "Uufgabe-Iistellige",
|
"job_settings": "Aufgabeneinstellungen",
|
||||||
"job_settings_description": "Uufgabe-Näbeläufigkeit verwalte",
|
"job_settings_description": "Gleichzeitige Ausführung von Aufgaben verwalten",
|
||||||
"jobs_over_time": "Uufgabe in ziitliche Verlauf",
|
"jobs_over_time": "Jobs im Laufe der Zeit",
|
||||||
"library_created": "Bibliothek erstellt: {library}",
|
"library_created": "Bibliothek erstellt: {library}",
|
||||||
"library_deleted": "Bibliothek glöscht",
|
"library_deleted": "Bibliothek gelöscht",
|
||||||
"library_details": "Bibliotheks-Details",
|
"library_details": "Bibliotheksdetails",
|
||||||
"library_folder_description": "Gib en Order zum Importiere a. Dä Order mit sine Underordner wird nach Bildli und Videos durchsucht.",
|
"library_folder_description": "Wähle einen Ordner zum Importieren. Dieser Ordner wird inklusive Unterordnern nach Bildern und Videos durchsucht.",
|
||||||
"library_remove_exclusion_pattern_prompt": "Bisch sicher, dass das Uuschluss-Muster wotsch lösche?",
|
"library_remove_exclusion_pattern_prompt": "Bilst du sicher, dass du dieses Ausschlussmuster entfernen möchtest?",
|
||||||
"library_remove_folder_prompt": "Bisch sicher, dass dä Import-Ordner wotsch lösche?",
|
"library_remove_folder_prompt": "Bist du sicher, dass du diesen Import-Ordner entfernen möchtest?",
|
||||||
"library_scanning": "Regelmässigi Überprüefig"
|
"library_scanning": "Regelmässiges Scannen"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Δημιουργία συνδέσμου για διαμοιρασμό",
|
"create_link_to_share": "Δημιουργία συνδέσμου για διαμοιρασμό",
|
||||||
"create_link_to_share_description": "Επιτρέψτε σε οποιονδήποτε έχει τον σύνδεσμο να δει τη/τις επιλεγμένη/ες φωτογραφία/ες",
|
"create_link_to_share_description": "Επιτρέψτε σε οποιονδήποτε έχει τον σύνδεσμο να δει τη/τις επιλεγμένη/ες φωτογραφία/ες",
|
||||||
"create_new": "ΔΗΜΙΟΥΡΓΙΑ ΝΕΟΥ",
|
"create_new": "ΔΗΜΙΟΥΡΓΙΑ ΝΕΟΥ",
|
||||||
"create_new_person": "Δημιουργία νέου προσώπου",
|
"create_new_face": "Δημιουργία νέου προσώπου",
|
||||||
|
"create_new_person": "Δημιουργία νέου ατόμου",
|
||||||
"create_new_person_hint": "Αντιστοίχιση των επιλεγμένων αρχείων σε ένα νέο πρόσωπο",
|
"create_new_person_hint": "Αντιστοίχιση των επιλεγμένων αρχείων σε ένα νέο πρόσωπο",
|
||||||
"create_new_user": "Δημιουργία νέου χρήστη",
|
"create_new_user": "Δημιουργία νέου χρήστη",
|
||||||
|
"create_person": "Δημιουργία ατόμου",
|
||||||
|
"create_person_subtitle": "Προσθέστε ένα όνομα στο επιλεγμένο πρόσωπο για να δημιουργηθεί και να επισημανθεί το νέο άτομο",
|
||||||
"create_shared_album_page_share_add_assets": "ΠΡΟΣΘΗΚΗ ΣΤΟΙΧΕΙΩΝ",
|
"create_shared_album_page_share_add_assets": "ΠΡΟΣΘΗΚΗ ΣΤΟΙΧΕΙΩΝ",
|
||||||
"create_shared_album_page_share_select_photos": "Επιλέξτε Φωτογραφίες",
|
"create_shared_album_page_share_select_photos": "Επιλέξτε Φωτογραφίες",
|
||||||
"create_shared_link": "Δημιουργία κοινόχρηστου συνδέσμου",
|
"create_shared_link": "Δημιουργία κοινόχρηστου συνδέσμου",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Ετικέτα",
|
"tag": "Ετικέτα",
|
||||||
"tag_assets": "Ετικετοποίηση στοιχείων",
|
"tag_assets": "Ετικετοποίηση στοιχείων",
|
||||||
"tag_created": "Δημιουργήθηκε ετικέτα: {tag}",
|
"tag_created": "Δημιουργήθηκε ετικέτα: {tag}",
|
||||||
|
"tag_face": "Επισήμανση προσώπου",
|
||||||
"tag_feature_description": "Περιήγηση σε φωτογραφίες και βίντεο που είναι οργανωμένα σύμφωνα με λογικά θέματα ετικετών",
|
"tag_feature_description": "Περιήγηση σε φωτογραφίες και βίντεο που είναι οργανωμένα σύμφωνα με λογικά θέματα ετικετών",
|
||||||
"tag_not_found_question": "Δεν μπορείτε να βρείτε μια ετικέτα; <link>Δημιουργήστε μια νέα ετικέτα.</link>",
|
"tag_not_found_question": "Δεν μπορείτε να βρείτε μια ετικέτα; <link>Δημιουργήστε μια νέα ετικέτα.</link>",
|
||||||
"tag_people": "Επισήμανση ατόμων",
|
"tag_people": "Επισήμανση ατόμων",
|
||||||
|
|||||||
@@ -267,6 +267,8 @@
|
|||||||
"notification_enable_email_notifications": "Enable email notifications",
|
"notification_enable_email_notifications": "Enable email notifications",
|
||||||
"notification_settings": "Notification Settings",
|
"notification_settings": "Notification Settings",
|
||||||
"notification_settings_description": "Manage notification settings, including email",
|
"notification_settings_description": "Manage notification settings, including email",
|
||||||
|
"oauth_allow_insecure_requests": "Allow insecure requests",
|
||||||
|
"oauth_allow_insecure_requests_description": "WARNING: This disables TLS certificate validation for OAuth requests and may expose you to MITM attacks.",
|
||||||
"oauth_auto_launch": "Auto launch",
|
"oauth_auto_launch": "Auto launch",
|
||||||
"oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page",
|
"oauth_auto_launch_description": "Start the OAuth login flow automatically upon navigating to the login page",
|
||||||
"oauth_auto_register": "Auto register",
|
"oauth_auto_register": "Auto register",
|
||||||
@@ -274,9 +276,11 @@
|
|||||||
"oauth_button_text": "Button text",
|
"oauth_button_text": "Button text",
|
||||||
"oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.",
|
"oauth_client_secret_description": "Required for confidential client, or if PKCE (Proof Key for Code Exchange) is not supported for public client.",
|
||||||
"oauth_enable_description": "Login with OAuth",
|
"oauth_enable_description": "Login with OAuth",
|
||||||
|
"oauth_end_session_url_description": "Redirect the user to this URI when they log out.",
|
||||||
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
"oauth_mobile_redirect_uri": "Mobile redirect URI",
|
||||||
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
"oauth_mobile_redirect_uri_override": "Mobile redirect URI override",
|
||||||
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
"oauth_mobile_redirect_uri_override_description": "Enable when OAuth provider does not allow a mobile URI, like ''{callback}''",
|
||||||
|
"oauth_prompt_description": "Prompt parameter (e.g. select_account, login, consent)",
|
||||||
"oauth_role_claim": "Role Claim",
|
"oauth_role_claim": "Role Claim",
|
||||||
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
"oauth_role_claim_description": "Automatically grant admin access based on the presence of this claim. The claim may have either 'user' or 'admin'.",
|
||||||
"oauth_settings": "OAuth",
|
"oauth_settings": "OAuth",
|
||||||
@@ -1236,6 +1240,7 @@
|
|||||||
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
|
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
|
||||||
"free_up_space_settings_subtitle": "Free up device storage",
|
"free_up_space_settings_subtitle": "Free up device storage",
|
||||||
"full_path": "Full path: {path}",
|
"full_path": "Full path: {path}",
|
||||||
|
"full_path_or_folder": "Full path or folder",
|
||||||
"gcast_enabled": "Google Cast",
|
"gcast_enabled": "Google Cast",
|
||||||
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
|
||||||
"general": "General",
|
"general": "General",
|
||||||
@@ -1392,6 +1397,7 @@
|
|||||||
"light_theme": "Switch to light theme",
|
"light_theme": "Switch to light theme",
|
||||||
"like": "Like",
|
"like": "Like",
|
||||||
"like_deleted": "Like deleted",
|
"like_deleted": "Like deleted",
|
||||||
|
"link": "Link",
|
||||||
"link_motion_video": "Link motion video",
|
"link_motion_video": "Link motion video",
|
||||||
"link_to_docs": "For more information, refer to the <link>documentation</link>.",
|
"link_to_docs": "For more information, refer to the <link>documentation</link>.",
|
||||||
"link_to_oauth": "Link to OAuth",
|
"link_to_oauth": "Link to OAuth",
|
||||||
@@ -1518,6 +1524,38 @@
|
|||||||
"marked_all_as_read": "Marked all as read",
|
"marked_all_as_read": "Marked all as read",
|
||||||
"matches": "Matches",
|
"matches": "Matches",
|
||||||
"matching_assets": "Matching Assets",
|
"matching_assets": "Matching Assets",
|
||||||
|
"media_chrome": {
|
||||||
|
"auto": "Auto",
|
||||||
|
"captions": "Captions",
|
||||||
|
"captions_off": "Off",
|
||||||
|
"closed_captions": "closed captions",
|
||||||
|
"decode_error": "Decode error",
|
||||||
|
"disable_captions": "Disable captions",
|
||||||
|
"enable_captions": "Enable captions",
|
||||||
|
"enter_fullscreen_mode": "Enter fullscreen mode",
|
||||||
|
"exit_fullscreen_mode": "Exit fullscreen mode",
|
||||||
|
"loop": "Loop",
|
||||||
|
"media_error_description": "A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.",
|
||||||
|
"media_loading": "media loading",
|
||||||
|
"mute": "Mute",
|
||||||
|
"network_error": "Network error",
|
||||||
|
"network_error_description": "A network error caused the media download to fail.",
|
||||||
|
"not_supported_error": "Source Not Supported",
|
||||||
|
"playback_rate": "Playback rate",
|
||||||
|
"playback_rate_current": "current playback rate",
|
||||||
|
"playback_rate_value": "Playback rate {playbackRate}",
|
||||||
|
"playback_time": "playback time",
|
||||||
|
"quality": "Quality",
|
||||||
|
"second": "second",
|
||||||
|
"seconds": "seconds",
|
||||||
|
"time_value_of_total_time": "{currentTime} of {totalTime}",
|
||||||
|
"time_value_remaining": "{time} remaining",
|
||||||
|
"unmute": "Unmute",
|
||||||
|
"unsupported_error_description": "An unsupported error occurred. The server or network failed, or your browser does not support this format.",
|
||||||
|
"video_not_loaded_unknown_time": "video not loaded, unknown time.",
|
||||||
|
"video_player": "video player",
|
||||||
|
"volume": "volume"
|
||||||
|
},
|
||||||
"media_type": "Media type",
|
"media_type": "Media type",
|
||||||
"memories": "Memories",
|
"memories": "Memories",
|
||||||
"memories_all_caught_up": "All caught up",
|
"memories_all_caught_up": "All caught up",
|
||||||
@@ -1562,6 +1600,8 @@
|
|||||||
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
"multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping",
|
||||||
"mute_memories": "Mute Memories",
|
"mute_memories": "Mute Memories",
|
||||||
"my_albums": "My albums",
|
"my_albums": "My albums",
|
||||||
|
"my_immich_description": "Copy current page as a My Immich link",
|
||||||
|
"my_immich_title": "My Immich link",
|
||||||
"name": "Name",
|
"name": "Name",
|
||||||
"name_or_nickname": "Name or nickname",
|
"name_or_nickname": "Name or nickname",
|
||||||
"name_required": "Name is required",
|
"name_required": "Name is required",
|
||||||
@@ -1926,6 +1966,8 @@
|
|||||||
"scan_settings": "Scan Settings",
|
"scan_settings": "Scan Settings",
|
||||||
"scanning": "Scanning",
|
"scanning": "Scanning",
|
||||||
"scanning_for_album": "Scanning for album...",
|
"scanning_for_album": "Scanning for album...",
|
||||||
|
"screencast_mode_description": "Show keyboard and mouse event indicators on the screen",
|
||||||
|
"screencast_mode_title": "Toggle screencast mode",
|
||||||
"search": "Search",
|
"search": "Search",
|
||||||
"search_albums": "Search albums",
|
"search_albums": "Search albums",
|
||||||
"search_by_context": "Search by context",
|
"search_by_context": "Search by context",
|
||||||
@@ -1933,6 +1975,8 @@
|
|||||||
"search_by_description_example": "Hiking day in Sapa",
|
"search_by_description_example": "Hiking day in Sapa",
|
||||||
"search_by_filename": "Search by file name or extension",
|
"search_by_filename": "Search by file name or extension",
|
||||||
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
|
||||||
|
"search_by_full_path": "Search by full path or folder",
|
||||||
|
"search_by_full_path_example": "/John/Projects/3D_Printing/2026-07-01 - you can search for Projects, 3D, Printing, 2026 etc.",
|
||||||
"search_by_ocr": "Search by OCR",
|
"search_by_ocr": "Search by OCR",
|
||||||
"search_by_ocr_example": "Latte",
|
"search_by_ocr_example": "Latte",
|
||||||
"search_camera_lens_model": "Search lens model...",
|
"search_camera_lens_model": "Search lens model...",
|
||||||
@@ -2214,6 +2258,8 @@
|
|||||||
"sync_status": "Sync Status",
|
"sync_status": "Sync Status",
|
||||||
"sync_status_subtitle": "View and manage the sync system",
|
"sync_status_subtitle": "View and manage the sync system",
|
||||||
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
|
"system_theme": "System theme",
|
||||||
|
"system_theme_command_description": "Use the system theme ({value})",
|
||||||
"tag": "Tag",
|
"tag": "Tag",
|
||||||
"tag_assets": "Tag assets",
|
"tag_assets": "Tag assets",
|
||||||
"tag_created": "Created tag: {tag}",
|
"tag_created": "Created tag: {tag}",
|
||||||
@@ -2425,6 +2471,7 @@
|
|||||||
"workflows": "Workflows",
|
"workflows": "Workflows",
|
||||||
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
|
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
|
||||||
"wrong_pin_code": "Wrong PIN code",
|
"wrong_pin_code": "Wrong PIN code",
|
||||||
|
"x_of_total": "{x}/{total}",
|
||||||
"year": "Year",
|
"year": "Year",
|
||||||
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
"years_ago": "{years, plural, one {# year} other {# years}} ago",
|
||||||
"yes": "Yes",
|
"yes": "Yes",
|
||||||
|
|||||||
+32
-1
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Krei ligilon por dividi",
|
"create_link_to_share": "Krei ligilon por dividi",
|
||||||
"create_link_to_share_description": "Permesi, ke iu ajn kun la ligilo povu vidi la elektita(j)n foto(j)n",
|
"create_link_to_share_description": "Permesi, ke iu ajn kun la ligilo povu vidi la elektita(j)n foto(j)n",
|
||||||
"create_new": "KREI NOVAN",
|
"create_new": "KREI NOVAN",
|
||||||
|
"create_new_face": "Krei novan vizaĝon",
|
||||||
"create_new_person": "Krei novan homon",
|
"create_new_person": "Krei novan homon",
|
||||||
"create_new_person_hint": "Atribui elektitajn elementojn al nova homo",
|
"create_new_person_hint": "Atribui elektitajn elementojn al nova homo",
|
||||||
"create_new_user": "Krei novan uzanton",
|
"create_new_user": "Krei novan uzanton",
|
||||||
|
"create_person": "Krei homon",
|
||||||
|
"create_person_subtitle": "Aldoni nomon al la elektita vizaĝo por krei kaj etikedi novan homon",
|
||||||
"create_shared_album_page_share_add_assets": "ALDONI ELEMENTOJN",
|
"create_shared_album_page_share_add_assets": "ALDONI ELEMENTOJN",
|
||||||
"create_shared_album_page_share_select_photos": "Elekti fotojn",
|
"create_shared_album_page_share_select_photos": "Elekti fotojn",
|
||||||
"create_shared_link": "Krei dividitan ligilon",
|
"create_shared_link": "Krei dividitan ligilon",
|
||||||
@@ -1043,7 +1046,7 @@
|
|||||||
"cannot_navigate_previous_asset": "Ne eblas navigi al antaŭa elemento",
|
"cannot_navigate_previous_asset": "Ne eblas navigi al antaŭa elemento",
|
||||||
"cant_apply_changes": "Ne eblas apliki ŝanĝojn",
|
"cant_apply_changes": "Ne eblas apliki ŝanĝojn",
|
||||||
"cant_change_activity": "Ne eblas {enabled, select, true {malŝalti} other {ŝalti}} tiun agon",
|
"cant_change_activity": "Ne eblas {enabled, select, true {malŝalti} other {ŝalti}} tiun agon",
|
||||||
"cant_change_asset_favorite": "Ne eblas ŝanĝi preferaton por tiu elemento",
|
"cant_change_asset_favorite": "Ne eblas ŝanĝi preferon por tiu elemento",
|
||||||
"cant_change_metadata_assets_count": "Ne eblas ŝanĝi metadatumojn de {count, plural, one {# elemento} other {# elementoj}}",
|
"cant_change_metadata_assets_count": "Ne eblas ŝanĝi metadatumojn de {count, plural, one {# elemento} other {# elementoj}}",
|
||||||
"cant_get_faces": "Ne eblas trovi vizaĝojn",
|
"cant_get_faces": "Ne eblas trovi vizaĝojn",
|
||||||
"cant_get_number_of_comments": "Ne eblas trovi nombron da komentoj",
|
"cant_get_number_of_comments": "Ne eblas trovi nombron da komentoj",
|
||||||
@@ -1074,7 +1077,18 @@
|
|||||||
"incorrect_email_or_password": "Neĝusta retadreso aŭ pasvorto",
|
"incorrect_email_or_password": "Neĝusta retadreso aŭ pasvorto",
|
||||||
"library_folder_already_exists": "Tiu ĉi import-vojo jam ekzistas.",
|
"library_folder_already_exists": "Tiu ĉi import-vojo jam ekzistas.",
|
||||||
"page_not_found": "Paĝo ne trovita",
|
"page_not_found": "Paĝo ne trovita",
|
||||||
|
"paths_validation_failed": "Evidentiĝis, ke {paths, plural, one {# vojo estas nevalida} other {# vojoj estas nevalidaj}}",
|
||||||
|
"profile_picture_transparent_pixels": "Ne eblas havi travideblaj bilderoj en profilbildo. Bonvolu zomi kaj/aŭ ŝovi la bildon al loko sen tiaj bilderoj.",
|
||||||
|
"quota_higher_than_disk_size": "Vi donis kvoton pli grandan ol la disko mem",
|
||||||
|
"something_went_wrong": "Io misis",
|
||||||
|
"unable_to_add_album_users": "Ne eblas aldoni uzantojn al la albumo",
|
||||||
|
"unable_to_add_assets_to_shared_link": "Ne eblas aldoni elementojn al la dividita ligilo",
|
||||||
|
"unable_to_add_comment": "Ne eblas aldoni komenton",
|
||||||
"unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo",
|
"unable_to_add_exclusion_pattern": "Ne eblas aldoni skemon de ekskludo",
|
||||||
|
"unable_to_add_partners": "Ne eblas aldoni partnerojn",
|
||||||
|
"unable_to_add_remove_archive": "Ne eblas {archived, select, true {forigi elementon de} other {aldoni elementon al}} la arĥivo",
|
||||||
|
"unable_to_add_remove_favorites": "Ne eblas {favorite, select, true {aldoni elementon al} other {forigi elementon de}} preferataĵoj",
|
||||||
|
"unable_to_change_favorite": "Ne eblas ŝanĝi preferon por tiu elemento",
|
||||||
"unable_to_create": "Ne eblis krei laborfluon",
|
"unable_to_create": "Ne eblis krei laborfluon",
|
||||||
"unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo",
|
"unable_to_delete_exclusion_pattern": "Ne eblas forigi skemon de ekskludo",
|
||||||
"unable_to_delete_workflow": "Ne eblis forigi laborfluon",
|
"unable_to_delete_workflow": "Ne eblis forigi laborfluon",
|
||||||
@@ -1088,15 +1102,25 @@
|
|||||||
"expand_all": "Etendi ĉiujn",
|
"expand_all": "Etendi ĉiujn",
|
||||||
"explore": "Esplori",
|
"explore": "Esplori",
|
||||||
"explorer": "Foliumilo",
|
"explorer": "Foliumilo",
|
||||||
|
"favorite": "Preferataĵo",
|
||||||
|
"favorite_action_prompt": "{count} aldonita(j) al Preferataĵoj",
|
||||||
|
"favorite_or_unfavorite_photo": "Aldoni/forigi foton al/de preferataĵoj",
|
||||||
|
"favorites": "Preferataĵoj",
|
||||||
|
"favorites_page_no_favorites": "Neniuj preferataj elementoj trovitaj",
|
||||||
"free_up_space": "Liberigi spacon",
|
"free_up_space": "Liberigi spacon",
|
||||||
"free_up_space_description": "Vi forigos fotojn kaj/aŭ videojn, kiuj havas savkopiojn en la servilo, por liberigi spacon en via aparato. La kopioj en la servilo restos.",
|
"free_up_space_description": "Vi forigos fotojn kaj/aŭ videojn, kiuj havas savkopiojn en la servilo, por liberigi spacon en via aparato. La kopioj en la servilo restos.",
|
||||||
"general": "Ĝeneralaj",
|
"general": "Ĝeneralaj",
|
||||||
|
"home_page_favorite_err_local": "Ankoraŭ ne eblas aldoni lokajn elementojn al Preferataĵoj; ignorita(j)",
|
||||||
|
"home_page_favorite_err_partner": "Ankoraŭ ne eblas aldoni elementojn de partnero al Preferataĵoj; ignorita(j)",
|
||||||
|
"keep_favorites": "Konservi preferataĵojn",
|
||||||
"manage_media_access_settings": "Malfermi agordaĵaron",
|
"manage_media_access_settings": "Malfermi agordaĵaron",
|
||||||
"manage_the_app_settings": "Agordi la apon",
|
"manage_the_app_settings": "Agordi la apon",
|
||||||
|
"map_settings_only_show_favorites": "Montri nur preferataĵojn",
|
||||||
"missing": "Netraktitaj",
|
"missing": "Netraktitaj",
|
||||||
"networking_subtitle": "Administri agordojn pri finpunktoj de la servilo",
|
"networking_subtitle": "Administri agordojn pri finpunktoj de la servilo",
|
||||||
"no_devices": "Neniuj aprobitaj aparatoj",
|
"no_devices": "Neniuj aprobitaj aparatoj",
|
||||||
"no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.",
|
"no_explore_results_message": "Alŝutu pli da fotoj por esplori vian kolekton.",
|
||||||
|
"no_favorites_message": "Aldoni al Preferataĵoj por rapide retrovi viajn plej bonajn bildojn kaj videojn",
|
||||||
"no_notifications": "Neniuj sciigoj",
|
"no_notifications": "Neniuj sciigoj",
|
||||||
"no_results_description": "Provu sinonimon aŭ pli ĝeneralan ŝlosilvorton",
|
"no_results_description": "Provu sinonimon aŭ pli ĝeneralan ŝlosilvorton",
|
||||||
"notification_permission_dialog_content": "Por ŝalti sciigojn, iru al Agordoj kaj elektu 'permesi'.",
|
"notification_permission_dialog_content": "Por ŝalti sciigojn, iru al Agordoj kaj elektu 'permesi'.",
|
||||||
@@ -1106,10 +1130,14 @@
|
|||||||
"notification_toggle_setting_description": "Ŝalti sciigojn per retmesaĝo",
|
"notification_toggle_setting_description": "Ŝalti sciigojn per retmesaĝo",
|
||||||
"notifications": "Sciigoj",
|
"notifications": "Sciigoj",
|
||||||
"notifications_setting_description": "Administri sciigojn",
|
"notifications_setting_description": "Administri sciigojn",
|
||||||
|
"only_favorites": "Nur preferataĵoj",
|
||||||
"preferences_settings_subtitle": "Administri agordojn pri la apo",
|
"preferences_settings_subtitle": "Administri agordojn pri la apo",
|
||||||
"purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aŭtentikeco por la servilo",
|
"purchase_settings_server_activated": "La administranto respondecas pri la ŝlosilo de aŭtentikeco por la servilo",
|
||||||
"rating_clear": "Forviŝi pritakson",
|
"rating_clear": "Forviŝi pritakson",
|
||||||
"refresh": "Denove",
|
"refresh": "Denove",
|
||||||
|
"remove_from_favorites": "Forigi el preferataĵoj",
|
||||||
|
"removed_from_favorites": "Forigita(j) el preferataĵoj",
|
||||||
|
"removed_from_favorites_count": "{count, plural, other {Forigis #}} el Preferataĵoj",
|
||||||
"rescan": "Reanalizi",
|
"rescan": "Reanalizi",
|
||||||
"reset": "Restartigi",
|
"reset": "Restartigi",
|
||||||
"reset_sqlite_clear_app_data": "Forviŝi datumojn",
|
"reset_sqlite_clear_app_data": "Forviŝi datumojn",
|
||||||
@@ -1127,7 +1155,10 @@
|
|||||||
"setting_notifications_subtitle": "Redakti viajn preferojn pri sciigoj",
|
"setting_notifications_subtitle": "Redakti viajn preferojn pri sciigoj",
|
||||||
"start_date": "Komenca dato",
|
"start_date": "Komenca dato",
|
||||||
"start_date_before_end_date": "Komenca dato devas esti antaŭ fina dato",
|
"start_date_before_end_date": "Komenca dato devas esti antaŭ fina dato",
|
||||||
|
"to_favorite": "Aldoni al preferataĵoj",
|
||||||
"trigger_description": "Evento, kiu ekfunkciigas la laborfluon",
|
"trigger_description": "Evento, kiu ekfunkciigas la laborfluon",
|
||||||
|
"unfavorite": "Forigi el preferataĵoj",
|
||||||
|
"unfavorite_action_prompt": "{count} forigita(j) el Preferataĵoj",
|
||||||
"untitled_workflow": "Sentitola laborfluo",
|
"untitled_workflow": "Sentitola laborfluo",
|
||||||
"upload_concurrency": "Nombro da samtempaj alŝutoj",
|
"upload_concurrency": "Nombro da samtempaj alŝutoj",
|
||||||
"user_pin_code_settings_description": "Administri vian PIN-kodon",
|
"user_pin_code_settings_description": "Administri vian PIN-kodon",
|
||||||
|
|||||||
+6
-2
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Crear enlace compartido",
|
"create_link_to_share": "Crear enlace compartido",
|
||||||
"create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)",
|
"create_link_to_share_description": "Permitir que cualquier persona con el enlace vea la(s) foto(s) seleccionada(s)",
|
||||||
"create_new": "CREAR NUEVO",
|
"create_new": "CREAR NUEVO",
|
||||||
|
"create_new_face": "Crear nueva cara",
|
||||||
"create_new_person": "Crear nueva persona",
|
"create_new_person": "Crear nueva persona",
|
||||||
"create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona",
|
"create_new_person_hint": "Asignar los recursos seleccionados a una nueva persona",
|
||||||
"create_new_user": "Crear nuevo usuario",
|
"create_new_user": "Crear nuevo usuario",
|
||||||
|
"create_person": "Crear persona",
|
||||||
|
"create_person_subtitle": "Añade un nombre a la cara seleccionada para crear y etiquetar a la nueva persona",
|
||||||
"create_shared_album_page_share_add_assets": "AÑADIR RECURSOS",
|
"create_shared_album_page_share_add_assets": "AÑADIR RECURSOS",
|
||||||
"create_shared_album_page_share_select_photos": "Seleccionar fotos",
|
"create_shared_album_page_share_select_photos": "Seleccionar fotos",
|
||||||
"create_shared_link": "Crear un enlace compartido",
|
"create_shared_link": "Crear un enlace compartido",
|
||||||
@@ -1022,7 +1025,7 @@
|
|||||||
"enable_biometric_auth_description": "Introduce tu código PIN para habilitar la autentificación biométrica",
|
"enable_biometric_auth_description": "Introduce tu código PIN para habilitar la autentificación biométrica",
|
||||||
"enabled": "Habilitado",
|
"enabled": "Habilitado",
|
||||||
"end_date": "Fecha final",
|
"end_date": "Fecha final",
|
||||||
"enqueued": "Agregado a la cola",
|
"enqueued": "Añadido a la cola",
|
||||||
"enter_wifi_name": "Introduce el nombre Wi-Fi",
|
"enter_wifi_name": "Introduce el nombre Wi-Fi",
|
||||||
"enter_your_pin_code": "Introduce tu código PIN",
|
"enter_your_pin_code": "Introduce tu código PIN",
|
||||||
"enter_your_pin_code_subtitle": "Introduce tu código PIN para acceder a la carpeta protegida",
|
"enter_your_pin_code_subtitle": "Introduce tu código PIN para acceder a la carpeta protegida",
|
||||||
@@ -1085,7 +1088,7 @@
|
|||||||
"unable_to_add_partners": "No se pueden añadir miembros",
|
"unable_to_add_partners": "No se pueden añadir miembros",
|
||||||
"unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {añadir el recurso al}} archivo",
|
"unable_to_add_remove_archive": "No se pudo {archived, select, true {eliminar el recurso del} other {añadir el recurso al}} archivo",
|
||||||
"unable_to_add_remove_favorites": "No se pudo {favorite, select, true {añadir el recuso a} other {eliminar el recurso de}} los favoritos",
|
"unable_to_add_remove_favorites": "No se pudo {favorite, select, true {añadir el recuso a} other {eliminar el recurso de}} los favoritos",
|
||||||
"unable_to_archive_unarchive": "No se pudo {archived, select, true {agregar el elemento al} other {quitar el elemento del}} archivo",
|
"unable_to_archive_unarchive": "No se pudo {archived, select, true {añadir el elemento al} other {quitar el elemento del}} archivo",
|
||||||
"unable_to_change_album_user_role": "No se puede cambiar la función del usuario del álbum",
|
"unable_to_change_album_user_role": "No se puede cambiar la función del usuario del álbum",
|
||||||
"unable_to_change_date": "No se puede cambiar la fecha",
|
"unable_to_change_date": "No se puede cambiar la fecha",
|
||||||
"unable_to_change_description": "Imposible cambiar la descripción",
|
"unable_to_change_description": "Imposible cambiar la descripción",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"tag_assets": "Etiquetar recursos",
|
"tag_assets": "Etiquetar recursos",
|
||||||
"tag_created": "Etiqueta creada: {tag}",
|
"tag_created": "Etiqueta creada: {tag}",
|
||||||
|
"tag_face": "Etiquetar cara",
|
||||||
"tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas",
|
"tag_feature_description": "Explore fotos y videos agrupados por temas de etiquetas lógicas",
|
||||||
"tag_not_found_question": "¿No encuentra una etiqueta? <link>Crea una nueva etiqueta.</link>",
|
"tag_not_found_question": "¿No encuentra una etiqueta? <link>Crea una nueva etiqueta.</link>",
|
||||||
"tag_people": "Etiquetar personas",
|
"tag_people": "Etiquetar personas",
|
||||||
|
|||||||
+14
-2
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Luo linkki jaettavaksi",
|
"create_link_to_share": "Luo linkki jaettavaksi",
|
||||||
"create_link_to_share_description": "Salli kaikkien linkin saaneiden nähdä valitut kuvat",
|
"create_link_to_share_description": "Salli kaikkien linkin saaneiden nähdä valitut kuvat",
|
||||||
"create_new": "LUO UUSI",
|
"create_new": "LUO UUSI",
|
||||||
|
"create_new_face": "Luo uudet kasvot",
|
||||||
"create_new_person": "Luo uusi henkilö",
|
"create_new_person": "Luo uusi henkilö",
|
||||||
"create_new_person_hint": "Määritä valitut mediat uudelle henkilölle",
|
"create_new_person_hint": "Määritä valitut mediat uudelle henkilölle",
|
||||||
"create_new_user": "Luo uusi käyttäjä",
|
"create_new_user": "Luo uusi käyttäjä",
|
||||||
|
"create_person": "Luo henkilö",
|
||||||
|
"create_person_subtitle": "Lisää nimi valituille kasvoille luodaksesi uudelle henkilölle tunnisteen",
|
||||||
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
"create_shared_album_page_share_add_assets": "LISÄÄ KOHTEITA",
|
||||||
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
"create_shared_album_page_share_select_photos": "Valitse kuvat",
|
||||||
"create_shared_link": "Luo jakolinkki",
|
"create_shared_link": "Luo jakolinkki",
|
||||||
@@ -866,6 +869,7 @@
|
|||||||
"crop_aspect_ratio_fixed": "Kiinteä",
|
"crop_aspect_ratio_fixed": "Kiinteä",
|
||||||
"crop_aspect_ratio_free": "Vapaa",
|
"crop_aspect_ratio_free": "Vapaa",
|
||||||
"crop_aspect_ratio_original": "Alkuperäinen",
|
"crop_aspect_ratio_original": "Alkuperäinen",
|
||||||
|
"crop_aspect_ratio_square": "Neliö",
|
||||||
"curated_object_page_title": "Asiat",
|
"curated_object_page_title": "Asiat",
|
||||||
"current_device": "Nykyinen laite",
|
"current_device": "Nykyinen laite",
|
||||||
"current_pin_code": "Nykyinen PIN-koodi",
|
"current_pin_code": "Nykyinen PIN-koodi",
|
||||||
@@ -880,7 +884,7 @@
|
|||||||
"daily_title_text_date": "E, dd MMM",
|
"daily_title_text_date": "E, dd MMM",
|
||||||
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
"daily_title_text_date_year": "E, dd MMM, yyyy",
|
||||||
"dark": "Tumma",
|
"dark": "Tumma",
|
||||||
"dark_theme": "Vaihda tumma teema",
|
"dark_theme": "Vaihda tummaan teemaan",
|
||||||
"date": "Päivämäärä",
|
"date": "Päivämäärä",
|
||||||
"date_after": "Päivämäärän jälkeen",
|
"date_after": "Päivämäärän jälkeen",
|
||||||
"date_and_time": "Päivämäärä ja aika",
|
"date_and_time": "Päivämäärä ja aika",
|
||||||
@@ -891,6 +895,8 @@
|
|||||||
"day": "Päivä",
|
"day": "Päivä",
|
||||||
"days": "Päivää",
|
"days": "Päivää",
|
||||||
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
|
"deduplicate_all": "Poista kaikkien kaksoiskappaleet",
|
||||||
|
"default_locale": "Oletuskieli",
|
||||||
|
"default_locale_description": "Muotoile päivämäärät ja luvut selaimesi kieliasetusten mukaan",
|
||||||
"delete": "Poista",
|
"delete": "Poista",
|
||||||
"delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myös paikallisesti",
|
"delete_action_confirmation_message": "Haluatko varmasti poistaa tämän aineiston? Tämä toiminto siirtää aineiston palvelimen roskakoriin ja kysyy, haluatko poistaa sen myös paikallisesti",
|
||||||
"delete_action_prompt": "{count} poistettu",
|
"delete_action_prompt": "{count} poistettu",
|
||||||
@@ -966,7 +972,7 @@
|
|||||||
"downloading_media": "Median lataaminen",
|
"downloading_media": "Median lataaminen",
|
||||||
"drop_files_to_upload": "Pudota tiedostot mihin tahansa ladataksesi ne",
|
"drop_files_to_upload": "Pudota tiedostot mihin tahansa ladataksesi ne",
|
||||||
"duplicates": "Kaksoiskappaleet",
|
"duplicates": "Kaksoiskappaleet",
|
||||||
"duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita",
|
"duplicates_description": "Selvitä jokaisen kohdalla mitkä (jos mitkään) ovat kaksoiskappaleita.",
|
||||||
"duration": "Kesto",
|
"duration": "Kesto",
|
||||||
"edit": "Muokkaa",
|
"edit": "Muokkaa",
|
||||||
"edit_album": "Muokkaa albumia",
|
"edit_album": "Muokkaa albumia",
|
||||||
@@ -1003,6 +1009,8 @@
|
|||||||
"editor_edits_applied_success": "Muutokset otettu käyttöön",
|
"editor_edits_applied_success": "Muutokset otettu käyttöön",
|
||||||
"editor_flip_horizontal": "Käännä vaakatasossa",
|
"editor_flip_horizontal": "Käännä vaakatasossa",
|
||||||
"editor_flip_vertical": "Käännä pystytasossa",
|
"editor_flip_vertical": "Käännä pystytasossa",
|
||||||
|
"editor_handle_corner": "{corner, select, top_left {Vasen yläkulma} top_right {Oikea yläkulma} bottom_left {Vasen alakulma} bottom_right {Oikea alakulma} other {A}} kulman kahva",
|
||||||
|
"editor_handle_edge": "{edge, select, top {Yläreuna} bottom {Alareuna} left {Vasen reuna} right {Oikea reuna} other {En}} reunan kahva",
|
||||||
"editor_orientation": "Suunta",
|
"editor_orientation": "Suunta",
|
||||||
"editor_reset_all_changes": "Nollaa muutokset",
|
"editor_reset_all_changes": "Nollaa muutokset",
|
||||||
"editor_rotate_left": "Kierrä 90° vastapäivään",
|
"editor_rotate_left": "Kierrä 90° vastapäivään",
|
||||||
@@ -1381,9 +1389,11 @@
|
|||||||
"library_page_sort_title": "Albumin otsikko",
|
"library_page_sort_title": "Albumin otsikko",
|
||||||
"licenses": "Lisenssit",
|
"licenses": "Lisenssit",
|
||||||
"light": "Vaalea",
|
"light": "Vaalea",
|
||||||
|
"light_theme": "Vaihda vaaleaan teemaan",
|
||||||
"like": "Tykkää",
|
"like": "Tykkää",
|
||||||
"like_deleted": "Tykkäys poistettu",
|
"like_deleted": "Tykkäys poistettu",
|
||||||
"link_motion_video": "Linkitä liikevideo",
|
"link_motion_video": "Linkitä liikevideo",
|
||||||
|
"link_to_docs": "Lisätietoja löytyy <link>dokumentaatiosta</link>.",
|
||||||
"link_to_oauth": "Linkki OAuth",
|
"link_to_oauth": "Linkki OAuth",
|
||||||
"linked_oauth_account": "Linkitetty OAuth-tili",
|
"linked_oauth_account": "Linkitetty OAuth-tili",
|
||||||
"list": "Lista",
|
"list": "Lista",
|
||||||
@@ -2207,6 +2217,7 @@
|
|||||||
"tag": "Tunniste",
|
"tag": "Tunniste",
|
||||||
"tag_assets": "Lisää tunnisteita",
|
"tag_assets": "Lisää tunnisteita",
|
||||||
"tag_created": "Luotu tunniste: {tag}",
|
"tag_created": "Luotu tunniste: {tag}",
|
||||||
|
"tag_face": "Merkitse kasvot",
|
||||||
"tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan",
|
"tag_feature_description": "Selaa valokuvia ja videoita, jotka on ryhmitelty loogisten tunnisteotsikoiden mukaan",
|
||||||
"tag_not_found_question": "Etkö löydä tunnistetta? <link>Luo uusi tunniste.</link>",
|
"tag_not_found_question": "Etkö löydä tunnistetta? <link>Luo uusi tunniste.</link>",
|
||||||
"tag_people": "Merkitse henkilö tunnisteella",
|
"tag_people": "Merkitse henkilö tunnisteella",
|
||||||
@@ -2388,6 +2399,7 @@
|
|||||||
"viewer_remove_from_stack": "Poista pinosta",
|
"viewer_remove_from_stack": "Poista pinosta",
|
||||||
"viewer_stack_use_as_main_asset": "Käytä pääkohteena",
|
"viewer_stack_use_as_main_asset": "Käytä pääkohteena",
|
||||||
"viewer_unstack": "Pura pino",
|
"viewer_unstack": "Pura pino",
|
||||||
|
"visibility": "Näkyvyys",
|
||||||
"visibility_changed": "{count, plural, one {# henkilön} other {# henkilöiden}} näkyvyys vaihdettu",
|
"visibility_changed": "{count, plural, one {# henkilön} other {# henkilöiden}} näkyvyys vaihdettu",
|
||||||
"visual": "Visuaalinen",
|
"visual": "Visuaalinen",
|
||||||
"visual_builder": "Visuaalinen koostaja",
|
"visual_builder": "Visuaalinen koostaja",
|
||||||
|
|||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Créer un lien pour partager",
|
"create_link_to_share": "Créer un lien pour partager",
|
||||||
"create_link_to_share_description": "Permettre à n'importe qui ayant le lien de voir la(es) photo(s) sélectionnée(s)",
|
"create_link_to_share_description": "Permettre à n'importe qui ayant le lien de voir la(es) photo(s) sélectionnée(s)",
|
||||||
"create_new": "NOUVEAU",
|
"create_new": "NOUVEAU",
|
||||||
|
"create_new_face": "Créer un nouveau visage",
|
||||||
"create_new_person": "Créer une nouvelle personne",
|
"create_new_person": "Créer une nouvelle personne",
|
||||||
"create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne",
|
"create_new_person_hint": "Attribuer les médias sélectionnés à une nouvelle personne",
|
||||||
"create_new_user": "Créer un nouvel utilisateur",
|
"create_new_user": "Créer un nouvel utilisateur",
|
||||||
|
"create_person": "Créer une personne",
|
||||||
|
"create_person_subtitle": "Ajouter un nom au visage sélectionné pour créer et étiqueter la nouvelle personne",
|
||||||
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
"create_shared_album_page_share_add_assets": "AJOUTER DES ÉLÉMENTS",
|
||||||
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
"create_shared_album_page_share_select_photos": "Sélectionner les photos",
|
||||||
"create_shared_link": "Créer un lien partagé",
|
"create_shared_link": "Créer un lien partagé",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Étiquette",
|
"tag": "Étiquette",
|
||||||
"tag_assets": "Étiqueter les médias",
|
"tag_assets": "Étiqueter les médias",
|
||||||
"tag_created": "Étiquette créée : {tag}",
|
"tag_created": "Étiquette créée : {tag}",
|
||||||
|
"tag_face": "Étiqueter le visage",
|
||||||
"tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques",
|
"tag_feature_description": "Parcourir les photos et vidéos groupées par thèmes logiques",
|
||||||
"tag_not_found_question": "Vous ne trouvez pas une étiquette ? <link>Créer une nouvelle étiquette.</link>",
|
"tag_not_found_question": "Vous ne trouvez pas une étiquette ? <link>Créer une nouvelle étiquette.</link>",
|
||||||
"tag_people": "Étiqueter les personnes",
|
"tag_people": "Étiqueter les personnes",
|
||||||
|
|||||||
@@ -849,9 +849,12 @@
|
|||||||
"create_link_to_share": "Crear ligazón para compartir",
|
"create_link_to_share": "Crear ligazón para compartir",
|
||||||
"create_link_to_share_description": "Permitir que calquera persoa coa ligazón vexa a(s) foto(s) seleccionada(s)",
|
"create_link_to_share_description": "Permitir que calquera persoa coa ligazón vexa a(s) foto(s) seleccionada(s)",
|
||||||
"create_new": "CREAR NOVO",
|
"create_new": "CREAR NOVO",
|
||||||
|
"create_new_face": "Crear nova cara",
|
||||||
"create_new_person": "Crear nova persoa",
|
"create_new_person": "Crear nova persoa",
|
||||||
"create_new_person_hint": "Asignar activos seleccionados a unha nova persoa",
|
"create_new_person_hint": "Asignar activos seleccionados a unha nova persoa",
|
||||||
"create_new_user": "Crear novo usuario",
|
"create_new_user": "Crear novo usuario",
|
||||||
|
"create_person": "Crear persona",
|
||||||
|
"create_person_subtitle": "Engade un nome á cara seleccionada para crear e etiquetar á nova persona",
|
||||||
"create_shared_album_page_share_add_assets": "ENGADIR ACTIVOS",
|
"create_shared_album_page_share_add_assets": "ENGADIR ACTIVOS",
|
||||||
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
|
"create_shared_album_page_share_select_photos": "Seleccionar Fotos",
|
||||||
"create_shared_link": "Crear ligazón compartida",
|
"create_shared_link": "Crear ligazón compartida",
|
||||||
@@ -2214,6 +2217,7 @@
|
|||||||
"tag": "Etiqueta",
|
"tag": "Etiqueta",
|
||||||
"tag_assets": "Etiquetar activos",
|
"tag_assets": "Etiquetar activos",
|
||||||
"tag_created": "Etiqueta creada: {tag}",
|
"tag_created": "Etiqueta creada: {tag}",
|
||||||
|
"tag_face": "Etiquetar cara",
|
||||||
"tag_feature_description": "Navegar por fotos e vídeos agrupados por temas de etiquetas lóxicas",
|
"tag_feature_description": "Navegar por fotos e vídeos agrupados por temas de etiquetas lóxicas",
|
||||||
"tag_not_found_question": "Non atopa unha etiqueta? <link>Crear unha nova etiqueta.</link>",
|
"tag_not_found_question": "Non atopa unha etiqueta? <link>Crear unha nova etiqueta.</link>",
|
||||||
"tag_people": "Etiquetar Persoas",
|
"tag_people": "Etiquetar Persoas",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user