mirror of
https://github.com/immich-app/immich.git
synced 2025-12-14 00:30:47 -08:00
Compare commits
1 Commits
chore/oxli
...
postgres-q
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d46e5f2436 |
@@ -1,10 +1,10 @@
|
|||||||
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:7c2e711a4f7b02f32d2da16192d5e05aa7c95279be4ce889cff5df316f251c1d
|
ARG BASEIMAGE=mcr.microsoft.com/devcontainers/typescript-node:22@sha256:a20b8a3538313487ac9266875bbf733e544c1aa2091df2bb99ab592a6d4f7399
|
||||||
FROM ${BASEIMAGE}
|
FROM ${BASEIMAGE}
|
||||||
|
|
||||||
# Flutter SDK
|
# Flutter SDK
|
||||||
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux
|
||||||
ENV FLUTTER_CHANNEL="stable"
|
ENV FLUTTER_CHANNEL="stable"
|
||||||
ENV FLUTTER_VERSION="3.29.3"
|
ENV FLUTTER_VERSION="3.29.1"
|
||||||
ENV FLUTTER_HOME=/flutter
|
ENV FLUTTER_HOME=/flutter
|
||||||
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
ENV PATH=${PATH}:${FLUTTER_HOME}/bin
|
||||||
|
|
||||||
|
|||||||
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -9,9 +9,6 @@ mobile/lib/**/*.g.dart 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
|
||||||
|
|
||||||
mobile/drift_schemas/main/drift_schema_*.json -diff -merge
|
|
||||||
mobile/drift_schemas/main/drift_schema_*.json linguist-generated=true
|
|
||||||
|
|
||||||
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
open-api/typescript-sdk/fetch-client.ts -diff -merge
|
||||||
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
open-api/typescript-sdk/fetch-client.ts linguist-generated=true
|
||||||
|
|
||||||
|
|||||||
2
.github/.nvmrc
vendored
2
.github/.nvmrc
vendored
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.14.0
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ body:
|
|||||||
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
label: I have searched the existing feature requests, both open and closed, to make sure this is not a duplicate request.
|
||||||
options:
|
options:
|
||||||
- label: 'Yes'
|
- label: 'Yes'
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: feature
|
id: feature
|
||||||
|
|||||||
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
1
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
@@ -6,6 +6,7 @@ body:
|
|||||||
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
label: I have searched the existing issues, both open and closed, to make sure this is not a duplicate report.
|
||||||
options:
|
options:
|
||||||
- label: 'Yes'
|
- label: 'Yes'
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
16
.github/workflows/build-mobile.yml
vendored
16
.github/workflows/build-mobile.yml
vendored
@@ -35,12 +35,12 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
mobile:
|
mobile:
|
||||||
@@ -61,19 +61,19 @@ jobs:
|
|||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ inputs.ref || github.sha }}
|
ref: ${{ inputs.ref || github.sha }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
- uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4
|
||||||
with:
|
with:
|
||||||
distribution: 'zulu'
|
distribution: 'zulu'
|
||||||
java-version: '17'
|
java-version: '17'
|
||||||
cache: 'gradle'
|
cache: 'gradle'
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -93,10 +93,6 @@ jobs:
|
|||||||
run: make translation
|
run: make translation
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate platform APIs
|
|
||||||
run: make pigeon
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Build Android App Bundle
|
- name: Build Android App Bundle
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
env:
|
env:
|
||||||
@@ -108,7 +104,7 @@ jobs:
|
|||||||
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
|
||||||
|
|
||||||
- name: Publish Android Artifact
|
- name: Publish Android Artifact
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
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
|
||||||
|
|||||||
2
.github/workflows/cache-cleanup.yml
vendored
2
.github/workflows/cache-cleanup.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
|||||||
actions: write
|
actions: write
|
||||||
steps:
|
steps:
|
||||||
- name: Check out code
|
- name: Check out code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
|
|||||||
12
.github/workflows/cli.yml
vendored
12
.github/workflows/cli.yml
vendored
@@ -29,12 +29,12 @@ jobs:
|
|||||||
working-directory: ./cli
|
working-directory: ./cli
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
@@ -59,7 +59,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -70,7 +70,7 @@ jobs:
|
|||||||
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
if: ${{ !github.event.pull_request.head.repo.fork }}
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
@@ -85,7 +85,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate docker image tags
|
- name: Generate docker image tags
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
with:
|
with:
|
||||||
flavor: |
|
flavor: |
|
||||||
latest=false
|
latest=false
|
||||||
@@ -96,7 +96,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@1dc73863535b631f98b2378be8619f83b136f4a0 # v6.17.0
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
with:
|
with:
|
||||||
file: cli/Dockerfile
|
file: cli/Dockerfile
|
||||||
platforms: linux/amd64,linux/arm64
|
platforms: linux/amd64,linux/arm64
|
||||||
|
|||||||
8
.github/workflows/codeql-analysis.yml
vendored
8
.github/workflows/codeql-analysis.yml
vendored
@@ -44,13 +44,13 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/init@28deaeda66b76a05916b6923827895f2b14ab387 # v3
|
||||||
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.
|
||||||
@@ -63,7 +63,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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/autobuild@28deaeda66b76a05916b6923827895f2b14ab387 # v3
|
||||||
|
|
||||||
# ℹ️ 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
|
||||||
@@ -76,6 +76,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@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/analyze@28deaeda66b76a05916b6923827895f2b14ab387 # v3
|
||||||
with:
|
with:
|
||||||
category: '/language:${{matrix.language}}'
|
category: '/language:${{matrix.language}}'
|
||||||
|
|||||||
493
.github/workflows/docker.yml
vendored
493
.github/workflows/docker.yml
vendored
@@ -24,11 +24,11 @@ jobs:
|
|||||||
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_ml: ${{ steps.found_paths.outputs.machine-learning == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
server:
|
server:
|
||||||
@@ -40,8 +40,6 @@ jobs:
|
|||||||
- 'machine-learning/**'
|
- 'machine-learning/**'
|
||||||
workflow:
|
workflow:
|
||||||
- '.github/workflows/docker.yml'
|
- '.github/workflows/docker.yml'
|
||||||
- '.github/workflows/multi-runner-build.yml'
|
|
||||||
- '.github/actions/image-build'
|
|
||||||
|
|
||||||
- name: Check if we should force jobs to run
|
- name: Check if we should force jobs to run
|
||||||
id: should_force
|
id: should_force
|
||||||
@@ -60,7 +58,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@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -89,7 +87,7 @@ jobs:
|
|||||||
suffix: ['']
|
suffix: ['']
|
||||||
steps:
|
steps:
|
||||||
- name: Login to GitHub Container Registry
|
- name: Login to GitHub Container Registry
|
||||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
with:
|
with:
|
||||||
registry: ghcr.io
|
registry: ghcr.io
|
||||||
username: ${{ github.repository_owner }}
|
username: ${{ github.repository_owner }}
|
||||||
@@ -105,89 +103,452 @@ jobs:
|
|||||||
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_PR}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
docker buildx imagetools create -t "${REGISTRY_NAME}/${REPOSITORY}:${TAG_COMMIT}" "${REGISTRY_NAME}/${REPOSITORY}:${TAG_OLD}"
|
||||||
|
|
||||||
machine-learning:
|
build_and_push_ml:
|
||||||
name: Build and Push ML
|
name: Build and Push ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
env:
|
||||||
|
image: immich-machine-learning
|
||||||
|
context: machine-learning
|
||||||
|
file: machine-learning/Dockerfile
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||||
|
strategy:
|
||||||
|
# Prevent a failure in one image from stopping the other builds
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
device: cpu
|
||||||
|
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
device: cpu
|
||||||
|
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
device: cuda
|
||||||
|
suffix: -cuda
|
||||||
|
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: mich
|
||||||
|
device: rocm
|
||||||
|
suffix: -rocm
|
||||||
|
|
||||||
|
- platform: linux/amd64
|
||||||
|
runner: ubuntu-latest
|
||||||
|
device: openvino
|
||||||
|
suffix: -openvino
|
||||||
|
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
device: armnn
|
||||||
|
suffix: -armnn
|
||||||
|
|
||||||
|
- platform: linux/arm64
|
||||||
|
runner: ubuntu-24.04-arm
|
||||||
|
device: rknn
|
||||||
|
suffix: -rknn
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform=${{ matrix.platform }}
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate cache key suffix
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||||
|
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate cache target
|
||||||
|
id: cache-target
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${{ matrix.device }}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
|
with:
|
||||||
|
context: ${{ env.context }}
|
||||||
|
file: ${{ env.file }}
|
||||||
|
platforms: ${{ matrix.platforms }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ matrix.device }}-main
|
||||||
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
build-args: |
|
||||||
|
DEVICE=${{ matrix.device }}
|
||||||
|
BUILD_ID=${{ github.run_id }}
|
||||||
|
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||||
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: | # zizmor: ignore[template-injection]
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: ml-digests-${{ matrix.device }}-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge_ml:
|
||||||
|
name: Merge & Push ML
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
actions: read
|
||||||
|
packages: write
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
|
env:
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-machine-learning
|
||||||
|
DOCKER_REPO: altran1502/immich-machine-learning
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- device: cpu
|
||||||
|
- device: cuda
|
||||||
|
suffix: -cuda
|
||||||
|
- device: rocm
|
||||||
|
suffix: -rocm
|
||||||
|
- device: openvino
|
||||||
|
suffix: -openvino
|
||||||
|
- device: armnn
|
||||||
|
suffix: -armnn
|
||||||
|
- device: rknn
|
||||||
|
suffix: -rknn
|
||||||
|
needs:
|
||||||
|
- build_and_push_ml
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: ml-digests-${{ matrix.device }}-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
# Disable latest tag
|
||||||
|
latest=false
|
||||||
|
suffix=${{ matrix.suffix }}
|
||||||
|
images: |
|
||||||
|
name=${{ env.GHCR_REPO }}
|
||||||
|
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||||
|
tags: |
|
||||||
|
# Tag with branch name
|
||||||
|
type=ref,event=branch
|
||||||
|
# Tag with pr-number
|
||||||
|
type=ref,event=pr
|
||||||
|
# Tag with long commit sha hash
|
||||||
|
type=sha,format=long,prefix=commit-
|
||||||
|
# Tag with git tag on release
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
# Process annotations
|
||||||
|
declare -a ANNOTATIONS=()
|
||||||
|
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
|
||||||
|
while IFS= read -r annotation; do
|
||||||
|
# Extract key and value by removing the manifest: prefix
|
||||||
|
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
value="${BASH_REMATCH[2]}"
|
||||||
|
# Use array to properly handle arguments with spaces
|
||||||
|
ANNOTATIONS+=(--annotation "index:$key=$value")
|
||||||
|
fi
|
||||||
|
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
|
||||||
|
|
||||||
|
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
||||||
|
|
||||||
|
build_and_push_server:
|
||||||
|
name: Build and Push Server
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
needs: pre-job
|
||||||
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
|
env:
|
||||||
|
image: immich-server
|
||||||
|
context: .
|
||||||
|
file: server/Dockerfile
|
||||||
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- device: cpu
|
- platform: linux/amd64
|
||||||
tag-suffix: ''
|
runner: ubuntu-latest
|
||||||
- device: cuda
|
- platform: linux/arm64
|
||||||
tag-suffix: '-cuda'
|
runner: ubuntu-24.04-arm
|
||||||
platforms: linux/amd64
|
steps:
|
||||||
- device: openvino
|
- name: Prepare
|
||||||
tag-suffix: '-openvino'
|
run: |
|
||||||
platforms: linux/amd64
|
platform=${{ matrix.platform }}
|
||||||
- device: armnn
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
tag-suffix: '-armnn'
|
|
||||||
platforms: linux/arm64
|
|
||||||
- device: rknn
|
|
||||||
tag-suffix: '-rknn'
|
|
||||||
platforms: linux/arm64
|
|
||||||
- device: rocm
|
|
||||||
tag-suffix: '-rocm'
|
|
||||||
platforms: linux/amd64
|
|
||||||
runner-mapping: '{"linux/amd64": "mich"}'
|
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
actions: read
|
|
||||||
packages: write
|
|
||||||
secrets:
|
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
|
||||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
|
||||||
with:
|
|
||||||
image: immich-machine-learning
|
|
||||||
context: machine-learning
|
|
||||||
dockerfile: machine-learning/Dockerfile
|
|
||||||
platforms: ${{ matrix.platforms }}
|
|
||||||
runner-mapping: ${{ matrix.runner-mapping }}
|
|
||||||
tag-suffix: ${{ matrix.tag-suffix }}
|
|
||||||
dockerhub-push: ${{ github.event_name == 'release' }}
|
|
||||||
build-args: |
|
|
||||||
DEVICE=${{ matrix.device }}
|
|
||||||
|
|
||||||
server:
|
- name: Checkout
|
||||||
name: Build and Push Server
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
needs: pre-job
|
with:
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
persist-credentials: false
|
||||||
uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@094bfb927b8cd75b343abaac27b3241be0fccfe9 # multi-runner-build-workflow-0.1.0
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
|
|
||||||
|
- name: Login to GitHub Container Registry
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
if: ${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Generate cache key suffix
|
||||||
|
env:
|
||||||
|
REF: ${{ github.ref_name }}
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
|
||||||
|
echo "CACHE_KEY_SUFFIX=pr-${{ github.event.number }}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
SUFFIX=$(echo "${REF}" | sed 's/[^a-zA-Z0-9]/-/g')
|
||||||
|
echo "CACHE_KEY_SUFFIX=${SUFFIX}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate cache target
|
||||||
|
id: cache-target
|
||||||
|
run: |
|
||||||
|
if [[ "${{ github.event.pull_request.head.repo.fork }}" == "true" ]]; then
|
||||||
|
# Essentially just ignore the cache output (forks can't write to registry cache)
|
||||||
|
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
|
||||||
|
else
|
||||||
|
echo "cache-to=type=registry,ref=${GHCR_REPO}-build-cache:${PLATFORM_PAIR}-${CACHE_KEY_SUFFIX},mode=max,compression=zstd" >> $GITHUB_OUTPUT
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
|
||||||
|
- name: Build and push image
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@14487ce63c7a62a4a324b0bfb37086795e31c6c1 # v6.16.0
|
||||||
|
with:
|
||||||
|
context: ${{ env.context }}
|
||||||
|
file: ${{ env.file }}
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
cache-to: ${{ steps.cache-target.outputs.cache-to }}
|
||||||
|
cache-from: |
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-${{ env.CACHE_KEY_SUFFIX }}
|
||||||
|
type=registry,ref=${{ env.GHCR_REPO }}-build-cache:${{ env.PLATFORM_PAIR }}-main
|
||||||
|
outputs: type=image,"name=${{ env.GHCR_REPO }}",push-by-digest=true,name-canonical=true,push=${{ !github.event.pull_request.head.repo.fork }}
|
||||||
|
build-args: |
|
||||||
|
DEVICE=cpu
|
||||||
|
BUILD_ID=${{ github.run_id }}
|
||||||
|
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||||
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
|
- name: Export digest
|
||||||
|
run: | # zizmor: ignore[template-injection]
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
touch "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
|
with:
|
||||||
|
name: server-digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge_server:
|
||||||
|
name: Merge & Push Server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
actions: read
|
actions: read
|
||||||
packages: write
|
packages: write
|
||||||
secrets:
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' && !github.event.pull_request.head.repo.fork }}
|
||||||
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
|
env:
|
||||||
DOCKERHUB_TOKEN: ${{ secrets.DOCKERHUB_TOKEN }}
|
GHCR_REPO: ghcr.io/${{ github.repository_owner }}/immich-server
|
||||||
with:
|
DOCKER_REPO: altran1502/immich-server
|
||||||
image: immich-server
|
needs:
|
||||||
context: .
|
- build_and_push_server
|
||||||
dockerfile: server/Dockerfile
|
steps:
|
||||||
dockerhub-push: ${{ github.event_name == 'release' }}
|
- name: Download digests
|
||||||
build-args: |
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
DEVICE=cpu
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: server-digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
if: ${{ github.event_name == 'release' }}
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3
|
||||||
|
|
||||||
|
- name: Generate docker image tags
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_PR_HEAD_SHA: 'true'
|
||||||
|
with:
|
||||||
|
flavor: |
|
||||||
|
# Disable latest tag
|
||||||
|
latest=false
|
||||||
|
suffix=${{ matrix.suffix }}
|
||||||
|
images: |
|
||||||
|
name=${{ env.GHCR_REPO }}
|
||||||
|
name=${{ env.DOCKER_REPO }},enable=${{ github.event_name == 'release' }}
|
||||||
|
tags: |
|
||||||
|
# Tag with branch name
|
||||||
|
type=ref,event=branch
|
||||||
|
# Tag with pr-number
|
||||||
|
type=ref,event=pr
|
||||||
|
# Tag with long commit sha hash
|
||||||
|
type=sha,format=long,prefix=commit-
|
||||||
|
# Tag with git tag on release
|
||||||
|
type=ref,event=tag
|
||||||
|
type=raw,value=release,enable=${{ github.event_name == 'release' }}
|
||||||
|
|
||||||
|
- name: Create manifest list and push
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
# Process annotations
|
||||||
|
declare -a ANNOTATIONS=()
|
||||||
|
if [[ -n "$DOCKER_METADATA_OUTPUT_JSON" ]]; then
|
||||||
|
while IFS= read -r annotation; do
|
||||||
|
# Extract key and value by removing the manifest: prefix
|
||||||
|
if [[ "$annotation" =~ ^manifest:(.+)=(.+)$ ]]; then
|
||||||
|
key="${BASH_REMATCH[1]}"
|
||||||
|
value="${BASH_REMATCH[2]}"
|
||||||
|
# Use array to properly handle arguments with spaces
|
||||||
|
ANNOTATIONS+=(--annotation "index:$key=$value")
|
||||||
|
fi
|
||||||
|
done < <(jq -r '.annotations[]' <<< "$DOCKER_METADATA_OUTPUT_JSON")
|
||||||
|
fi
|
||||||
|
|
||||||
|
TAGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") \
|
||||||
|
SOURCE_ARGS=$(printf "${GHCR_REPO}@sha256:%s " *)
|
||||||
|
|
||||||
|
docker buildx imagetools create $TAGS "${ANNOTATIONS[@]}" $SOURCE_ARGS
|
||||||
|
|
||||||
success-check-server:
|
success-check-server:
|
||||||
name: Docker Build & Push Server Success
|
name: Docker Build & Push Server Success
|
||||||
needs: [server, retag_server]
|
needs: [merge_server, retag_server]
|
||||||
permissions: {}
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- name: Any jobs failed?
|
||||||
with:
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
needs: ${{ toJSON(needs) }}
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|
||||||
success-check-ml:
|
success-check-ml:
|
||||||
name: Docker Build & Push ML Success
|
name: Docker Build & Push ML Success
|
||||||
needs: [machine-learning, retag_ml]
|
needs: [merge_ml, retag_ml]
|
||||||
permissions: {}
|
permissions: {}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- name: Any jobs failed?
|
||||||
with:
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
needs: ${{ toJSON(needs) }}
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|||||||
10
.github/workflows/docs-build.yml
vendored
10
.github/workflows/docs-build.yml
vendored
@@ -21,11 +21,11 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
docs:
|
docs:
|
||||||
@@ -49,12 +49,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './docs/.nvmrc'
|
node-version-file: './docs/.nvmrc'
|
||||||
|
|
||||||
@@ -68,7 +68,7 @@ jobs:
|
|||||||
run: npm run build
|
run: npm run build
|
||||||
|
|
||||||
- name: Upload build output
|
- name: Upload build output
|
||||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||||
with:
|
with:
|
||||||
name: docs-build-output
|
name: docs-build-output
|
||||||
path: docs/build/
|
path: docs/build/
|
||||||
|
|||||||
21
.github/workflows/docs-deploy.yml
vendored
21
.github/workflows/docs-deploy.yml
vendored
@@ -20,7 +20,7 @@ jobs:
|
|||||||
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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
||||||
@@ -38,7 +38,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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||||
with:
|
with:
|
||||||
@@ -108,13 +108,13 @@ 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:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Load parameters
|
- name: Load parameters
|
||||||
id: parameters
|
id: parameters
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
PARAM_JSON: ${{ needs.checks.outputs.parameters }}
|
||||||
with:
|
with:
|
||||||
@@ -125,7 +125,7 @@ jobs:
|
|||||||
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
core.setOutput("shouldDeploy", parameters.shouldDeploy);
|
||||||
|
|
||||||
- name: Download artifact
|
- name: Download artifact
|
||||||
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
env:
|
env:
|
||||||
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
ARTIFACT_JSON: ${{ needs.checks.outputs.artifact }}
|
||||||
with:
|
with:
|
||||||
@@ -150,7 +150,7 @@ jobs:
|
|||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
||||||
with:
|
with:
|
||||||
tg_version: '0.58.12'
|
tg_version: '0.58.12'
|
||||||
tofu_version: '1.7.1'
|
tofu_version: '1.7.1'
|
||||||
@@ -165,7 +165,7 @@ jobs:
|
|||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
||||||
with:
|
with:
|
||||||
tg_version: '0.58.12'
|
tg_version: '0.58.12'
|
||||||
tofu_version: '1.7.1'
|
tofu_version: '1.7.1'
|
||||||
@@ -181,8 +181,7 @@ jobs:
|
|||||||
echo "output=$CLEANED" >> $GITHUB_OUTPUT
|
echo "output=$CLEANED" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Publish to Cloudflare Pages
|
- name: Publish to Cloudflare Pages
|
||||||
# TODO: Action is deprecated
|
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1
|
||||||
uses: cloudflare/pages-action@f0a1cd58cd66095dee69bfa18fa5efd1dde93bca # v1.5.0
|
|
||||||
with:
|
with:
|
||||||
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
|
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
|
||||||
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
@@ -199,7 +198,7 @@ jobs:
|
|||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
||||||
with:
|
with:
|
||||||
tg_version: '0.58.12'
|
tg_version: '0.58.12'
|
||||||
tofu_version: '1.7.1'
|
tofu_version: '1.7.1'
|
||||||
@@ -207,7 +206,7 @@ jobs:
|
|||||||
tg_command: 'apply'
|
tg_command: 'apply'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
||||||
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
if: ${{ steps.parameters.outputs.event == 'pr' }}
|
||||||
with:
|
with:
|
||||||
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
|
||||||
|
|||||||
6
.github/workflows/docs-destroy.yml
vendored
6
.github/workflows/docs-destroy.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ jobs:
|
|||||||
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
|
||||||
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
|
||||||
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
|
||||||
uses: gruntwork-io/terragrunt-action@aee21a7df999be8b471c2a8564c6cd853cb674e1 # v2.1.8
|
uses: gruntwork-io/terragrunt-action@9559e51d05873b0ea467c42bbabcb5c067642ccc # v2
|
||||||
with:
|
with:
|
||||||
tg_version: '0.58.12'
|
tg_version: '0.58.12'
|
||||||
tofu_version: '1.7.1'
|
tofu_version: '1.7.1'
|
||||||
@@ -33,7 +33,7 @@ jobs:
|
|||||||
tg_command: 'destroy -refresh=false'
|
tg_command: 'destroy -refresh=false'
|
||||||
|
|
||||||
- name: Comment
|
- name: Comment
|
||||||
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
|
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3
|
||||||
with:
|
with:
|
||||||
number: ${{ github.event.number }}
|
number: ${{ github.event.number }}
|
||||||
delete: true
|
delete: true
|
||||||
|
|||||||
10
.github/workflows/fix-format.yml
vendored
10
.github/workflows/fix-format.yml
vendored
@@ -16,20 +16,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: 'Checkout'
|
- name: 'Checkout'
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
ref: ${{ github.event.pull_request.head.ref }}
|
ref: ${{ github.event.pull_request.head.ref }}
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
@@ -37,13 +37,13 @@ jobs:
|
|||||||
run: make install-all && make format-all
|
run: make install-all && make format-all
|
||||||
|
|
||||||
- name: Commit and push
|
- name: Commit and push
|
||||||
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9.1.4
|
uses: EndBug/add-and-commit@a94899bca583c204427a224a7af87c02f9b325d5 # v9
|
||||||
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@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
if: always()
|
if: always()
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
|
|||||||
2
.github/workflows/pr-label-validation.yml
vendored
2
.github/workflows/pr-label-validation.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- name: Require PR to have a changelog label
|
- name: Require PR to have a changelog label
|
||||||
uses: mheap/github-action-required-labels@fb29a14a076b0f74099f6198f77750e8fc236016 # v5.5.0
|
uses: mheap/github-action-required-labels@388fd6af37b34cdfe5a23b37060e763217e58b03 # v5
|
||||||
with:
|
with:
|
||||||
mode: exactly
|
mode: exactly
|
||||||
count: 1
|
count: 1
|
||||||
|
|||||||
2
.github/workflows/pr-labeler.yml
vendored
2
.github/workflows/pr-labeler.yml
vendored
@@ -11,4 +11,4 @@ jobs:
|
|||||||
pull-requests: write
|
pull-requests: write
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5.0.0
|
- uses: actions/labeler@8558fd74291d67161a8a78ce36a881fa63b766a9 # v5
|
||||||
|
|||||||
16
.github/workflows/prepare-release.yml
vendored
16
.github/workflows/prepare-release.yml
vendored
@@ -32,19 +32,19 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: true
|
persist-credentials: true
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
|
|
||||||
- name: Bump version
|
- name: Bump version
|
||||||
env:
|
env:
|
||||||
@@ -54,7 +54,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@a94899bca583c204427a224a7af87c02f9b325d5 # v9
|
||||||
with:
|
with:
|
||||||
default_author: github_actions
|
default_author: github_actions
|
||||||
message: 'chore: version ${{ env.IMMICH_VERSION }}'
|
message: 'chore: version ${{ env.IMMICH_VERSION }}'
|
||||||
@@ -83,24 +83,24 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Generate a token
|
- name: Generate a token
|
||||||
id: generate-token
|
id: generate-token
|
||||||
uses: actions/create-github-app-token@df432ceedc7162793a195dd1713ff69aefc7379e # v2.0.6
|
uses: actions/create-github-app-token@3ff1caaa28b64c9cc276ce0a02e2ff584f3900c5 # v2
|
||||||
with:
|
with:
|
||||||
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
|
||||||
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
|
||||||
|
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
token: ${{ steps.generate-token.outputs.token }}
|
token: ${{ steps.generate-token.outputs.token }}
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Download APK
|
- name: Download APK
|
||||||
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
||||||
with:
|
with:
|
||||||
name: release-apk-signed
|
name: release-apk-signed
|
||||||
|
|
||||||
- name: Create draft release
|
- name: Create draft release
|
||||||
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2.2.2
|
uses: softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631 # v2
|
||||||
with:
|
with:
|
||||||
draft: true
|
draft: true
|
||||||
tag_name: ${{ env.IMMICH_VERSION }}
|
tag_name: ${{ env.IMMICH_VERSION }}
|
||||||
|
|||||||
4
.github/workflows/preview-label.yaml
vendored
4
.github/workflows/preview-label.yaml
vendored
@@ -13,7 +13,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2.8.2
|
- uses: mshick/add-pr-comment@b8f338c590a895d50bcbfa6c5859251edc8952fc # v2
|
||||||
with:
|
with:
|
||||||
message-id: 'preview-status'
|
message-id: 'preview-status'
|
||||||
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/'
|
message: 'Deploying preview environment to https://pr-${{ github.event.pull_request.number }}.preview.internal.immich.cloud/'
|
||||||
@@ -24,7 +24,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
|
- uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
github.rest.issues.removeLabel({
|
github.rest.issues.removeLabel({
|
||||||
|
|||||||
4
.github/workflows/sdk.yml
vendored
4
.github/workflows/sdk.yml
vendored
@@ -16,12 +16,12 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./open-api/typescript-sdk
|
working-directory: ./open-api/typescript-sdk
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Setup .npmrc file to publish to npm
|
# Setup .npmrc file to publish to npm
|
||||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
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'
|
||||||
|
|||||||
34
.github/workflows/static_analysis.yml
vendored
34
.github/workflows/static_analysis.yml
vendored
@@ -20,11 +20,11 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
mobile:
|
mobile:
|
||||||
@@ -44,12 +44,12 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
@@ -58,28 +58,16 @@ jobs:
|
|||||||
run: dart pub get
|
run: dart pub get
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Install DCM
|
|
||||||
run: |
|
|
||||||
sudo apt-get update
|
|
||||||
wget -qO- https://dcm.dev/pgp-key.public | sudo gpg --dearmor -o /usr/share/keyrings/dcm.gpg
|
|
||||||
echo 'deb [signed-by=/usr/share/keyrings/dcm.gpg arch=amd64] https://dcm.dev/debian stable main' | sudo tee /etc/apt/sources.list.d/dart_stable.list
|
|
||||||
sudo apt-get update
|
|
||||||
sudo apt-get install dcm
|
|
||||||
|
|
||||||
- name: Generate translation file
|
- name: Generate translation file
|
||||||
run: make translation
|
run: make translation; dart format lib/generated/codegen_loader.g.dart
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run Build Runner
|
- name: Run Build Runner
|
||||||
run: make build
|
run: make build
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Generate platform API
|
|
||||||
run: make pigeon
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -108,10 +96,6 @@ jobs:
|
|||||||
run: dart run custom_lint
|
run: dart run custom_lint
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
|
|
||||||
- name: Run DCM
|
|
||||||
run: dcm analyze lib
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
zizmor:
|
zizmor:
|
||||||
name: zizmor
|
name: zizmor
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -121,12 +105,12 @@ jobs:
|
|||||||
actions: read
|
actions: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
- name: Install the latest version of uv
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
|
|
||||||
- name: Run zizmor 🌈
|
- name: Run zizmor 🌈
|
||||||
run: uvx zizmor --format=sarif . > results.sarif
|
run: uvx zizmor --format=sarif . > results.sarif
|
||||||
@@ -134,7 +118,7 @@ jobs:
|
|||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload SARIF file
|
- name: Upload SARIF file
|
||||||
uses: github/codeql-action/upload-sarif@ff0a06e83cb2de871e5a09832bc6a81e7276941f # v3.28.18
|
uses: github/codeql-action/upload-sarif@28deaeda66b76a05916b6923827895f2b14ab387 # v3
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
category: zizmor
|
category: zizmor
|
||||||
|
|||||||
140
.github/workflows/test.yml
vendored
140
.github/workflows/test.yml
vendored
@@ -17,7 +17,6 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
outputs:
|
outputs:
|
||||||
should_run_i18n: ${{ steps.found_paths.outputs.i18n == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
|
||||||
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_web: ${{ steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_server: ${{ steps.found_paths.outputs.server == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
should_run_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
|
||||||
@@ -29,16 +28,14 @@ jobs:
|
|||||||
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
|
should_run_.github: ${{ steps.found_paths.outputs['.github'] == 'true' || steps.should_force.outputs.should_force == 'true' }} # redundant to have should_force but if someone changes the trigger then this won't have to be changed
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
i18n:
|
|
||||||
- 'i18n/**'
|
|
||||||
web:
|
web:
|
||||||
- 'web/**'
|
- 'web/**'
|
||||||
- 'i18n/**'
|
- 'i18n/**'
|
||||||
@@ -76,12 +73,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
@@ -117,12 +114,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
|
|
||||||
@@ -162,12 +159,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './cli/.nvmrc'
|
node-version-file: './cli/.nvmrc'
|
||||||
|
|
||||||
@@ -200,12 +197,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
|
|
||||||
@@ -241,12 +238,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './web/.nvmrc'
|
node-version-file: './web/.nvmrc'
|
||||||
|
|
||||||
@@ -265,46 +262,6 @@ jobs:
|
|||||||
run: npm run test:cov
|
run: npm run test:cov
|
||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
i18n-tests:
|
|
||||||
name: Test i18n
|
|
||||||
needs: pre-job
|
|
||||||
if: ${{ needs.pre-job.outputs.should_run_i18n == 'true' }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup Node
|
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
|
||||||
with:
|
|
||||||
node-version-file: './web/.nvmrc'
|
|
||||||
|
|
||||||
- name: Install dependencies
|
|
||||||
run: npm --prefix=web ci
|
|
||||||
|
|
||||||
- name: Format
|
|
||||||
run: npm --prefix=web run format:i18n
|
|
||||||
|
|
||||||
- name: Find file changes
|
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
|
||||||
id: verify-changed-files
|
|
||||||
with:
|
|
||||||
files: |
|
|
||||||
i18n/**
|
|
||||||
|
|
||||||
- name: Verify files have not changed
|
|
||||||
if: steps.verify-changed-files.outputs.files_changed == 'true'
|
|
||||||
env:
|
|
||||||
CHANGED_FILES: ${{ steps.verify-changed-files.outputs.changed_files }}
|
|
||||||
run: |
|
|
||||||
echo "ERROR: i18n files not up to date!"
|
|
||||||
echo "Changed files: ${CHANGED_FILES}"
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
e2e-tests-lint:
|
e2e-tests-lint:
|
||||||
name: End-to-End Lint
|
name: End-to-End Lint
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
@@ -318,12 +275,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
|
||||||
@@ -361,12 +318,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
@@ -389,17 +346,17 @@ jobs:
|
|||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [mich, ubuntu-24.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
|
||||||
@@ -437,17 +394,17 @@ jobs:
|
|||||||
working-directory: ./e2e
|
working-directory: ./e2e
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
runner: [ubuntu-latest, ubuntu-24.04-arm]
|
runner: [mich, ubuntu-24.04-arm]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
submodules: 'recursive'
|
submodules: 'recursive'
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './e2e/.nvmrc'
|
node-version-file: './e2e/.nvmrc'
|
||||||
|
|
||||||
@@ -479,9 +436,13 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- name: Any jobs failed?
|
||||||
with:
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
needs: ${{ toJSON(needs) }}
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Unit Test Mobile
|
name: Unit Test Mobile
|
||||||
@@ -491,20 +452,15 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Flutter SDK
|
- name: Setup Flutter SDK
|
||||||
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2.19.0
|
uses: subosito/flutter-action@e938fdf56512cc96ef2f93601a5a40bde3801046 # v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: 'stable'
|
||||||
flutter-version-file: ./mobile/pubspec.yaml
|
flutter-version-file: ./mobile/pubspec.yaml
|
||||||
|
|
||||||
- name: Generate translation file
|
|
||||||
run: make translation
|
|
||||||
working-directory: ./mobile
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
working-directory: ./mobile
|
working-directory: ./mobile
|
||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
@@ -520,13 +476,13 @@ jobs:
|
|||||||
run:
|
run:
|
||||||
working-directory: ./machine-learning
|
working-directory: ./machine-learning
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Install uv
|
- name: Install uv
|
||||||
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5.4.2
|
uses: astral-sh/setup-uv@d4b2f3b6ecc6e67c4457f6d3e41ec42d3d0fcb86 # v5
|
||||||
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
|
- uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5
|
||||||
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
# TODO: add caching when supported (https://github.com/actions/setup-python/pull/818)
|
||||||
# with:
|
# with:
|
||||||
# python-version: 3.11
|
# python-version: 3.11
|
||||||
@@ -560,12 +516,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './.github/.nvmrc'
|
node-version-file: './.github/.nvmrc'
|
||||||
|
|
||||||
@@ -582,7 +538,7 @@ jobs:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
@@ -601,12 +557,12 @@ jobs:
|
|||||||
contents: read
|
contents: read
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
@@ -620,7 +576,7 @@ jobs:
|
|||||||
run: make open-api
|
run: make open-api
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -637,14 +593,14 @@ jobs:
|
|||||||
echo "Changed files: ${CHANGED_FILES}"
|
echo "Changed files: ${CHANGED_FILES}"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
||||||
sql-schema-up-to-date:
|
generated-typeorm-migrations-up-to-date:
|
||||||
name: SQL Schema Checks
|
name: TypeORM Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env:
|
env:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
@@ -662,12 +618,12 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
- name: Setup Node
|
- name: Setup Node
|
||||||
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
|
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||||
with:
|
with:
|
||||||
node-version-file: './server/.nvmrc'
|
node-version-file: './server/.nvmrc'
|
||||||
|
|
||||||
@@ -685,10 +641,10 @@ jobs:
|
|||||||
|
|
||||||
- name: Generate new migrations
|
- name: Generate new migrations
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
run: npm run migrations:generate src/TestMigration
|
run: npm run migrations:generate TestMigration
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
||||||
id: verify-changed-files
|
id: verify-changed-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
@@ -709,7 +665,7 @@ jobs:
|
|||||||
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
DB_URL: postgres://postgres:postgres@localhost:5432/immich
|
||||||
|
|
||||||
- name: Find file changes
|
- name: Find file changes
|
||||||
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
|
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20
|
||||||
id: verify-changed-sql-files
|
id: verify-changed-sql-files
|
||||||
with:
|
with:
|
||||||
files: |
|
files: |
|
||||||
|
|||||||
16
.github/workflows/weblate-lock.yml
vendored
16
.github/workflows/weblate-lock.yml
vendored
@@ -15,11 +15,11 @@ jobs:
|
|||||||
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
should_run: ${{ steps.found_paths.outputs.i18n == 'true' && github.head_ref != 'chore/translations'}}
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4
|
||||||
with:
|
with:
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
- id: found_paths
|
- id: found_paths
|
||||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
|
||||||
with:
|
with:
|
||||||
filters: |
|
filters: |
|
||||||
i18n:
|
i18n:
|
||||||
@@ -38,7 +38,7 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
- name: Find Pull Request
|
- name: Find Pull Request
|
||||||
uses: juliangruber/find-pull-request-action@48b6133aa6c826f267ebd33aa2d29470f9d9e7d0 # v1.9.0
|
uses: juliangruber/find-pull-request-action@48b6133aa6c826f267ebd33aa2d29470f9d9e7d0 # v1
|
||||||
id: find-pr
|
id: find-pr
|
||||||
with:
|
with:
|
||||||
branch: chore/translations
|
branch: chore/translations
|
||||||
@@ -52,6 +52,10 @@ jobs:
|
|||||||
permissions: {}
|
permissions: {}
|
||||||
if: always()
|
if: always()
|
||||||
steps:
|
steps:
|
||||||
- uses: immich-app/devtools/actions/success-check@6b81b1572e466f7f48ba3c823159ce3f4a4d66a6 # success-check-action-0.0.3
|
- name: Any jobs failed?
|
||||||
with:
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
needs: ${{ toJSON(needs) }}
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
# zizmor: ignore[template-injection]
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,7 +3,6 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/launch.json
|
!.vscode/launch.json
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
.idea
|
||||||
|
|
||||||
docker/upload
|
docker/upload
|
||||||
|
|||||||
10
.vscode/extensions.json
vendored
10
.vscode/extensions.json
vendored
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"svelte.svelte-vscode",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"dart-code.flutter",
|
|
||||||
"dart-code.dart-code",
|
|
||||||
"dcmdev.dcm-vscode-extension"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.14.0
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:22.16.0-alpine3.20@sha256:2289fb1fba0f4633b08ec47b94a89c7e20b829fc5679f9b7b298eaa2f1ed8b7e AS core
|
FROM node:22.15.0-alpine3.20@sha256:686b8892b69879ef5bfd6047589666933508f9a5451c67320df3070ba0e9807b AS core
|
||||||
|
|
||||||
WORKDIR /usr/src/open-api/typescript-sdk
|
WORKDIR /usr/src/open-api/typescript-sdk
|
||||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||||
|
|||||||
574
cli/package-lock.json
generated
574
cli/package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.68",
|
"version": "2.2.65",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.68",
|
"version": "2.2.65",
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"chokidar": "^4.0.3",
|
"chokidar": "^4.0.3",
|
||||||
@@ -27,7 +27,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": "^22.15.29",
|
"@types/node": "^22.14.1",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^57.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -54,14 +54,14 @@
|
|||||||
},
|
},
|
||||||
"../open-api/typescript-sdk": {
|
"../open-api/typescript-sdk": {
|
||||||
"name": "@immich/sdk",
|
"name": "@immich/sdk",
|
||||||
"version": "1.134.0",
|
"version": "1.132.3",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "GNU Affero General Public License version 3",
|
"license": "GNU Affero General Public License version 3",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@oazapfts/runtime": "^1.0.2"
|
"@oazapfts/runtime": "^1.0.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^22.14.1",
|
||||||
"typescript": "^5.3.3"
|
"typescript": "^5.3.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -79,6 +79,21 @@
|
|||||||
"node": ">=6.0.0"
|
"node": ">=6.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@babel/code-frame": {
|
||||||
|
"version": "7.26.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||||
|
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
|
"js-tokens": "^4.0.0",
|
||||||
|
"picocolors": "^1.0.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.9.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@babel/helper-string-parser": {
|
"node_modules/@babel/helper-string-parser": {
|
||||||
"version": "7.25.9",
|
"version": "7.25.9",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
|
||||||
@@ -565,9 +580,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint-community/eslint-utils": {
|
"node_modules/@eslint-community/eslint-utils": {
|
||||||
"version": "4.7.0",
|
"version": "4.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.6.0.tgz",
|
||||||
"integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==",
|
"integrity": "sha512-WhCn7Z7TauhBtmzhvKpoQs0Wwb/kBcy4CwpuI0/eEIr2Lx2auxmulAzLr91wVZJaz47iUZdkXOK7WlAfxGKCnA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -632,9 +647,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/core": {
|
"node_modules/@eslint/core": {
|
||||||
"version": "0.14.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
||||||
"integrity": "sha512-qIbV0/JZr7iSDjqAc60IqbLdsj9GDt16xQtWD+B78d/HAlvysGdZZ6rpJHGAc2T0FQx1X6thsSPdnoiGKdNtdg==",
|
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -682,16 +697,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.27.0",
|
"version": "9.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.1.tgz",
|
||||||
"integrity": "sha512-G5JD9Tu5HJEu4z2Uo4aHY2sLV64B7CDMXxFzqzjl3NKd6RVzSXNoE80jk7Y0lJkTTkjiIhBAqmlYwjuBY3tvpA==",
|
"integrity": "sha512-dEIwmjntEx8u3Uvv+kr3PDeeArL8Hw07H9kyYxCjnM9pBjfEhk6uLXSchxxzgiwtRhhzVzqmUSDFBOi1TuZ7qg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://eslint.org/donate"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/object-schema": {
|
"node_modules/@eslint/object-schema": {
|
||||||
@@ -705,13 +717,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/plugin-kit": {
|
"node_modules/@eslint/plugin-kit": {
|
||||||
"version": "0.3.1",
|
"version": "0.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
||||||
"integrity": "sha512-0J+zgWxHN+xXONWIyPWKFMgVuJoZuGiIFu8yxk7RJjxkzpGmyja5wRFqZIVtjDVOQpV+Rw0iOAjYPE2eQyjr0w==",
|
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.13.0",
|
||||||
"levn": "^0.4.1"
|
"levn": "^0.4.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1338,31 +1350,38 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "22.15.30",
|
"version": "22.14.1",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||||
"integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==",
|
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~6.21.0"
|
"undici-types": "~6.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/normalize-package-data": {
|
||||||
|
"version": "2.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||||
|
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.31.0.tgz",
|
||||||
"integrity": "sha512-6u6Plg9nP/J1GRpe/vcjjabo6Uc5YQPAMxsgQyGC/I0RuukiG1wIe3+Vtg3IrSCVJDmqK3j8adrtzXSENRtFgg==",
|
"integrity": "sha512-evaQJZ/J/S4wisevDvC1KFZkPzRetH8kYZbkgcTRyql3mcKsf+ZFDV1BVWUGTCAW5pQHoqn5gK5b8kn7ou9aFQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/regexpp": "^4.10.0",
|
"@eslint-community/regexpp": "^4.10.0",
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.31.0",
|
||||||
"@typescript-eslint/type-utils": "8.32.1",
|
"@typescript-eslint/type-utils": "8.31.0",
|
||||||
"@typescript-eslint/utils": "8.32.1",
|
"@typescript-eslint/utils": "8.31.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/visitor-keys": "8.31.0",
|
||||||
"graphemer": "^1.4.0",
|
"graphemer": "^1.4.0",
|
||||||
"ignore": "^7.0.0",
|
"ignore": "^5.3.1",
|
||||||
"natural-compare": "^1.4.0",
|
"natural-compare": "^1.4.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1377,27 +1396,17 @@
|
|||||||
"typescript": ">=4.8.4 <5.9.0"
|
"typescript": ">=4.8.4 <5.9.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
|
|
||||||
"version": "7.0.4",
|
|
||||||
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.4.tgz",
|
|
||||||
"integrity": "sha512-gJzzk+PQNznz8ysRrC0aOkBNVRBDtE1n53IqyqEf3PXrYwomFs5q4pGMizBMJF+ykh03insJ27hB8gSrD2Hn8A==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@typescript-eslint/parser": {
|
"node_modules/@typescript-eslint/parser": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.31.0.tgz",
|
||||||
"integrity": "sha512-LKMrmwCPoLhM45Z00O1ulb6jwyVr2kr3XJp+G+tSEZcbauNnScewcQwtJqXDhXeYPDEjZ8C1SjXm015CirEmGg==",
|
"integrity": "sha512-67kYYShjBR0jNI5vsf/c3WG4u+zDnCTHTPqVMQguffaWWFs7artgwKmfwdifl+r6XyM5LYLas/dInj2T0SgJyw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.31.0",
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.31.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
"@typescript-eslint/typescript-estree": "8.31.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/visitor-keys": "8.31.0",
|
||||||
"debug": "^4.3.4"
|
"debug": "^4.3.4"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1413,14 +1422,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/scope-manager": {
|
"node_modules/@typescript-eslint/scope-manager": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.31.0.tgz",
|
||||||
"integrity": "sha512-7IsIaIDeZn7kffk7qXC3o6Z4UblZJKV3UBpkvRNpr5NSyLji7tvTcvmnMNYuYLyh26mN8W723xpo3i4MlD33vA==",
|
"integrity": "sha512-knO8UyF78Nt8O/B64i7TlGXod69ko7z6vJD9uhSlm0qkAbGeRUSudcm0+K/4CrRjrpiHfBCjMWlc08Vav1xwcw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.31.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1"
|
"@typescript-eslint/visitor-keys": "8.31.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1431,16 +1440,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/type-utils": {
|
"node_modules/@typescript-eslint/type-utils": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.31.0.tgz",
|
||||||
"integrity": "sha512-mv9YpQGA8iIsl5KyUPi+FGLm7+bA4fgXaeRcFKRDRwDMu4iwrSHeDPipwueNXhdIIZltwCJv+NkxftECbIZWfA==",
|
"integrity": "sha512-DJ1N1GdjI7IS7uRlzJuEDCgDQix3ZVYVtgeWEyhyn4iaoitpMBX6Ndd488mXSx0xah/cONAkEaYyylDyAeHMHg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1",
|
"@typescript-eslint/typescript-estree": "8.31.0",
|
||||||
"@typescript-eslint/utils": "8.32.1",
|
"@typescript-eslint/utils": "8.31.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1455,9 +1464,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/types": {
|
"node_modules/@typescript-eslint/types": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.31.0.tgz",
|
||||||
"integrity": "sha512-YmybwXUJcgGqgAp6bEsgpPXEg6dcCyPyCSr0CAAueacR/CCBi25G3V8gGQ2kRzQRBNol7VQknxMs9HvVa9Rvfg==",
|
"integrity": "sha512-Ch8oSjVyYyJxPQk8pMiP2FFGYatqXQfQIaMp+TpuuLlDachRWpUAeEu1u9B/v/8LToehUIWyiKcA/w5hUFRKuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1469,20 +1478,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree": {
|
"node_modules/@typescript-eslint/typescript-estree": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.31.0.tgz",
|
||||||
"integrity": "sha512-Y3AP9EIfYwBb4kWGb+simvPaqQoT5oJuzzj9m0i6FCY6SPvlomY2Ei4UEMm7+FXtlNJbor80ximyslzaQF6xhg==",
|
"integrity": "sha512-xLmgn4Yl46xi6aDSZ9KkyfhhtnYI15/CvHbpOy/eR5NWhK/BK8wc709KKwhAR0m4ZKRP7h07bm4BWUYOCuRpQQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.31.0",
|
||||||
"@typescript-eslint/visitor-keys": "8.32.1",
|
"@typescript-eslint/visitor-keys": "8.31.0",
|
||||||
"debug": "^4.3.4",
|
"debug": "^4.3.4",
|
||||||
"fast-glob": "^3.3.2",
|
"fast-glob": "^3.3.2",
|
||||||
"is-glob": "^4.0.3",
|
"is-glob": "^4.0.3",
|
||||||
"minimatch": "^9.0.4",
|
"minimatch": "^9.0.4",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"ts-api-utils": "^2.1.0"
|
"ts-api-utils": "^2.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1522,16 +1531,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/utils": {
|
"node_modules/@typescript-eslint/utils": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.31.0.tgz",
|
||||||
"integrity": "sha512-DsSFNIgLSrc89gpq1LJB7Hm1YpuhK086DRDJSNrewcGvYloWW1vZLHBTIvarKZDcAORIy/uWNx8Gad+4oMpkSA==",
|
"integrity": "sha512-qi6uPLt9cjTFxAb1zGNgTob4x9ur7xC6mHQJ8GwEzGMGE9tYniublmJaowOJ9V2jUzxrltTPfdG2nKlWsq0+Ww==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.7.0",
|
"@eslint-community/eslint-utils": "^4.4.0",
|
||||||
"@typescript-eslint/scope-manager": "8.32.1",
|
"@typescript-eslint/scope-manager": "8.31.0",
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.31.0",
|
||||||
"@typescript-eslint/typescript-estree": "8.32.1"
|
"@typescript-eslint/typescript-estree": "8.31.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -1546,13 +1555,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/visitor-keys": {
|
"node_modules/@typescript-eslint/visitor-keys": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.31.0.tgz",
|
||||||
"integrity": "sha512-ar0tjQfObzhSaW3C3QNmTc5ofj0hDoNQ5XWrCy6zDyabdr0TWhCkClp+rywGNj/odAFBVzzJrK4tEq5M4Hmu4w==",
|
"integrity": "sha512-QcGHmlRHWOl93o64ZUMNewCdwKGU6WItOU52H0djgNmn1EOrhVudrDzXz4OycCRSCPwFCDrE2iIt5vmuUdHxuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/types": "8.32.1",
|
"@typescript-eslint/types": "8.31.0",
|
||||||
"eslint-visitor-keys": "^4.2.0"
|
"eslint-visitor-keys": "^4.2.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -1564,9 +1573,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/coverage-v8": {
|
"node_modules/@vitest/coverage-v8": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.1.2.tgz",
|
||||||
"integrity": "sha512-G4p6OtioySL+hPV7Y6JHlhpsODbJzt1ndwHAFkyk6vVjpK03PFsKnauZIzcd0PrK4zAbc5lc+jeZ+eNGiMA+iw==",
|
"integrity": "sha512-XDdaDOeaTMAMYW7N63AqoK32sYUWbXnTkC6tEbVcu3RlU1bB9of32T+PGf8KZvxqLNqeXhafDFqCkwpf2+dyaQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1587,8 +1596,8 @@
|
|||||||
"url": "https://opencollective.com/vitest"
|
"url": "https://opencollective.com/vitest"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"@vitest/browser": "3.1.4",
|
"@vitest/browser": "3.1.2",
|
||||||
"vitest": "3.1.4"
|
"vitest": "3.1.2"
|
||||||
},
|
},
|
||||||
"peerDependenciesMeta": {
|
"peerDependenciesMeta": {
|
||||||
"@vitest/browser": {
|
"@vitest/browser": {
|
||||||
@@ -1597,14 +1606,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/expect": {
|
"node_modules/@vitest/expect": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.1.2.tgz",
|
||||||
"integrity": "sha512-xkD/ljeliyaClDYqHPNCiJ0plY5YIcM0OlRiZizLhlPmpXWpxnGMyTZXOHFhFeG7w9P5PBeL4IdtJ/HeQwTbQA==",
|
"integrity": "sha512-O8hJgr+zREopCAqWl3uCVaOdqJwZ9qaDwUP7vy3Xigad0phZe9APxKhPcDNqYYi0rX5oMvwJMSCAXY2afqeTSA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.1.4",
|
"@vitest/spy": "3.1.2",
|
||||||
"@vitest/utils": "3.1.4",
|
"@vitest/utils": "3.1.2",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1613,13 +1622,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/mocker": {
|
"node_modules/@vitest/mocker": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.1.2.tgz",
|
||||||
"integrity": "sha512-8IJ3CvwtSw/EFXqWFL8aCMu+YyYXG2WUSrQbViOZkWTKTVicVwZ/YiEZDSqD00kX+v/+W+OnxhNWoeVKorHygA==",
|
"integrity": "sha512-kOtd6K2lc7SQ0mBqYv/wdGedlqPdM/B38paPY+OwJ1XiNi44w3Fpog82UfOibmHaV9Wod18A09I9SCKLyDMqgw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/spy": "3.1.4",
|
"@vitest/spy": "3.1.2",
|
||||||
"estree-walker": "^3.0.3",
|
"estree-walker": "^3.0.3",
|
||||||
"magic-string": "^0.30.17"
|
"magic-string": "^0.30.17"
|
||||||
},
|
},
|
||||||
@@ -1640,9 +1649,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/pretty-format": {
|
"node_modules/@vitest/pretty-format": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.1.2.tgz",
|
||||||
"integrity": "sha512-cqv9H9GvAEoTaoq+cYqUTCGscUjKqlJZC7PRwY5FMySVj5J+xOm1KQcCiYHJOEzOKRUhLH4R2pTwvFlWCEScsg==",
|
"integrity": "sha512-R0xAiHuWeDjTSB3kQ3OQpT8Rx3yhdOAIm/JM4axXxnG7Q/fS8XUwggv/A4xzbQA+drYRjzkMnpYnOGAc4oeq8w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1653,13 +1662,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/runner": {
|
"node_modules/@vitest/runner": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.1.2.tgz",
|
||||||
"integrity": "sha512-djTeF1/vt985I/wpKVFBMWUlk/I7mb5hmD5oP8K9ACRmVXgKTae3TUOtXAEBfslNKPzUQvnKhNd34nnRSYgLNQ==",
|
"integrity": "sha512-bhLib9l4xb4sUMPXnThbnhX2Yi8OutBMA8Yahxa7yavQsFDtwY/jrUZwpKp2XH9DhRFJIeytlyGpXCqZ65nR+g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/utils": "3.1.4",
|
"@vitest/utils": "3.1.2",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
@@ -1667,13 +1676,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/snapshot": {
|
"node_modules/@vitest/snapshot": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.1.2.tgz",
|
||||||
"integrity": "sha512-JPHf68DvuO7vilmvwdPr9TS0SuuIzHvxeaCkxYcCD4jTk67XwL45ZhEHFKIuCm8CYstgI6LZ4XbwD6ANrwMpFg==",
|
"integrity": "sha512-Q1qkpazSF/p4ApZg1vfZSQ5Yw6OCQxVMVrLjslbLFA1hMDrT2uxtqMaw8Tc/jy5DLka1sNs1Y7rBcftMiaSH/Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.1.4",
|
"@vitest/pretty-format": "3.1.2",
|
||||||
"magic-string": "^0.30.17",
|
"magic-string": "^0.30.17",
|
||||||
"pathe": "^2.0.3"
|
"pathe": "^2.0.3"
|
||||||
},
|
},
|
||||||
@@ -1682,9 +1691,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/spy": {
|
"node_modules/@vitest/spy": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.1.2.tgz",
|
||||||
"integrity": "sha512-Xg1bXhu+vtPXIodYN369M86K8shGLouNjoVI78g8iAq2rFoHFdajNvJJ5A/9bPMFcfQqdaCpOgWKEoMQg/s0Yg==",
|
"integrity": "sha512-OEc5fSXMws6sHVe4kOFyDSj/+4MSwst0ib4un0DlcYgQvRuYQ0+M2HyqGaauUMnjq87tmUaMNDxKQx7wNfVqPA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1695,13 +1704,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@vitest/utils": {
|
"node_modules/@vitest/utils": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.1.2.tgz",
|
||||||
"integrity": "sha512-yriMuO1cfFhmiGc8ataN51+9ooHRuURdfAZfwFd3usWynjzpLslZdYnRegTv32qdgtJTsj15FoeZe2g15fY1gg==",
|
"integrity": "sha512-5GGd0ytZ7BH3H6JTj9Kw7Prn1Nbg0wZVrIvou+UWxm54d+WoXXgAgjFJ8wn3LdagWLFSEfpPeyYrByZaGEZHLg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/pretty-format": "3.1.4",
|
"@vitest/pretty-format": "3.1.2",
|
||||||
"loupe": "^3.1.3",
|
"loupe": "^3.1.3",
|
||||||
"tinyrainbow": "^2.0.0"
|
"tinyrainbow": "^2.0.0"
|
||||||
},
|
},
|
||||||
@@ -1856,9 +1865,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/builtin-modules": {
|
"node_modules/builtin-modules": {
|
||||||
"version": "5.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-4.0.0.tgz",
|
||||||
"integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==",
|
"integrity": "sha512-p1n8zyCkt1BVrKNFymOHjcDSAl7oq/gUvfgULv2EblgpPVQlQr9yHnWjg9IJ2MhfwPqiYqMMrr01OY7yQoK2yA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2232,9 +2241,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.27.0",
|
"version": "9.25.1",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.27.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.25.1.tgz",
|
||||||
"integrity": "sha512-ixRawFQuMB9DZ7fjU3iGGganFDp3+45bPOdaRurcFHSXO1e/sYwUX/FtQZpLZJR6SjMoJH8hR2pPEAfDyCoU2Q==",
|
"integrity": "sha512-E6Mtz9oGQWDCpV12319d59n4tx9zOTXSTmc8BLVxBx+G/0RdM5MvEEJLU9c0+aleoePYYgVTOsRblx433qmhWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2242,10 +2251,10 @@
|
|||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
"@eslint/config-array": "^0.20.0",
|
"@eslint/config-array": "^0.20.0",
|
||||||
"@eslint/config-helpers": "^0.2.1",
|
"@eslint/config-helpers": "^0.2.1",
|
||||||
"@eslint/core": "^0.14.0",
|
"@eslint/core": "^0.13.0",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.27.0",
|
"@eslint/js": "9.25.1",
|
||||||
"@eslint/plugin-kit": "^0.3.1",
|
"@eslint/plugin-kit": "^0.2.8",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
"@humanwhocodes/retry": "^0.4.2",
|
"@humanwhocodes/retry": "^0.4.2",
|
||||||
@@ -2293,25 +2302,22 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-config-prettier": {
|
"node_modules/eslint-config-prettier": {
|
||||||
"version": "10.1.5",
|
"version": "10.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.2.tgz",
|
||||||
"integrity": "sha512-zc1UmCpNltmVY34vuLRV61r1K27sWuX39E+uyUnY8xS2Bex88VV9cugG+UZbRSRGtGyFboj+D8JODyme1plMpw==",
|
"integrity": "sha512-Epgp/EofAUeEpIdZkW60MHKvPyru1ruQJxPL+WIycnaPApuseK0Zpkrh/FwL9oIpQvIhJwV7ptOy0DWUjTlCiA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"eslint-config-prettier": "bin/cli.js"
|
"eslint-config-prettier": "bin/cli.js"
|
||||||
},
|
},
|
||||||
"funding": {
|
|
||||||
"url": "https://opencollective.com/eslint-config-prettier"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=7.0.0"
|
"eslint": ">=7.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-prettier": {
|
"node_modules/eslint-plugin-prettier": {
|
||||||
"version": "5.4.0",
|
"version": "5.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.6.tgz",
|
||||||
"integrity": "sha512-BvQOvUhkVQM1i63iMETK9Hjud9QhqBnbtT1Zc642p9ynzBuCe5pybkOnvqZIBypXmMlsGcnU4HZ8sCTPfpAexA==",
|
"integrity": "sha512-mUcf7QG2Tjk7H055Jk0lGBjbgDnfrvqjhXh9t2xLMSCjZVcw9Rb1V6sVNXO0th3jgeO7zllWPTNRil3JW94TnQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -2340,65 +2346,50 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn": {
|
"node_modules/eslint-plugin-unicorn": {
|
||||||
"version": "59.0.1",
|
"version": "57.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-59.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-57.0.0.tgz",
|
||||||
"integrity": "sha512-EtNXYuWPUmkgSU2E7Ttn57LbRREQesIP1BiLn7OZLKodopKfDXfBUkC/0j6mpw2JExwf43Uf3qLSvrSvppgy8Q==",
|
"integrity": "sha512-zUYYa6zfNdTeG9BISWDlcLmz16c+2Ck2o5ZDHh0UzXJz3DEP7xjmlVDTzbyV0W+XksgZ0q37WEWzN2D2Ze+g9Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/helper-validator-identifier": "^7.25.9",
|
"@babel/helper-validator-identifier": "^7.25.9",
|
||||||
"@eslint-community/eslint-utils": "^4.5.1",
|
"@eslint-community/eslint-utils": "^4.4.1",
|
||||||
"@eslint/plugin-kit": "^0.2.7",
|
"ci-info": "^4.1.0",
|
||||||
"ci-info": "^4.2.0",
|
|
||||||
"clean-regexp": "^1.0.0",
|
"clean-regexp": "^1.0.0",
|
||||||
"core-js-compat": "^3.41.0",
|
"core-js-compat": "^3.40.0",
|
||||||
"esquery": "^1.6.0",
|
"esquery": "^1.6.0",
|
||||||
"find-up-simple": "^1.0.1",
|
"globals": "^15.15.0",
|
||||||
"globals": "^16.0.0",
|
|
||||||
"indent-string": "^5.0.0",
|
"indent-string": "^5.0.0",
|
||||||
"is-builtin-module": "^5.0.0",
|
"is-builtin-module": "^4.0.0",
|
||||||
"jsesc": "^3.1.0",
|
"jsesc": "^3.1.0",
|
||||||
"pluralize": "^8.0.0",
|
"pluralize": "^8.0.0",
|
||||||
|
"read-package-up": "^11.0.0",
|
||||||
"regexp-tree": "^0.1.27",
|
"regexp-tree": "^0.1.27",
|
||||||
"regjsparser": "^0.12.0",
|
"regjsparser": "^0.12.0",
|
||||||
"semver": "^7.7.1",
|
"semver": "^7.7.1",
|
||||||
"strip-indent": "^4.0.0"
|
"strip-indent": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.20.0 || ^20.10.0 || >=21.0.0"
|
"node": ">=18.18"
|
||||||
},
|
},
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||||
},
|
},
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
"eslint": ">=9.22.0"
|
"eslint": ">=9.20.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/core": {
|
"node_modules/eslint-plugin-unicorn/node_modules/globals": {
|
||||||
"version": "0.13.0",
|
"version": "15.15.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-15.15.0.tgz",
|
||||||
"integrity": "sha512-yfkgDw1KR66rkT5A8ci4irzDysN7FRpq3ttJolR88OqQikAWqwA8j5VZyas+vjyBNFIJ7MfybJ9plMILI2UrCw==",
|
"integrity": "sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "MIT",
|
||||||
"dependencies": {
|
|
||||||
"@types/json-schema": "^7.0.15"
|
|
||||||
},
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": ">=18"
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/eslint-plugin-unicorn/node_modules/@eslint/plugin-kit": {
|
|
||||||
"version": "0.2.8",
|
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.8.tgz",
|
|
||||||
"integrity": "sha512-ZAoA40rNMPwSm+AeHpCq8STiNAwzWLJuP8Xv4CHIc9wv/PSuExjMrmjfYNj682vW0OOiZ1HKxzvjQr9XZIisQA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "Apache-2.0",
|
|
||||||
"dependencies": {
|
|
||||||
"@eslint/core": "^0.13.0",
|
|
||||||
"levn": "^0.4.1"
|
|
||||||
},
|
},
|
||||||
"engines": {
|
"funding": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint-scope": {
|
"node_modules/eslint-scope": {
|
||||||
@@ -2749,9 +2740,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/globals": {
|
"node_modules/globals": {
|
||||||
"version": "16.1.0",
|
"version": "16.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/globals/-/globals-16.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/globals/-/globals-16.0.0.tgz",
|
||||||
"integrity": "sha512-aibexHNbb/jiUSObBgpHLj+sIuUmJnYcgXBlrfsiDZ9rt4aF2TFRbyLgZ2iFQuVZ1K5Mx3FVkbKRSgKrbK3K2g==",
|
"integrity": "sha512-iInW14XItCXET01CQFqudPOWP2jYMl7T+QRQT+UNcR/iQncN/F0UNpgd76iFkBPgNQb4+X3LV9tLJYzwh+Gl3A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -2785,6 +2776,19 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/hosted-git-info": {
|
||||||
|
"version": "7.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
|
||||||
|
"integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC",
|
||||||
|
"dependencies": {
|
||||||
|
"lru-cache": "^10.0.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/html-escaper": {
|
"node_modules/html-escaper": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||||
@@ -2842,14 +2846,27 @@
|
|||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/index-to-position": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XPdx9Dq4t9Qk1mTMbWONJqU7boCoumEH7fRET37HX5+khDUl3J2W6PdALxhILYlIYx2amlwYcRPp28p0tSiojg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/is-builtin-module": {
|
"node_modules/is-builtin-module": {
|
||||||
"version": "5.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-4.0.0.tgz",
|
||||||
"integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==",
|
"integrity": "sha512-rWP3AMAalQSesXO8gleROyL2iKU73SX5Er66losQn9rWOWL4Gef0a/xOEOVqjWGMuR2vHG3FJ8UUmT700O8oFg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"builtin-modules": "^5.0.0"
|
"builtin-modules": "^4.0.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.20"
|
"node": ">=18.20"
|
||||||
@@ -2975,6 +2992,13 @@
|
|||||||
"@pkgjs/parseargs": "^0.11.0"
|
"@pkgjs/parseargs": "^0.11.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/js-tokens": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||||
|
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/js-yaml": {
|
"node_modules/js-yaml": {
|
||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
|
||||||
@@ -3232,6 +3256,21 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/normalize-package-data": {
|
||||||
|
"version": "6.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz",
|
||||||
|
"integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "BSD-2-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"hosted-git-info": "^7.0.0",
|
||||||
|
"semver": "^7.3.5",
|
||||||
|
"validate-npm-package-license": "^3.0.4"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": "^16.14.0 || >=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/optionator": {
|
"node_modules/optionator": {
|
||||||
"version": "0.9.4",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
|
||||||
@@ -3302,6 +3341,24 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/parse-json": {
|
||||||
|
"version": "8.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz",
|
||||||
|
"integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@babel/code-frame": "^7.26.2",
|
||||||
|
"index-to-position": "^1.1.0",
|
||||||
|
"type-fest": "^4.39.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-exists": {
|
"node_modules/path-exists": {
|
||||||
"version": "4.0.0",
|
"version": "4.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
|
||||||
@@ -3500,6 +3557,44 @@
|
|||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/read-package-up": {
|
||||||
|
"version": "11.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz",
|
||||||
|
"integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"find-up-simple": "^1.0.0",
|
||||||
|
"read-pkg": "^9.0.0",
|
||||||
|
"type-fest": "^4.6.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/read-pkg": {
|
||||||
|
"version": "9.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz",
|
||||||
|
"integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/normalize-package-data": "^2.4.3",
|
||||||
|
"normalize-package-data": "^6.0.0",
|
||||||
|
"parse-json": "^8.0.0",
|
||||||
|
"type-fest": "^4.6.0",
|
||||||
|
"unicorn-magic": "^0.1.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/readdirp": {
|
"node_modules/readdirp": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
||||||
@@ -3698,6 +3793,42 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/spdx-correct": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"spdx-expression-parse": "^3.0.0",
|
||||||
|
"spdx-license-ids": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/spdx-exceptions": {
|
||||||
|
"version": "2.5.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz",
|
||||||
|
"integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "CC-BY-3.0"
|
||||||
|
},
|
||||||
|
"node_modules/spdx-expression-parse": {
|
||||||
|
"version": "3.0.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||||
|
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"spdx-exceptions": "^2.1.0",
|
||||||
|
"spdx-license-ids": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/spdx-license-ids": {
|
||||||
|
"version": "3.0.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz",
|
||||||
|
"integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "CC0-1.0"
|
||||||
|
},
|
||||||
"node_modules/stackback": {
|
"node_modules/stackback": {
|
||||||
"version": "0.0.2",
|
"version": "0.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
|
||||||
@@ -4025,6 +4156,19 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/type-fest": {
|
||||||
|
"version": "4.40.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.40.0.tgz",
|
||||||
|
"integrity": "sha512-ABHZ2/tS2JkvH1PEjxFDTUWC8dB5OsIGZP4IFLhR293GqT5Y5qB1WwL2kMPYhQW9DVgVD8Hd7I8gjwPIf5GFkw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "(MIT OR CC0-1.0)",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=16"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/typescript": {
|
"node_modules/typescript": {
|
||||||
"version": "5.8.3",
|
"version": "5.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
|
||||||
@@ -4040,15 +4184,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/typescript-eslint": {
|
"node_modules/typescript-eslint": {
|
||||||
"version": "8.32.1",
|
"version": "8.31.0",
|
||||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.32.1.tgz",
|
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.31.0.tgz",
|
||||||
"integrity": "sha512-D7el+eaDHAmXvrZBy1zpzSNIRqnCOrkwTgZxTu3MUqRWk8k0q9m9Ho4+vPf7iHtgUfrK/o8IZaEApsxPlHTFCg==",
|
"integrity": "sha512-u+93F0sB0An8WEAPtwxVhFby573E8ckdjwUUQUj9QA4v8JAvgtoDdIyYR3XFwFHq2W1KJ1AurwJCO+w+Y1ixyQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "8.32.1",
|
"@typescript-eslint/eslint-plugin": "8.31.0",
|
||||||
"@typescript-eslint/parser": "8.32.1",
|
"@typescript-eslint/parser": "8.31.0",
|
||||||
"@typescript-eslint/utils": "8.32.1"
|
"@typescript-eslint/utils": "8.31.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
@@ -4069,6 +4213,19 @@
|
|||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/unicorn-magic": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
|
||||||
|
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/update-browserslist-db": {
|
"node_modules/update-browserslist-db": {
|
||||||
"version": "1.1.3",
|
"version": "1.1.3",
|
||||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz",
|
||||||
@@ -4110,10 +4267,21 @@
|
|||||||
"punycode": "^2.1.0"
|
"punycode": "^2.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/validate-npm-package-license": {
|
||||||
|
"version": "3.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||||
|
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"spdx-correct": "^3.0.0",
|
||||||
|
"spdx-expression-parse": "^3.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.3.5",
|
"version": "6.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.3.tgz",
|
||||||
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
|
"integrity": "sha512-5nXH+QsELbFKhsEfWLkHrvgRpTdGJzqOZ+utSdmPTvwHmvU6ITTm3xx+mRusihkcI8GeC7lCDyn3kDtiki9scw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4186,15 +4354,15 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite-node": {
|
"node_modules/vite-node": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.1.2.tgz",
|
||||||
"integrity": "sha512-6enNwYnpyDo4hEgytbmc6mYWHXDHYEn0D1/rw4Q+tnHUGtKTJsn8T1YkX6Q18wI5LCrS8CTYlBaiCqxOy2kvUA==",
|
"integrity": "sha512-/8iMryv46J3aK13iUXsei5G/A3CUlW4665THCPS+K8xAaqrVWiGB4RfXMQXCLjpK9P2eK//BczrVkn5JLAk6DA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"es-module-lexer": "^1.7.0",
|
"es-module-lexer": "^1.6.0",
|
||||||
"pathe": "^2.0.3",
|
"pathe": "^2.0.3",
|
||||||
"vite": "^5.0.0 || ^6.0.0"
|
"vite": "^5.0.0 || ^6.0.0"
|
||||||
},
|
},
|
||||||
@@ -4257,19 +4425,19 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vitest": {
|
"node_modules/vitest": {
|
||||||
"version": "3.1.4",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.4.tgz",
|
"resolved": "https://registry.npmjs.org/vitest/-/vitest-3.1.2.tgz",
|
||||||
"integrity": "sha512-Ta56rT7uWxCSJXlBtKgIlApJnT6e6IGmTYxYcmxjJ4ujuZDI59GUQgVDObXXJujOmPDBYXHK1qmaGtneu6TNIQ==",
|
"integrity": "sha512-WaxpJe092ID1C0mr+LH9MmNrhfzi8I65EX/NRU/Ld016KqQNRgxSOlGNP1hHN+a/F8L15Mh8klwaF77zR3GeDQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@vitest/expect": "3.1.4",
|
"@vitest/expect": "3.1.2",
|
||||||
"@vitest/mocker": "3.1.4",
|
"@vitest/mocker": "3.1.2",
|
||||||
"@vitest/pretty-format": "^3.1.4",
|
"@vitest/pretty-format": "^3.1.2",
|
||||||
"@vitest/runner": "3.1.4",
|
"@vitest/runner": "3.1.2",
|
||||||
"@vitest/snapshot": "3.1.4",
|
"@vitest/snapshot": "3.1.2",
|
||||||
"@vitest/spy": "3.1.4",
|
"@vitest/spy": "3.1.2",
|
||||||
"@vitest/utils": "3.1.4",
|
"@vitest/utils": "3.1.2",
|
||||||
"chai": "^5.2.0",
|
"chai": "^5.2.0",
|
||||||
"debug": "^4.4.0",
|
"debug": "^4.4.0",
|
||||||
"expect-type": "^1.2.1",
|
"expect-type": "^1.2.1",
|
||||||
@@ -4282,7 +4450,7 @@
|
|||||||
"tinypool": "^1.0.2",
|
"tinypool": "^1.0.2",
|
||||||
"tinyrainbow": "^2.0.0",
|
"tinyrainbow": "^2.0.0",
|
||||||
"vite": "^5.0.0 || ^6.0.0",
|
"vite": "^5.0.0 || ^6.0.0",
|
||||||
"vite-node": "3.1.4",
|
"vite-node": "3.1.2",
|
||||||
"why-is-node-running": "^2.3.0"
|
"why-is-node-running": "^2.3.0"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
@@ -4298,8 +4466,8 @@
|
|||||||
"@edge-runtime/vm": "*",
|
"@edge-runtime/vm": "*",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
|
||||||
"@vitest/browser": "3.1.4",
|
"@vitest/browser": "3.1.2",
|
||||||
"@vitest/ui": "3.1.4",
|
"@vitest/ui": "3.1.2",
|
||||||
"happy-dom": "*",
|
"happy-dom": "*",
|
||||||
"jsdom": "*"
|
"jsdom": "*"
|
||||||
},
|
},
|
||||||
@@ -4488,16 +4656,16 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.0",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
|
||||||
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
|
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
"yaml": "bin.mjs"
|
"yaml": "bin.mjs"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">= 14.6"
|
"node": ">= 14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/yocto-queue": {
|
"node_modules/yocto-queue": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@immich/cli",
|
"name": "@immich/cli",
|
||||||
"version": "2.2.68",
|
"version": "2.2.65",
|
||||||
"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",
|
||||||
@@ -21,7 +21,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": "^22.15.29",
|
"@types/node": "^22.14.1",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"byte-size": "^9.0.0",
|
"byte-size": "^9.0.0",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^57.0.0",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"mock-fs": "^5.2.0",
|
"mock-fs": "^5.2.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -69,6 +69,6 @@
|
|||||||
"micromatch": "^4.0.8"
|
"micromatch": "^4.0.8"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,7 +43,6 @@ export interface UploadOptionsDto {
|
|||||||
concurrency: number;
|
concurrency: number;
|
||||||
progress?: boolean;
|
progress?: boolean;
|
||||||
watch?: boolean;
|
watch?: boolean;
|
||||||
jsonOutput?: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class UploadFile extends File {
|
class UploadFile extends File {
|
||||||
@@ -66,9 +65,6 @@ class UploadFile extends File {
|
|||||||
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
|
const uploadBatch = async (files: string[], options: UploadOptionsDto) => {
|
||||||
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
const { newFiles, duplicates } = await checkForDuplicates(files, options);
|
||||||
const newAssets = await uploadFiles(newFiles, options);
|
const newAssets = await uploadFiles(newFiles, options);
|
||||||
if (options.jsonOutput) {
|
|
||||||
console.log(JSON.stringify({ newFiles, duplicates, newAssets }, undefined, 4));
|
|
||||||
}
|
|
||||||
await updateAlbums([...newAssets, ...duplicates], options);
|
await updateAlbums([...newAssets, ...duplicates], options);
|
||||||
await deleteFiles(newFiles, options);
|
await deleteFiles(newFiles, options);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -68,11 +68,6 @@ program
|
|||||||
.env('IMMICH_UPLOAD_CONCURRENCY')
|
.env('IMMICH_UPLOAD_CONCURRENCY')
|
||||||
.default(4),
|
.default(4),
|
||||||
)
|
)
|
||||||
.addOption(
|
|
||||||
new Option('-j, --json-output', 'Output detailed information in json format')
|
|
||||||
.env('IMMICH_JSON_OUTPUT')
|
|
||||||
.default(false),
|
|
||||||
)
|
|
||||||
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
.addOption(new Option('--delete', 'Delete local assets after upload').env('IMMICH_DELETE_ASSETS'))
|
||||||
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
|
.addOption(new Option('--no-progress', 'Hide progress bars').env('IMMICH_PROGRESS_BAR').default(true))
|
||||||
.addOption(
|
.addOption(
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ name: immich-dev
|
|||||||
services:
|
services:
|
||||||
immich-server:
|
immich-server:
|
||||||
container_name: immich_server
|
container_name: immich_server
|
||||||
command: [ '/usr/src/app/bin/immich-dev' ]
|
command: ['/usr/src/app/bin/immich-dev']
|
||||||
image: immich-server-dev:latest
|
image: immich-server-dev:latest
|
||||||
# extends:
|
# extends:
|
||||||
# file: hwaccel.transcoding.yml
|
# file: hwaccel.transcoding.yml
|
||||||
@@ -33,6 +33,7 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
- ${UPLOAD_LOCATION}/photos/upload:/usr/src/app/upload/upload
|
||||||
- /usr/src/app/node_modules
|
- /usr/src/app/node_modules
|
||||||
- /etc/localtime:/etc/localtime:ro
|
- /etc/localtime:/etc/localtime:ro
|
||||||
|
- ../flickr30k-images:/flickr30k:ro
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -48,7 +49,7 @@ services:
|
|||||||
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
|
IMMICH_THIRD_PARTY_SOURCE_URL: https://github.com/immich-app/immich/
|
||||||
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
IMMICH_THIRD_PARTY_BUG_FEATURE_URL: https://github.com/immich-app/immich/issues
|
||||||
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
|
IMMICH_THIRD_PARTY_DOCUMENTATION_URL: https://immich.app/docs
|
||||||
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/community-guides
|
IMMICH_THIRD_PARTY_SUPPORT_URL: https://immich.app/docs/third-party
|
||||||
ulimits:
|
ulimits:
|
||||||
nofile:
|
nofile:
|
||||||
soft: 1048576
|
soft: 1048576
|
||||||
@@ -58,7 +59,6 @@ services:
|
|||||||
- 9231:9231
|
- 9231:9231
|
||||||
- 2283:2283
|
- 2283:2283
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
|
||||||
- database
|
- database
|
||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
@@ -70,7 +70,7 @@ services:
|
|||||||
# user: 0:0
|
# user: 0:0
|
||||||
build:
|
build:
|
||||||
context: ../web
|
context: ../web
|
||||||
command: [ '/usr/src/app/bin/immich-web' ]
|
command: ['/usr/src/app/bin/immich-web']
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
ports:
|
ports:
|
||||||
@@ -114,15 +114,9 @@ services:
|
|||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
|
|
||||||
healthcheck:
|
|
||||||
test: redis-cli ping || exit 1
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -134,26 +128,45 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
healthcheck:
|
||||||
|
test: >-
|
||||||
|
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1;
|
||||||
|
Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align
|
||||||
|
--command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')";
|
||||||
|
echo "checksum failure count is $$Chksum";
|
||||||
|
[ "$$Chksum" = '0' ] || exit 1
|
||||||
|
interval: 5m
|
||||||
|
start_interval: 30s
|
||||||
|
start_period: 5m
|
||||||
|
command: >-
|
||||||
|
postgres
|
||||||
|
-c shared_preload_libraries=vectors.so
|
||||||
|
-c 'search_path="$$user", public, vectors'
|
||||||
|
-c logging_collector=on
|
||||||
|
-c max_wal_size=2GB
|
||||||
|
-c shared_buffers=512MB
|
||||||
|
-c wal_compression=on
|
||||||
|
|
||||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||||
# immich-prometheus:
|
immich-prometheus:
|
||||||
# container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
# ports:
|
ports:
|
||||||
# - 9090:9090
|
- 9090:9090
|
||||||
# image: prom/prometheus
|
image: prom/prometheus
|
||||||
# volumes:
|
volumes:
|
||||||
# - ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
# - prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
|
|
||||||
# first login uses admin/admin
|
# first login uses admin/admin
|
||||||
# add data source for http://immich-prometheus:9090 to get started
|
# add data source for http://immich-prometheus:9090 to get started
|
||||||
# immich-grafana:
|
immich-grafana:
|
||||||
# container_name: immich_grafana
|
container_name: immich_grafana
|
||||||
# command: ['./run.sh', '-disable-reporting']
|
command: ['./run.sh', '-disable-reporting']
|
||||||
# ports:
|
ports:
|
||||||
# - 3000:3000
|
- 3001:3000
|
||||||
# image: grafana/grafana:10.3.3-ubuntu
|
image: grafana/grafana:10.3.3-ubuntu
|
||||||
# volumes:
|
volumes:
|
||||||
# - grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
model-cache:
|
model-cache:
|
||||||
|
|||||||
@@ -27,7 +27,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- 2283:2283
|
- 2283:2283
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
|
||||||
- database
|
- database
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -54,16 +53,9 @@ services:
|
|||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
|
|
||||||
healthcheck:
|
|
||||||
test: redis-cli ping || exit 1
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
environment:
|
environment:
|
||||||
@@ -75,6 +67,14 @@ services:
|
|||||||
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
- ${UPLOAD_LOCATION}/postgres:/var/lib/postgresql/data
|
||||||
ports:
|
ports:
|
||||||
- 5432:5432
|
- 5432:5432
|
||||||
|
healthcheck:
|
||||||
|
test: >-
|
||||||
|
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
|
interval: 5m
|
||||||
|
start_interval: 30s
|
||||||
|
start_period: 5m
|
||||||
|
command: >-
|
||||||
|
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
# set IMMICH_TELEMETRY_INCLUDE=all in .env to enable metrics
|
||||||
@@ -82,7 +82,7 @@ services:
|
|||||||
container_name: immich_prometheus
|
container_name: immich_prometheus
|
||||||
ports:
|
ports:
|
||||||
- 9090:9090
|
- 9090:9090
|
||||||
image: prom/prometheus@sha256:9abc6cf6aea7710d163dbb28d8eeb7dc5baef01e38fa4cd146a406dd9f07f70d
|
image: prom/prometheus@sha256:339ce86a59413be18d0e445472891d022725b4803fab609069110205e79fb2f1
|
||||||
volumes:
|
volumes:
|
||||||
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
- ./prometheus.yml:/etc/prometheus/prometheus.yml
|
||||||
- prometheus-data:/prometheus
|
- prometheus-data:/prometheus
|
||||||
@@ -94,7 +94,7 @@ services:
|
|||||||
command: [ './run.sh', '-disable-reporting' ]
|
command: [ './run.sh', '-disable-reporting' ]
|
||||||
ports:
|
ports:
|
||||||
- 3000:3000
|
- 3000:3000
|
||||||
image: grafana/grafana:12.0.1-ubuntu@sha256:65575bb9c761335e2ff30e364f21d38632e3b2e75f5f81d83cc92f44b9bbc055
|
image: grafana/grafana:11.6.1-ubuntu@sha256:6fc273288470ef499dd3c6b36aeade093170d4f608f864c5dd3a7fabeae77b50
|
||||||
volumes:
|
volumes:
|
||||||
- grafana-data:/var/lib/grafana
|
- grafana-data:/var/lib/grafana
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ services:
|
|||||||
ports:
|
ports:
|
||||||
- '2283:2283'
|
- '2283:2283'
|
||||||
depends_on:
|
depends_on:
|
||||||
- redis
|
|
||||||
- database
|
- database
|
||||||
restart: always
|
restart: always
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -47,26 +46,25 @@ services:
|
|||||||
healthcheck:
|
healthcheck:
|
||||||
disable: false
|
disable: false
|
||||||
|
|
||||||
redis:
|
|
||||||
container_name: immich_redis
|
|
||||||
image: docker.io/valkey/valkey:8-bookworm@sha256:a19bebed6a91bd5e6e2106fef015f9602a3392deeb7c9ed47548378dcee3dfc2
|
|
||||||
healthcheck:
|
|
||||||
test: redis-cli ping || exit 1
|
|
||||||
restart: always
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
container_name: immich_postgres
|
container_name: immich_postgres
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.1-pgvectors0.2.0
|
image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
POSTGRES_USER: ${DB_USERNAME}
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
POSTGRES_DB: ${DB_DATABASE_NAME}
|
POSTGRES_DB: ${DB_DATABASE_NAME}
|
||||||
POSTGRES_INITDB_ARGS: '--data-checksums'
|
POSTGRES_INITDB_ARGS: '--data-checksums'
|
||||||
# Uncomment the DB_STORAGE_TYPE: 'HDD' var if your database isn't stored on SSDs
|
|
||||||
# DB_STORAGE_TYPE: 'HDD'
|
|
||||||
volumes:
|
volumes:
|
||||||
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
# Do not edit the next line. If you want to change the database storage location on your system, edit the value of DB_DATA_LOCATION in the .env file
|
||||||
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
- ${DB_DATA_LOCATION}:/var/lib/postgresql/data
|
||||||
|
healthcheck:
|
||||||
|
test: >-
|
||||||
|
pg_isready --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" || exit 1; Chksum="$$(psql --dbname="$${POSTGRES_DB}" --username="$${POSTGRES_USER}" --tuples-only --no-align --command='SELECT COALESCE(SUM(checksum_failures), 0) FROM pg_stat_database')"; echo "checksum failure count is $$Chksum"; [ "$$Chksum" = '0' ] || exit 1
|
||||||
|
interval: 5m
|
||||||
|
start_interval: 30s
|
||||||
|
start_period: 5m
|
||||||
|
command: >-
|
||||||
|
postgres -c shared_preload_libraries=vectors.so -c 'search_path="$$user", public, vectors' -c logging_collector=on -c max_wal_size=2GB -c shared_buffers=512MB -c wal_compression=on
|
||||||
restart: always
|
restart: always
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
global:
|
global:
|
||||||
scrape_interval: 15s
|
scrape_interval: 3s
|
||||||
evaluation_interval: 15s
|
evaluation_interval: 3s
|
||||||
|
|
||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: immich_api
|
- job_name: immich_api
|
||||||
|
scrape_interval: 3s
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8081']
|
- targets: ["immich-server:8081"]
|
||||||
|
|
||||||
- job_name: immich_microservices
|
- job_name: immich_microservices
|
||||||
|
scrape_interval:
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ['immich-server:8082']
|
- targets: ["immich-server:8082"]
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.14.0
|
||||||
|
|||||||
@@ -278,7 +278,7 @@ You can use [Smart Search](/docs/features/searching.md) for this to some extent.
|
|||||||
|
|
||||||
### I'm getting a lot of "faces" that aren't faces, what can I do?
|
### I'm getting a lot of "faces" that aren't faces, what can I do?
|
||||||
|
|
||||||
You can increase the MIN DETECTION SCORE to 0.8 to help prevent bad thumbnails. Setting the score too high (above 0.9) might filter out too many real faces depending on the library used. If you just want to hide specific faces, you can adjust the 'MIN FACES DETECTED' setting in the administration panel
|
You can increase the MIN DETECTION SCORE to 0.8 to help prevent bad thumbnails. Setting the score too high (above 0.9) might filter out too many real faces depending on the library used. If you just want to hide specific faces, you can adjust the 'MIN FACES DETECTED' setting in the administration panel
|
||||||
to increase the bar for what the algorithm considers a "core face" for that person, reducing the chance of bad thumbnails being chosen.
|
to increase the bar for what the algorithm considers a "core face" for that person, reducing the chance of bad thumbnails being chosen.
|
||||||
|
|
||||||
### The immich_model-cache volume takes up a lot of space, what could be the problem?
|
### The immich_model-cache volume takes up a lot of space, what could be the problem?
|
||||||
@@ -367,12 +367,6 @@ You need to [enable WebSockets](/docs/administration/reverse-proxy/) on your rev
|
|||||||
|
|
||||||
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
|
Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md).
|
||||||
|
|
||||||
### How can I reduce the log verbosity of Redis?
|
|
||||||
|
|
||||||
To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`:
|
|
||||||
|
|
||||||
` command: redis-server --loglevel warning`
|
|
||||||
|
|
||||||
### How can I run Immich as a non-root user?
|
### How can I run Immich as a non-root user?
|
||||||
|
|
||||||
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service.
|
||||||
@@ -380,7 +374,6 @@ You may need to add mount points or docker volumes for the following internal co
|
|||||||
|
|
||||||
- `immich-machine-learning:/.config`
|
- `immich-machine-learning:/.config`
|
||||||
- `immich-machine-learning:/.cache`
|
- `immich-machine-learning:/.cache`
|
||||||
- `redis:/data`
|
|
||||||
|
|
||||||
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
|
The non-root user/group needs read/write access to the volume mounts, including `UPLOAD_LOCATION` and `/cache` for machine-learning.
|
||||||
|
|
||||||
@@ -425,7 +418,7 @@ After removing the containers and volumes, there are a few directories that need
|
|||||||
- `UPLOAD_LOCATION` contains all the media uploaded to Immich.
|
- `UPLOAD_LOCATION` contains all the media uploaded to Immich.
|
||||||
|
|
||||||
:::note Portainer
|
:::note Portainer
|
||||||
If you use portainer, bring down the stack in portainer. Go into the volumes section
|
If you use portainer, bring down the stack in portainer. Go into the volumes section
|
||||||
and remove all the volumes related to immich then restart the stack.
|
and remove all the volumes related to immich then restart the stack.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
|
|||||||
@@ -219,10 +219,3 @@ When you turn off the storage template engine, it will leave the assets in `UPLO
|
|||||||
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
|
Do not touch the files inside these folders under any circumstances except taking a backup. Changing or removing an asset can cause untracked and missing files.
|
||||||
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
|
You can think of it as App-Which-Must-Not-Be-Named, the only access to viewing, changing and deleting assets is only through the mobile or browser interface.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Backup ordering
|
|
||||||
|
|
||||||
A backup of Immich should contain both the database and the asset files. When backing these up it's possible for them to get out of sync, potentially resulting in broken assets after you restore.
|
|
||||||
The best way of dealing with this is to stop the immich-server container while you take a backup. If nothing is changing then the backup will always be in sync.
|
|
||||||
|
|
||||||
If stopping the container is not an option, then the recommended order is to back up the database first, and the filesystem second. This way, the worst case scenario is that there are files on the filesystem that the database doesn't know about. If necessary, these can be (re)uploaded manually after a restore. If the backup is done the other way around, with the filesystem first and the database second, it's possible for the restored database to reference files that aren't in the filesystem backup, thus resulting in broken assets.
|
|
||||||
|
|||||||
@@ -93,7 +93,6 @@ The `.well-known/openid-configuration` part of the url is optional and will be a
|
|||||||
## Auto Launch
|
## Auto Launch
|
||||||
|
|
||||||
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
|
When Auto Launch is enabled, the login page will automatically redirect the user to the OAuth authorization url, to login with OAuth. To access the login screen again, use the browser's back button, or navigate directly to `/auth/login?autoLaunch=0`.
|
||||||
Auto Launch can also be enabled on a per-request basis by navigating to `/auth/login?authLaunch=1`, this can be useful in situations where Immich is called from e.g. Nextcloud using the _External sites_ app and the _oidc_ app so as to enable users to directly interact with a logged-in instance of Immich.
|
|
||||||
|
|
||||||
## Mobile Redirect URI
|
## Mobile Redirect URI
|
||||||
|
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ Running with a pre-existing Postgres server can unlock powerful administrative f
|
|||||||
|
|
||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
You must install `pgvector` (`>= 0.7.0, < 1.0.0`), as it is a prerequisite for `vchord`.
|
You must install pgvecto.rs into your instance of Postgres using their [instructions][vectors-install]. After installation, add `shared_preload_libraries = 'vectors.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vectors.so'`.
|
||||||
The easiest way to do this on Debian/Ubuntu is by adding the [PostgreSQL Apt repository][pg-apt] and then
|
|
||||||
running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`).
|
|
||||||
|
|
||||||
You must install VectorChord into your instance of Postgres using their [instructions][vchord-install]. After installation, add `shared_preload_libraries = 'vchord.so'` to your `postgresql.conf`. If you already have some `shared_preload_libraries` set, you can separate each extension with a comma. For example, `shared_preload_libraries = 'pg_stat_statements, vchord.so'`.
|
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
Immich is known to work with Postgres versions `>= 14, < 18`.
|
Immich is known to work with Postgres versions 14, 15, and 16. Earlier versions are unsupported. Postgres 17 is nominally compatible, but pgvecto.rs does not have prebuilt images or packages for it as of writing.
|
||||||
|
|
||||||
Make sure the installed version of VectorChord is compatible with your version of Immich. The current accepted range for VectorChord is `>= 0.3.0, < 0.5.0`.
|
Make sure the installed version of pgvecto.rs is compatible with your version of Immich. The current accepted range for pgvecto.rs is `>= 0.2.0, < 0.4.0`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Specifying the connection URL
|
## Specifying the connection URL
|
||||||
@@ -57,81 +53,21 @@ CREATE DATABASE <immichdatabasename>;
|
|||||||
\c <immichdatabasename>
|
\c <immichdatabasename>
|
||||||
BEGIN;
|
BEGIN;
|
||||||
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
ALTER DATABASE <immichdatabasename> OWNER TO <immichdbusername>;
|
||||||
CREATE EXTENSION vchord CASCADE;
|
CREATE EXTENSION vectors;
|
||||||
CREATE EXTENSION earthdistance CASCADE;
|
CREATE EXTENSION earthdistance CASCADE;
|
||||||
|
ALTER DATABASE <immichdatabasename> SET search_path TO "$user", public, vectors;
|
||||||
|
ALTER SCHEMA vectors OWNER TO <immichdbusername>;
|
||||||
COMMIT;
|
COMMIT;
|
||||||
```
|
```
|
||||||
|
|
||||||
### Updating VectorChord
|
### Updating pgvecto.rs
|
||||||
|
|
||||||
When installing a new version of VectorChord, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vchord UPDATE;`.
|
When installing a new version of pgvecto.rs, you will need to manually update the extension by connecting to the Immich database and running `ALTER EXTENSION vectors UPDATE;`.
|
||||||
|
|
||||||
## Migrating to VectorChord
|
### Common errors
|
||||||
|
|
||||||
VectorChord is the successor extension to pgvecto.rs, allowing for higher performance, lower memory usage and higher quality results for smart search and facial recognition.
|
#### Permission denied for view
|
||||||
|
|
||||||
### Migrating from pgvecto.rs
|
If you get the error `driverError: error: permission denied for view pg_vector_index_stat`, you can fix this by connecting to the Immich database and running `GRANT SELECT ON TABLE pg_vector_index_stat TO <immichdbusername>;`.
|
||||||
|
|
||||||
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.
|
[vectors-install]: https://docs.vectorchord.ai/getting-started/installation.html
|
||||||
|
|
||||||
The easiest option is to have both extensions installed during the migration:
|
|
||||||
|
|
||||||
1. Ensure you still have pgvecto.rs installed
|
|
||||||
2. Install `pgvector` (`>= 0.7.0, < 1.0.0`). The easiest way to do this is on Debian/Ubuntu by adding the [PostgreSQL Apt repository][pg-apt] and then running `apt install postgresql-NN-pgvector`, where `NN` is your Postgres version (e.g., `16`)
|
|
||||||
3. [Install VectorChord][vchord-install]
|
|
||||||
4. Add `shared_preload_libraries= 'vchord.so, vectors.so'` to your `postgresql.conf`, making sure to include _both_ `vchord.so` and `vectors.so`. You may include other libraries here as well if needed
|
|
||||||
5. Restart the Postgres database
|
|
||||||
6. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;` using psql or your choice of database client
|
|
||||||
7. Start Immich and wait for the logs `Reindexed face_index` and `Reindexed clip_index` to be output
|
|
||||||
8. If Immich does not have superuser permissions, run the SQL command `DROP EXTENSION vectors;`
|
|
||||||
9. Drop the old schema by running `DROP SCHEMA vectors;`
|
|
||||||
10. Remove the `vectors.so` entry from the `shared_preload_libraries` setting
|
|
||||||
11. Restart the Postgres database
|
|
||||||
12. Uninstall pgvecto.rs (e.g. `apt-get purge vectors-pg14` on Debian-based environments, replacing `pg14` as appropriate). `pgvector` must remain installed as it provides the data types used by `vchord`
|
|
||||||
|
|
||||||
If it is not possible to have both VectorChord and pgvecto.rs installed at the same time, you can perform the migration with more manual steps:
|
|
||||||
|
|
||||||
1. While pgvecto.rs is still installed, run the following SQL command using psql or your choice of database client. Take note of the number outputted by this command as you will need it later
|
|
||||||
|
|
||||||
```sql
|
|
||||||
SELECT atttypmod as dimsize
|
|
||||||
FROM pg_attribute f
|
|
||||||
JOIN pg_class c ON c.oid = f.attrelid
|
|
||||||
WHERE c.relkind = 'r'::char
|
|
||||||
AND f.attnum > 0
|
|
||||||
AND c.relname = 'smart_search'::text
|
|
||||||
AND f.attname = 'embedding'::text;
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Remove references to pgvecto.rs using the below SQL commands
|
|
||||||
|
|
||||||
```sql
|
|
||||||
DROP INDEX IF EXISTS clip_index;
|
|
||||||
DROP INDEX IF EXISTS face_index;
|
|
||||||
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE real[];
|
|
||||||
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE real[];
|
|
||||||
```
|
|
||||||
|
|
||||||
3. [Install VectorChord][vchord-install]
|
|
||||||
4. Change the columns back to the appropriate vector types, replacing `<number>` with the number from step 1
|
|
||||||
|
|
||||||
```sql
|
|
||||||
CREATE EXTENSION IF NOT EXISTS vchord CASCADE;
|
|
||||||
ALTER TABLE smart_search ALTER COLUMN embedding SET DATA TYPE vector(<number>);
|
|
||||||
ALTER TABLE face_search ALTER COLUMN embedding SET DATA TYPE vector(512);
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Start Immich and let it create new indices using VectorChord
|
|
||||||
|
|
||||||
### Migrating from pgvector
|
|
||||||
|
|
||||||
1. Ensure you have at least 0.7.0 of pgvector installed. If it is below that, please upgrade it and run the SQL command `ALTER EXTENSION vector UPDATE;` using psql or your choice of database client
|
|
||||||
2. Follow the Prerequisites to install VectorChord
|
|
||||||
3. If Immich does not have superuser permissions, run the SQL command `CREATE EXTENSION vchord CASCADE;`
|
|
||||||
4. Remove the `DB_VECTOR_EXTENSION=pgvector` environmental variable as it will make Immich still use pgvector if set
|
|
||||||
5. Start Immich and let it create new indices using VectorChord
|
|
||||||
|
|
||||||
Note that VectorChord itself uses pgvector types, so you should not uninstall pgvector after following these steps.
|
|
||||||
|
|
||||||
[vchord-install]: https://docs.vectorchord.ai/vectorchord/getting-started/installation.html
|
|
||||||
[pg-apt]: https://www.postgresql.org/download/linux/#generic
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ server {
|
|||||||
client_max_body_size 50000M;
|
client_max_body_size 50000M;
|
||||||
|
|
||||||
# Set headers
|
# Set headers
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $http_host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ Immich uses a traditional client-server design, with a dedicated database for da
|
|||||||
|
|
||||||
<img alt="Immich Architecture" src={AppArchitecture} className="p-4 dark:bg-immich-dark-primary my-4" />
|
<img alt="Immich Architecture" src={AppArchitecture} className="p-4 dark:bg-immich-dark-primary my-4" />
|
||||||
|
|
||||||
The diagram shows clients communicating with the server's API via REST. The server communicates with downstream systems (i.e. Redis, Postgres, Machine Learning, file system) through repository interfaces. Not shown in the diagram, is that the server is split into two separate containers `immich-server` and `immich-microservices`. The microservices container does not handle API requests or schedule cron jobs, but primarily handles incoming job requests from Redis.
|
The diagram shows clients communicating with the server's API via REST. The server communicates with downstream systems (i.e. Postgres, Machine Learning, file system) through repository interfaces. Not shown in the diagram, is that the server is split into two separate containers `immich-server` and `immich-microservices`. The microservices container does not handle API requests or schedule cron jobs, but primarily handles incoming job requests from Postgres.
|
||||||
|
|
||||||
## Clients
|
## Clients
|
||||||
|
|
||||||
@@ -53,7 +53,6 @@ The Immich backend is divided into several services, which are run as individual
|
|||||||
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
1. `immich-server` - Handle and respond to REST API requests, execute background jobs (thumbnail generation, metadata extraction, transcoding, etc.)
|
||||||
1. `immich-machine-learning` - Execute machine learning models
|
1. `immich-machine-learning` - Execute machine learning models
|
||||||
1. `postgres` - Persistent data storage
|
1. `postgres` - Persistent data storage
|
||||||
1. `redis`- Queue management for background jobs
|
|
||||||
|
|
||||||
### Immich Server
|
### Immich Server
|
||||||
|
|
||||||
@@ -111,7 +110,3 @@ Immich persists data in Postgres, which includes information about access and au
|
|||||||
:::info
|
:::info
|
||||||
See [Database Migrations](./database-migrations.md) for more information about how to modify the database to create an index, modify a table, add a new column, etc.
|
See [Database Migrations](./database-migrations.md) for more information about how to modify the database to create an index, modify a table, add a new column, etc.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Redis
|
|
||||||
|
|
||||||
Immich uses [Redis](https://redis.com/) via [BullMQ](https://docs.bullmq.io/) to manage job queues. Some jobs trigger subsequent jobs. For example, Smart Search and Facial Recognition relies on thumbnail generation and automatically run after one is generated.
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ This environment includes the services below. Additional details are available i
|
|||||||
- Server - [`/server`](https://github.com/immich-app/immich/tree/main/server)
|
- Server - [`/server`](https://github.com/immich-app/immich/tree/main/server)
|
||||||
- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web)
|
- Web app - [`/web`](https://github.com/immich-app/immich/tree/main/web)
|
||||||
- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning)
|
- Machine learning - [`/machine-learning`](https://github.com/immich-app/immich/tree/main/machine-learning)
|
||||||
- Redis
|
|
||||||
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
- PostgreSQL development database with exposed port `5432` so you can use any database client to access it
|
||||||
|
|
||||||
All the services are packaged to run as with single Docker Compose command.
|
All the services are packaged to run as with single Docker Compose command.
|
||||||
@@ -75,12 +74,11 @@ npm run dev
|
|||||||
To see local changes to `@immich/ui` in Immich, do the following:
|
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 `npm run build`
|
1. Build the `@immich/ui` project via `npm run build`
|
||||||
3. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
1. Uncomment the corresponding volume in web service of the `docker/docker-compose.dev.yaml` file (`../../ui:/usr/ui`)
|
||||||
4. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
1. Uncomment the corresponding alias in the `web/vite.config.js` file (`'@immich/ui': path.resolve(\_\_dirname, '../../ui')`)
|
||||||
5. Uncomment the import statement in `web/src/app.css` file `@import '/usr/ui/dist/theme/default.css';` and comment out `@import '@immich/ui/theme/default.css';`
|
1. Start up the stack via `make dev`
|
||||||
6. Start up the stack via `make dev`
|
1. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
||||||
7. After making changes in `@immich/ui`, rebuild it (`npm run build`)
|
|
||||||
|
|
||||||
### Mobile app
|
### Mobile app
|
||||||
|
|
||||||
@@ -115,72 +113,32 @@ Note: Activating the license is not required.
|
|||||||
|
|
||||||
### VSCode
|
### VSCode
|
||||||
|
|
||||||
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions. These extensions are listed in the `extensions.json` file under `.vscode/` and should appear as workspace recommendations.
|
Install `Flutter`, `DCM`, `Prettier`, `ESLint` and `Svelte` extensions.
|
||||||
|
|
||||||
Here are the settings we use, they should be active as workspace settings (`settings.json`):
|
in User `settings.json` (`cmd + shift + p` and search for `Open User Settings JSON`) add the following:
|
||||||
|
|
||||||
```json title="settings.json"
|
```json title="settings.json"
|
||||||
{
|
{
|
||||||
"[css]": {
|
"editor.formatOnSave": true,
|
||||||
|
"[javascript][typescript][css]": {
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||||
"editor.formatOnSave": true,
|
"editor.tabSize": 2,
|
||||||
|
"editor.formatOnSave": true
|
||||||
|
},
|
||||||
|
"[svelte]": {
|
||||||
|
"editor.defaultFormatter": "svelte.svelte-vscode",
|
||||||
"editor.tabSize": 2
|
"editor.tabSize": 2
|
||||||
},
|
},
|
||||||
|
"svelte.enable-ts-plugin": true,
|
||||||
|
"eslint.validate": ["javascript", "svelte"],
|
||||||
"[dart]": {
|
"[dart]": {
|
||||||
"editor.defaultFormatter": "Dart-Code.dart-code",
|
|
||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.selectionHighlight": false,
|
"editor.selectionHighlight": false,
|
||||||
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
"editor.suggest.snippetsPreventQuickSuggestions": false,
|
||||||
"editor.suggestSelection": "first",
|
"editor.suggestSelection": "first",
|
||||||
"editor.tabCompletion": "onlySnippets",
|
"editor.tabCompletion": "onlySnippets",
|
||||||
"editor.wordBasedSuggestions": "off"
|
"editor.wordBasedSuggestions": "off",
|
||||||
},
|
"editor.defaultFormatter": "Dart-Code.dart-code"
|
||||||
"[javascript]": {
|
}
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[json]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[jsonc]": {
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[svelte]": {
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "svelte.svelte-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"[typescript]": {
|
|
||||||
"editor.codeActionsOnSave": {
|
|
||||||
"source.organizeImports": "explicit",
|
|
||||||
"source.removeUnusedImports": "explicit"
|
|
||||||
},
|
|
||||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"editor.tabSize": 2
|
|
||||||
},
|
|
||||||
"cSpell.words": ["immich"],
|
|
||||||
"editor.formatOnSave": true,
|
|
||||||
"eslint.validate": ["javascript", "svelte"],
|
|
||||||
"explorer.fileNesting.enabled": true,
|
|
||||||
"explorer.fileNesting.patterns": {
|
|
||||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
|
||||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts"
|
|
||||||
},
|
|
||||||
"svelte.enable-ts-plugin": true,
|
|
||||||
"typescript.preferences.importModuleSpecifier": "non-relative"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -1,19 +0,0 @@
|
|||||||
# Chromecast support
|
|
||||||
|
|
||||||
Immich supports the Google's Cast protocol so that photos and videos can be cast to devices such as a Chromecast and a Nest Hub. This feature is considered experimental and has several important limitations listed below. Currently, this feature is only supported by the web client, support on Android and iOS is planned for the future.
|
|
||||||
|
|
||||||
## Enable Google Cast Support
|
|
||||||
|
|
||||||
Google Cast support is disabled by default. The web UI uses Google-provided scripts and must retreive them from Google servers when the page loads. This is a privacy concern for some and is thus opt-in.
|
|
||||||
|
|
||||||
You can enable Google Cast support through `Account Settings > Features > Cast > Google Cast`
|
|
||||||
|
|
||||||
<img src={require('./img/gcast-enable.webp').default} width="70%" title='Enable Google Cast Support' />
|
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
To use casting with Immich, there are a few prerequisites:
|
|
||||||
|
|
||||||
1. Your instance must be accessed via an HTTPS connection in order for the casting menu to show.
|
|
||||||
2. Your instance must be publicly accessible via HTTPS and a DNS record for the server must be accessible via Google's DNS servers (`8.8.8.8` and `8.8.4.4`)
|
|
||||||
3. Videos must be in a format that is compatible with Google Cast. For more info, check out [Google's documentation](https://developers.google.com/cast/docs/media)
|
|
||||||
@@ -90,22 +90,19 @@ Usage: immich upload [paths...] [options]
|
|||||||
Upload assets
|
Upload assets
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
paths One or more paths to assets to be uploaded
|
paths One or more paths to assets to be uploaded
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
-r, --recursive Recursive (default: false, env: IMMICH_RECURSIVE)
|
||||||
-i, --ignore <pattern> Pattern to ignore (env: IMMICH_IGNORE_PATHS)
|
-i, --ignore [paths...] Paths to ignore (default: [], env: IMMICH_IGNORE_PATHS)
|
||||||
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
-h, --skip-hash Don't hash files before upload (default: false, env: IMMICH_SKIP_HASH)
|
||||||
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
-H, --include-hidden Include hidden folders (default: false, env: IMMICH_INCLUDE_HIDDEN)
|
||||||
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
-a, --album Automatically create albums based on folder name (default: false, env: IMMICH_AUTO_CREATE_ALBUM)
|
||||||
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
-A, --album-name <name> Add all assets to specified album (env: IMMICH_ALBUM_NAME)
|
||||||
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
-n, --dry-run Don't perform any actions, just show what will be done (default: false, env: IMMICH_DRY_RUN)
|
||||||
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
-c, --concurrency <number> Number of assets to upload at the same time (default: 4, env: IMMICH_UPLOAD_CONCURRENCY)
|
||||||
-j, --json-output Output detailed information in json format (default: false, env: IMMICH_JSON_OUTPUT)
|
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
||||||
--delete Delete local assets after upload (env: IMMICH_DELETE_ASSETS)
|
--help display help for command
|
||||||
--no-progress Hide progress bars (env: IMMICH_PROGRESS_BAR)
|
|
||||||
--watch Watch for changes and upload automatically (default: false, env: IMMICH_WATCH_CHANGES)
|
|
||||||
--help display help for command
|
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
@@ -175,16 +172,6 @@ By default, hidden files are skipped. If you want to include hidden files, use t
|
|||||||
immich upload --include-hidden --recursive directory/
|
immich upload --include-hidden --recursive directory/
|
||||||
```
|
```
|
||||||
|
|
||||||
You can use the `--json-output` option to get a json printed which includes
|
|
||||||
three keys: `newFiles`, `duplicates` and `newAssets`. Due to some logging
|
|
||||||
output you will need to strip the first three lines of output to get the json.
|
|
||||||
For example to get a list of files that would be uploaded for further
|
|
||||||
processing:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
immich upload --dry-run . | tail -n +4 | jq .newFiles[]
|
|
||||||
```
|
|
||||||
|
|
||||||
### Obtain the API Key
|
### Obtain the API Key
|
||||||
|
|
||||||
The API key can be obtained in the user setting panel on the web interface.
|
The API key can be obtained in the user setting panel on the web interface.
|
||||||
|
|||||||
@@ -121,6 +121,6 @@ Once this is done, you can continue to step 3 of "Basic Setup".
|
|||||||
|
|
||||||
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
|
[hw-file]: https://github.com/immich-app/immich/releases/latest/download/hwaccel.transcoding.yml
|
||||||
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
[nvct]: https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html
|
||||||
[jellyfin-lp]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#low-power-encoding
|
[jellyfin-lp]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#configure-and-verify-lp-mode-on-linux
|
||||||
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/post-install/transcoding/hardware-acceleration/intel#known-issues-and-limitations-on-linux
|
[jellyfin-kernel-bug]: https://jellyfin.org/docs/general/administration/hardware-acceleration/intel/#known-issues-and-limitations
|
||||||
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
[libmali-rockchip]: https://github.com/tsukumijima/libmali-rockchip/releases
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB |
@@ -72,7 +72,7 @@ In rare cases, the library watcher can hang, preventing Immich from starting up.
|
|||||||
|
|
||||||
### Nightly job
|
### Nightly job
|
||||||
|
|
||||||
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library management page.
|
There is an automatic scan job that is scheduled to run once a day. This job also cleans up any libraries stuck in deletion. It is possible to trigger the cleanup by clicking "Scan all libraries" in the library managment page.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
@@ -112,15 +112,12 @@ _Remember to run `docker compose up -d` to register the changes. Make sure you c
|
|||||||
|
|
||||||
These actions must be performed by the Immich administrator.
|
These actions must be performed by the Immich administrator.
|
||||||
|
|
||||||
- Click on your avatar on the upper right corner
|
- Click on Administration -> Libraries
|
||||||
- Click on Administration -> External Libraries
|
- Click on Create External Library
|
||||||
- Click on Create an external library…
|
|
||||||
- Select which user owns the library, this can not be changed later
|
- Select which user owns the library, this can not be changed later
|
||||||
- Enter `/mnt/media/christmas-trip` then click Add
|
- Enter `/mnt/media/christmas-trip` then click Add
|
||||||
- Click on Save
|
- Click on Save
|
||||||
- Click the drop-down menu on the newly created library
|
- Click the drop-down menu on the newly created library
|
||||||
- Click on Scan
|
|
||||||
- Click the drop-down menu on the newly created library
|
|
||||||
- Click on Rename Library and rename it to "Christmas Trip"
|
- Click on Rename Library and rename it to "Christmas Trip"
|
||||||
|
|
||||||
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
|
NOTE: We have to use the `/mnt/media/christmas-trip` path and not the `/mnt/nas/christmas-trip` path since all paths have to be what the Docker containers see.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import TabItem from '@theme/TabItem';
|
|||||||
|
|
||||||
Immich uses Postgres as its search database for both metadata and contextual CLIP search.
|
Immich uses Postgres as its search database for both metadata and contextual CLIP search.
|
||||||
|
|
||||||
Contextual CLIP search is powered by the [VectorChord](https://github.com/tensorchord/VectorChord) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
|
Contextual CLIP search is powered by the [pgvecto.rs](https://github.com/tensorchord/pgvecto.rs) extension, utilizing machine learning models like [CLIP](https://openai.com/research/clip) to provide relevant search results. This allows for freeform searches without requiring specific keywords in the image or video metadata.
|
||||||
|
|
||||||
## Advanced Search Filters
|
## Advanced Search Filters
|
||||||
|
|
||||||
@@ -92,7 +92,7 @@ Memory and execution time estimates were obtained without acceleration on a 7800
|
|||||||
|
|
||||||
**Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input.
|
**Execution Time (ms)**: After warming up the model with one pass, the mean execution time of 100 passes with the same input.
|
||||||
|
|
||||||
**Memory (MiB)**: The peak RSS usage of the process after performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors.
|
**Memory (MiB)**: The peak RSS usage of the process afer performing the above timing benchmark. Does not include image decoding, concurrent processing, the web server, etc., which are relatively constant factors.
|
||||||
|
|
||||||
**Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets.
|
**Recall (%)**: Evaluated on Crossmodal-3600, the average of the recall@1, recall@5 and recall@10 results for zeroshot image retrieval. Chinese (Simplified), English, French, German, Italian, Japanese, Korean, Polish, Russian, Spanish and Turkish are additionally tested on XTD-10. Chinese (Simplified) and English are additionally tested on Flickr30k. The recall metrics are the average across all tested datasets.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Scaling Immich
|
# Scaling Immich
|
||||||
|
|
||||||
Immich is built with modern deployment practices in mind, and the backend is designed to be able to run multiple instances in parallel. When doing this, the only requirement you need to be aware of is that every instance needs to be connected to the shared infrastructure. That means they should all have access to the same Postgres and Redis instances, and have the same files mounted into the containers.
|
Immich is built with modern deployment practices in mind, and the backend is designed to be able to run multiple instances in parallel. When doing this, the only requirement you need to be aware of is that every instance needs to be connected to the shared infrastructure. That means they should all have access to the same Postgres instance, and have the same files mounted into the containers.
|
||||||
|
|
||||||
Scaling can be useful for many reasons. Maybe you have a gaming PC that you want to use for transcoding and thumbnail generation, or perhaps you run a Kubernetes cluster across a handful of powerful servers that you want to make use of.
|
Scaling can be useful for many reasons. Maybe you have a gaming PC that you want to use for transcoding and thumbnail generation, or perhaps you run a Kubernetes cluster across a handful of powerful servers that you want to make use of.
|
||||||
|
|
||||||
@@ -16,4 +16,4 @@ By default, each running `immich-server` container comes with multiple internal
|
|||||||
|
|
||||||
## Scaling down
|
## Scaling down
|
||||||
|
|
||||||
In the same way you can scale up to multiple containers, you can also choose to scale down. All state is stored in Postgres, Redis, and the filesystem so there is no risk in stopping a running immich-server container, for example if you want to use your GPU to play some games. As long as there is an API worker running you will still be able to browse Immich, and jobs will wait to be processed until there is a worker available for them.
|
In the same way you can scale up to multiple containers, you can also choose to scale down. All state is stored in Postgres and the filesystem so there is no risk in stopping a running immich-server container, for example if you want to use your GPU to play some games. As long as there is an API worker running you will still be able to browse Immich, and jobs will wait to be processed until there is a worker available for them.
|
||||||
|
|||||||
@@ -2,13 +2,53 @@
|
|||||||
sidebar_position: 30
|
sidebar_position: 30
|
||||||
---
|
---
|
||||||
|
|
||||||
|
import CodeBlock from '@theme/CodeBlock';
|
||||||
|
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
|
||||||
|
|
||||||
# Docker Compose [Recommended]
|
# Docker Compose [Recommended]
|
||||||
|
|
||||||
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
Docker Compose is the recommended method to run Immich in production. Below are the steps to deploy Immich with Docker Compose.
|
||||||
|
|
||||||
import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx';
|
## Step 1 - Download the required files
|
||||||
|
|
||||||
<DockerComposeSteps />
|
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
|
||||||
|
|
||||||
|
```bash title="Move to the directory you created"
|
||||||
|
mkdir ./immich-app
|
||||||
|
cd ./immich-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Download [`docker-compose.yml`][compose-file] and [`example.env`][env-file] by running the following commands:
|
||||||
|
|
||||||
|
```bash title="Get docker-compose.yml file"
|
||||||
|
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash title="Get .env file"
|
||||||
|
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||||
|
```
|
||||||
|
|
||||||
|
You can alternatively download these two files from your browser and move them to the directory that you created, in which case ensure that you rename `example.env` to `.env`.
|
||||||
|
|
||||||
|
## Step 2 - Populate the .env file with custom values
|
||||||
|
|
||||||
|
<CodeBlock language="bash" title="Default environmental variable content">
|
||||||
|
{ExampleEnv}
|
||||||
|
</CodeBlock>
|
||||||
|
|
||||||
|
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
||||||
|
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
|
||||||
|
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
||||||
|
- Set your timezone by uncommenting the `TZ=` line.
|
||||||
|
- Populate custom database information if necessary.
|
||||||
|
|
||||||
|
## Step 3 - Start the containers
|
||||||
|
|
||||||
|
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
|
||||||
|
|
||||||
|
```bash title="Start the containers"
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
:::info Docker version
|
:::info Docker version
|
||||||
If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by following the complete [Docker Engine install](https://docs.docker.com/engine/install/) procedure for your distribution, crucially the "Uninstall old versions" and "Install using the apt/rpm repository" sections. These replace the distro's Docker packages with Docker's official ones.
|
If you get an error such as `unknown shorthand flag: 'd' in -d` or `open <location of your .env file>: permission denied`, you are probably running the wrong Docker version. (This happens, for example, with the docker.io package in Ubuntu 22.04.3 LTS.) You can correct the problem by following the complete [Docker Engine install](https://docs.docker.com/engine/install/) procedure for your distribution, crucially the "Uninstall old versions" and "Install using the apt/rpm repository" sections. These replace the distro's Docker packages with Docker's official ones.
|
||||||
@@ -30,3 +70,6 @@ If you get an error `can't set healthcheck.start_interval as feature require Doc
|
|||||||
## Next Steps
|
## Next Steps
|
||||||
|
|
||||||
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
|
Read the [Post Installation](/docs/install/post-install.mdx) steps and [upgrade instructions](/docs/install/upgrading.md).
|
||||||
|
|
||||||
|
[compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
||||||
|
[env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env
|
||||||
|
|||||||
@@ -72,21 +72,20 @@ Information on the current workers can be found [here](/docs/administration/jobs
|
|||||||
|
|
||||||
## Database
|
## Database
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
| :---------------------------------- | :--------------------------------------------------------------------------- | :--------: | :----------------------------- |
|
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
|
||||||
| `DB_URL` | Database URL | | server |
|
| `DB_URL` | Database URL | | server |
|
||||||
| `DB_HOSTNAME` | Database host | `database` | server |
|
| `DB_HOSTNAME` | Database host | `database` | server |
|
||||||
| `DB_PORT` | Database port | `5432` | server |
|
| `DB_PORT` | Database port | `5432` | server |
|
||||||
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_USERNAME` | Database user | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
| `DB_PASSWORD` | Database password | `postgres` | server, database<sup>\*1</sup> |
|
||||||
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
| `DB_DATABASE_NAME` | Database name | `immich` | server, database<sup>\*1</sup> |
|
||||||
| `DB_SSL_MODE` | Database SSL mode | | server |
|
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
|
||||||
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
|
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
||||||
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
|
|
||||||
|
|
||||||
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
|
||||||
|
|
||||||
\*2: If not provided, the appropriate extension to use is auto-detected at startup by introspecting the database. When multiple extensions are installed, the order of preference is VectorChord, pgvecto.rs, pgvector.
|
\*2: This setting cannot be changed after the server has successfully started up.
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
|
|
||||||
@@ -99,54 +98,6 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Redis
|
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
|
||||||
| :--------------- | :------------- | :-----: | :--------- |
|
|
||||||
| `REDIS_URL` | Redis URL | | server |
|
|
||||||
| `REDIS_SOCKET` | Redis socket | | server |
|
|
||||||
| `REDIS_HOSTNAME` | Redis host | `redis` | server |
|
|
||||||
| `REDIS_PORT` | Redis port | `6379` | server |
|
|
||||||
| `REDIS_USERNAME` | Redis username | | server |
|
|
||||||
| `REDIS_PASSWORD` | Redis password | | server |
|
|
||||||
| `REDIS_DBINDEX` | Redis DB index | `0` | server |
|
|
||||||
|
|
||||||
:::info
|
|
||||||
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
|
||||||
|
|
||||||
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
|
||||||
More information can be found in the upstream [ioredis] documentation.
|
|
||||||
|
|
||||||
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
|
|
||||||
:::
|
|
||||||
|
|
||||||
Redis (Sentinel) URL example JSON before encoding:
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>JSON</summary>
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"sentinels": [
|
|
||||||
{
|
|
||||||
"host": "redis-sentinel-node-0",
|
|
||||||
"port": 26379
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"host": "redis-sentinel-node-1",
|
|
||||||
"port": 26379
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"host": "redis-sentinel-node-2",
|
|
||||||
"port": 26379
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "redis-sentinel"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## Machine Learning
|
## Machine Learning
|
||||||
|
|
||||||
| Variable | Description | Default | Containers |
|
| Variable | Description | Default | Containers |
|
||||||
@@ -213,16 +164,10 @@ the `_FILE` variable should be set to the path of a file containing the variable
|
|||||||
| `DB_USERNAME` | `DB_USERNAME_FILE`<sup>\*1</sup> |
|
| `DB_USERNAME` | `DB_USERNAME_FILE`<sup>\*1</sup> |
|
||||||
| `DB_PASSWORD` | `DB_PASSWORD_FILE`<sup>\*1</sup> |
|
| `DB_PASSWORD` | `DB_PASSWORD_FILE`<sup>\*1</sup> |
|
||||||
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
|
| `DB_URL` | `DB_URL_FILE`<sup>\*1</sup> |
|
||||||
| `REDIS_PASSWORD` | `REDIS_PASSWORD_FILE`<sup>\*2</sup> |
|
|
||||||
|
|
||||||
\*1: See the [official documentation][docker-secrets-docs] for
|
\*1: See the [official documentation][docker-secrets-docs] for
|
||||||
details on how to use Docker Secrets in the Postgres image.
|
details on how to use Docker Secrets in the Postgres image.
|
||||||
|
|
||||||
\*2: See [this comment][docker-secrets-example] for an example of how
|
|
||||||
to use a Docker secret for the password in the Redis container.
|
|
||||||
|
|
||||||
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
[tz-list]: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
|
||||||
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
|
||||||
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
||||||
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
||||||
[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis
|
|
||||||
|
|||||||
@@ -25,11 +25,11 @@ When you're all done, you should have the following:
|
|||||||
- `./docker/immich-app/postgres`
|
- `./docker/immich-app/postgres`
|
||||||
- `./docker/immich-app/library`
|
- `./docker/immich-app/library`
|
||||||
|
|
||||||
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory, and rename `example.env` to `.env`.
|
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) to your computer. Upload the files to the `./docker/immich-app` directory.
|
||||||
|
|
||||||
## Step 2 - Populate the .env file with custom values
|
## Step 2 - Populate the .env file with custom values
|
||||||
|
|
||||||
Follow [Step 2 in Docker Compose](/docs/install/docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
|
Follow [Step 2 in Docker Compose](./docker-compose#step-2---populate-the-env-file-with-custom-values) for instructions on customizing the `.env` file, and then return back to this guide to continue.
|
||||||
|
|
||||||
## Step 3 - Create a new project in Container Manager
|
## Step 3 - Create a new project in Container Manager
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
sidebar_position: 80
|
sidebar_position: 80
|
||||||
---
|
---
|
||||||
|
|
||||||
# TrueNAS [Community]
|
# TrueNAS SCALE [Community]
|
||||||
|
|
||||||
:::note
|
:::note
|
||||||
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
This is a community contribution and not officially supported by the Immich team, but included here for convenience.
|
||||||
@@ -12,17 +12,17 @@ Community support can be found in the dedicated channel on the [Discord Server](
|
|||||||
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
**Please report app issues to the corresponding [Github Repository](https://github.com/truenas/charts/tree/master/community/immich).**
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Immich can easily be installed on TrueNAS Community Edition via the **Community** train application.
|
Immich can easily be installed on TrueNAS SCALE via the **Community** train application.
|
||||||
Consider reviewing the TrueNAS [Apps resources](https://apps.truenas.com/getting-started/) if you have not previously configured applications on your system.
|
Consider reviewing the TrueNAS [Apps tutorial](https://www.truenas.com/docs/scale/scaletutorials/apps/) if you have not previously configured applications on your system.
|
||||||
|
|
||||||
TrueNAS Community Edition makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries.
|
TrueNAS SCALE makes installing and updating Immich easy, but you must use the Immich web portal and mobile app to configure accounts and access libraries.
|
||||||
|
|
||||||
## First Steps
|
## First Steps
|
||||||
|
|
||||||
The Immich app in TrueNAS Community Edition installs, completes the initial configuration, then starts the Immich web portal.
|
The Immich app in TrueNAS SCALE installs, completes the initial configuration, then starts the Immich web portal.
|
||||||
When updates become available, TrueNAS alerts and provides easy updates.
|
When updates become available, SCALE alerts and provides easy updates.
|
||||||
|
|
||||||
Before installing the Immich app in TrueNAS, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation.
|
Before installing the Immich app in SCALE, review the [Environment Variables](#environment-variables) documentation to see if you want to configure any during installation.
|
||||||
You may also configure environment variables at any time after deploying the application.
|
You may also configure environment variables at any time after deploying the application.
|
||||||
|
|
||||||
### Setting up Storage Datasets
|
### Setting up Storage Datasets
|
||||||
@@ -107,8 +107,6 @@ Accept the default option or select the **Machine Learning Image Type** for your
|
|||||||
|
|
||||||
Immich's default is `postgres` but you should consider setting the **Database Password** to a custom value using only the characters `A-Za-z0-9`.
|
Immich's default is `postgres` but you should consider setting the **Database Password** to a custom value using only the characters `A-Za-z0-9`.
|
||||||
|
|
||||||
The **Redis Password** should be set to a custom value using only the characters `A-Za-z0-9`.
|
|
||||||
|
|
||||||
Accept the **Log Level** default of **Log**.
|
Accept the **Log Level** default of **Log**.
|
||||||
|
|
||||||
Leave **Hugging Face Endpoint** blank. (This is for downloading ML models from a different source.)
|
Leave **Hugging Face Endpoint** blank. (This is for downloading ML models from a different source.)
|
||||||
@@ -126,9 +124,9 @@ className="border rounded-xl"
|
|||||||
|
|
||||||
Accept the default port `30041` in **WebUI Port** or enter a custom port number.
|
Accept the default port `30041` in **WebUI Port** or enter a custom port number.
|
||||||
:::info Allowed Port Numbers
|
:::info Allowed Port Numbers
|
||||||
Only numbers within the range 9000-65535 may be used on TrueNAS versions below TrueNAS Community Edition 24.10 Electric Eel.
|
Only numbers within the range 9000-65535 may be used on SCALE versions below TrueNAS Scale 24.10 Electric Eel.
|
||||||
|
|
||||||
Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/solutions/optimizations/security/#truenas-default-ports).
|
Regardless of version, to avoid port conflicts, don't use [ports on this list](https://www.truenas.com/docs/references/defaultports/).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
### Storage Configuration
|
### Storage Configuration
|
||||||
@@ -173,7 +171,7 @@ className="border rounded-xl"
|
|||||||
|
|
||||||
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
|
You may configure [External Libraries](/docs/features/libraries) by mounting them using **Additional Storage**.
|
||||||
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
|
The **Mount Path** is the location you will need to copy and paste into the External Library settings within Immich.
|
||||||
The **Host Path** is the location on the TrueNAS Community Edition server where your external library is located.
|
The **Host Path** is the location on the TrueNAS SCALE server where your external library is located.
|
||||||
|
|
||||||
<!-- A section for Labels would go here but I don't know what they do. -->
|
<!-- A section for Labels would go here but I don't know what they do. -->
|
||||||
|
|
||||||
@@ -188,17 +186,17 @@ className="border rounded-xl"
|
|||||||
|
|
||||||
Accept the default **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core).
|
Accept the default **CPU** limit of `2` threads or specify the number of threads (CPUs with Multi-/Hyper-threading have 2 threads per core).
|
||||||
|
|
||||||
Specify the **Memory** limit in MB of RAM. Immich recommends at least 6000 MB (6GB). If you selected **Enable Machine Learning** in **Immich Configuration**, you should probably set this above 8000 MB.
|
Accept the default **Memory** limit of `4096` MB or specify the number of MB of RAM. If you're using Machine Learning you should probably set this above 8000 MB.
|
||||||
|
|
||||||
:::info Older TrueNAS Versions
|
:::info Older SCALE Versions
|
||||||
Before TrueNAS Community Edition version 24.10 Electric Eel:
|
Before TrueNAS SCALE version 24.10 Electric Eel:
|
||||||
|
|
||||||
The **CPU** value was specified in a different format with a default of `4000m` which is 4 threads.
|
The **CPU** value was specified in a different format with a default of `4000m` which is 4 threads.
|
||||||
|
|
||||||
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
The **Memory** value was specified in a different format with a default of `8Gi` which is 8 GiB of RAM. The value was specified in bytes or a number with a measurement suffix. Examples: `129M`, `123Mi`, `1000000000`
|
||||||
:::
|
:::
|
||||||
|
|
||||||
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://apps.truenas.com/managing-apps/installing-apps/#gpu-passthrough)
|
Enable **GPU Configuration** options if you have a GPU that you will use for [Hardware Transcoding](/docs/features/hardware-transcoding) and/or [Hardware-Accelerated Machine Learning](/docs/features/ml-hardware-acceleration.md). More info: [GPU Passthrough Docs for TrueNAS Apps](https://www.truenas.com/docs/truenasapps/#gpu-passthrough)
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
@@ -240,9 +238,9 @@ className="border rounded-xl"
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
:::info
|
:::info
|
||||||
Some Environment Variables are not available for the TrueNAS Community Edition app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings).
|
Some Environment Variables are not available for the TrueNAS SCALE app. This is mainly because they can be configured through GUI options in the [Edit Immich screen](#edit-app-settings).
|
||||||
|
|
||||||
Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`, `REDIS_PASSWORD`.
|
Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`, `IMMICH_LOG_LEVEL`, `DB_PASSWORD`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
## Updating the App
|
## Updating the App
|
||||||
@@ -251,7 +249,7 @@ Some examples are: `IMMICH_VERSION`, `UPLOAD_LOCATION`, `DB_DATA_LOCATION`, `TZ`
|
|||||||
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
|
Make sure to read the general [upgrade instructions](/docs/install/upgrading.md).
|
||||||
:::
|
:::
|
||||||
|
|
||||||
When updates become available, TrueNAS alerts and provides easy updates.
|
When updates become available, SCALE alerts and provides easy updates.
|
||||||
To update the app to the latest version:
|
To update the app to the latest version:
|
||||||
|
|
||||||
- Go to the **Installed Applications** screen and select Immich from the list of installed applications.
|
- Go to the **Installed Applications** screen and select Immich from the list of installed applications.
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ Immich can easily be installed and updated on Unraid via:
|
|||||||
|
|
||||||
:::
|
:::
|
||||||
|
|
||||||
In order to install Immich from the Unraid CA, you will need an existing Redis and PostgreSQL 14 container, If you do not already have Redis or PostgreSQL you can install them from the Unraid CA, just make sure you choose PostgreSQL **14**.
|
In order to install Immich from the Unraid CA, you will need an existing PostgreSQL 14 container, If you do not already have PostgreSQL you can install it from the Unraid CA, just make sure you choose PostgreSQL **14**.
|
||||||
|
|
||||||
Once you have Redis and PostgreSQL running, search for Immich on the Unraid CA, choose either of the templates listed and fill out the example variables.
|
Once you have PostgreSQL running, search for Immich on the Unraid CA, choose either of the templates listed and fill out the example variables.
|
||||||
|
|
||||||
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
|
For more information about setting up the community image see [here](https://github.com/imagegenius/docker-immich#application-setup)
|
||||||
|
|
||||||
@@ -45,63 +45,63 @@ width="70%"
|
|||||||
alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
alt="Select Plugins > Compose.Manager > Add New Stack > Label it Immich"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
3. Select the cogwheel ⚙️ next to Immich and click "**Edit Stack**"
|
3. Select the cogwheel ⚙️ next to Immich and click "**Edit Stack**"
|
||||||
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
|
4. Click "**Compose File**" and then paste the entire contents of the [Immich Docker Compose](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) file into the Unraid editor. Remove any text that may be in the text area by default. Note that Unraid v6.12.10 uses version 24.0.9 of the Docker Engine, which does not support healthcheck `start_interval` as defined in the `database` service of the Docker compose file (version 25 or higher is needed). This parameter defines an initial waiting period before starting health checks, to give the container time to start up. Commenting out the `start_interval` and `start_period` parameters will allow the containers to start up normally. The only downside to this is that the database container will not receive an initial health check until `interval` time has passed.
|
||||||
|
|
||||||
<details >
|
<details >
|
||||||
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
|
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 5.</summary>
|
||||||
<ul>
|
<ul>
|
||||||
<li>Comment out the database service</li>
|
<li>Comment out the database service</li>
|
||||||
<img
|
<img
|
||||||
src={require('./img/unraid02.webp').default}
|
src={require('./img/unraid02.webp').default}
|
||||||
width="50%"
|
width="50%"
|
||||||
alt="Comment out database service in the compose file"
|
alt="Comment out database service in the compose file"
|
||||||
/>
|
/>
|
||||||
<li>Comment out the database dependency for <b>each service</b> <i>(example in screenshot below only shows 2 of the services - ensure you do this for all services)</i></li>
|
<li>Comment out the database dependency for <b>each service</b> <i>(example in screenshot below only shows 2 of the services - ensure you do this for all services)</i></li>
|
||||||
<img
|
<img
|
||||||
src={require('./img/unraid03.webp').default}
|
src={require('./img/unraid03.webp').default}
|
||||||
width="50%"
|
width="50%"
|
||||||
alt="Comment out every reference to the database service in the compose file"
|
alt="Comment out every reference to the database service in the compose file"
|
||||||
/>
|
/>
|
||||||
<li>Comment out the volumes</li>
|
<li>Comment out the volumes</li>
|
||||||
<img
|
<img
|
||||||
src={require('./img/unraid04.webp').default}
|
src={require('./img/unraid04.webp').default}
|
||||||
width="20%"
|
width="20%"
|
||||||
alt="Comment out database volume"
|
alt="Comment out database volume"
|
||||||
/>
|
/>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
5. Click "**Save Changes**", you will be prompted to edit stack UI labels, just leave this blank and click "**Ok**"
|
||||||
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
6. Select the cog ⚙️ next to Immich, click "**Edit Stack**", then click "**Env File**"
|
||||||
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
7. Paste the entire contents of the [Immich example.env](https://github.com/immich-app/immich/releases/latest/download/example.env) file into the Unraid editor, then **before saving** edit the following:
|
||||||
|
|
||||||
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
- `UPLOAD_LOCATION`: Create a folder in your Images Unraid share and place the **absolute** location here > For example my _"images"_ share has a folder within it called _"immich"_. If I browse to this directory in the terminal and type `pwd` the output is `/mnt/user/images/immich`. This is the exact value I need to enter as my `UPLOAD_LOCATION`
|
||||||
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
- `DB_DATA_LOCATION`: Change this to use an Unraid share (preferably a cache pool, e.g. `/mnt/user/appdata/postgresql/data`). This uses the `appdata` share. Do also create the `postgresql` folder, by running `mkdir /mnt/user/{share_location}/postgresql/data`. If left at default it will try to use Unraid's `/boot/config/plugins/compose.manager/projects/[stack_name]/postgres` folder which it doesn't have permissions to, resulting in this container continuously restarting.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/unraid05.webp').default}
|
src={require('./img/unraid05.webp').default}
|
||||||
width="70%"
|
width="70%"
|
||||||
alt="Absolute location of where you want immich images stored"
|
alt="Absolute location of where you want immich images stored"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<details >
|
<details >
|
||||||
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 8.</summary>
|
<summary>Using an existing Postgres container? Click me! Otherwise proceed to step 8.</summary>
|
||||||
<p>Update the following database variables as relevant to your Postgres container:</p>
|
<p>Update the following database variables as relevant to your Postgres container:</p>
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>DB_HOSTNAME</code></li>
|
<li><code>DB_HOSTNAME</code></li>
|
||||||
<li><code>DB_USERNAME</code></li>
|
<li><code>DB_USERNAME</code></li>
|
||||||
<li><code>DB_PASSWORD</code></li>
|
<li><code>DB_PASSWORD</code></li>
|
||||||
<li><code>DB_DATABASE_NAME</code></li>
|
<li><code>DB_DATABASE_NAME</code></li>
|
||||||
<li><code>DB_PORT</code></li>
|
<li><code>DB_PORT</code></li>
|
||||||
</ul>
|
</ul>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
8. Click "**Save Changes**" followed by "**Compose Up**" and Unraid will begin to create the Immich containers in a popup window. Once complete you will see a message on the popup window stating _"Connection Closed"_. Click "**Done**" and go to the Unraid "**Docker**" page
|
8. Click "**Save Changes**" followed by "**Compose Up**" and Unraid will begin to create the Immich containers in a popup window. Once complete you will see a message on the popup window stating _"Connection Closed"_. Click "**Done**" and go to the Unraid "**Docker**" page
|
||||||
|
|
||||||
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
|
> Note: This can take several minutes depending on your Internet speed and Unraid hardware
|
||||||
|
|
||||||
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_server` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
9. Once on the Docker page you will see several Immich containers, one of them will be labelled `immich_server` and will have a port mapping. Visit the `IP:PORT` displayed in your web browser and you should see the Immich admin setup page.
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={require('./img/unraid06.webp').default}
|
src={require('./img/unraid06.webp').default}
|
||||||
@@ -122,7 +122,7 @@ alt="Go to Docker Tab and visit the address listed next to immich-web"
|
|||||||
width="90%"
|
width="90%"
|
||||||
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
alt="Go to Docker Tab and visit the address listed next to immich-web"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
:::tip
|
:::tip
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
sidebar_position: 3
|
sidebar_position: 2
|
||||||
---
|
---
|
||||||
|
|
||||||
# Comparison
|
# Comparison
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 233 KiB |
@@ -2,13 +2,9 @@
|
|||||||
sidebar_position: 1
|
sidebar_position: 1
|
||||||
---
|
---
|
||||||
|
|
||||||
# Welcome to Immich
|
# Introduction
|
||||||
|
|
||||||
<img
|
<img src={require('./img/feature-panel.webp').default} alt="Immich - Self-hosted photos and videos backup tool" />
|
||||||
src={require('./img/social-preview-light.webp').default}
|
|
||||||
alt="Immich - Self-hosted photos and videos backup tool"
|
|
||||||
data-theme="light"
|
|
||||||
/>
|
|
||||||
|
|
||||||
## Welcome!
|
## Welcome!
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
sidebar_position: 2
|
sidebar_position: 3
|
||||||
---
|
---
|
||||||
|
|
||||||
# Quick start
|
# Quick start
|
||||||
@@ -10,20 +10,11 @@ to install and use it.
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
- A system with at least 4GB of RAM and 2 CPU cores.
|
Check the [requirements page](/docs/install/requirements) to get started.
|
||||||
- [Docker](https://docs.docker.com/engine/install/)
|
|
||||||
|
|
||||||
> For a more detailed list of requirements, see the [requirements page](/docs/install/requirements).
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Set up the server
|
## Set up the server
|
||||||
|
|
||||||
import DockerComposeSteps from '/docs/partials/_docker-compose-install-steps.mdx';
|
Follow the [Docker Compose (Recommended)](/docs/install/docker-compose) instructions to install the server.
|
||||||
|
|
||||||
<DockerComposeSteps />
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Try the web app
|
## Try the web app
|
||||||
|
|
||||||
@@ -35,8 +26,6 @@ Try uploading a picture from your browser.
|
|||||||
|
|
||||||
<img src={require('./img/upload-button.webp').default} title="Upload button" />
|
<img src={require('./img/upload-button.webp').default} title="Upload button" />
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Try the mobile app
|
## Try the mobile app
|
||||||
|
|
||||||
### Download the Mobile App
|
### Download the Mobile App
|
||||||
@@ -67,8 +56,6 @@ You can select the **Jobs** tab to see Immich processing your photos.
|
|||||||
|
|
||||||
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} />
|
<img src={require('/docs/guides/img/jobs-tab.webp').default} title="Jobs tab" width={300} />
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Review the database backup and restore process
|
## Review the database backup and restore process
|
||||||
|
|
||||||
Immich has built-in database backups. You can refer to the
|
Immich has built-in database backups. You can refer to the
|
||||||
@@ -78,8 +65,6 @@ Immich has built-in database backups. You can refer to the
|
|||||||
The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`.
|
The database only contains metadata and user information. You must setup manual backups of the images and videos stored in `UPLOAD_LOCATION`.
|
||||||
:::
|
:::
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Where to go from here?
|
## Where to go from here?
|
||||||
|
|
||||||
You may decide you'd like to install the server a different way; the Install category on the left menu provides many options.
|
You may decide you'd like to install the server a different way; the Install category on the left menu provides many options.
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
import CodeBlock from '@theme/CodeBlock';
|
|
||||||
import ExampleEnv from '!!raw-loader!../../../docker/example.env';
|
|
||||||
|
|
||||||
### Step 1 - Download the required files
|
|
||||||
|
|
||||||
Create a directory of your choice (e.g. `./immich-app`) to hold the `docker-compose.yml` and `.env` files.
|
|
||||||
|
|
||||||
```bash title="Move to the directory you created"
|
|
||||||
mkdir ./immich-app
|
|
||||||
cd ./immich-app
|
|
||||||
```
|
|
||||||
|
|
||||||
Download [`docker-compose.yml`](https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml) and [`example.env`](https://github.com/immich-app/immich/releases/latest/download/example.env) by running the following commands:
|
|
||||||
|
|
||||||
```bash title="Get docker-compose.yml file"
|
|
||||||
wget -O docker-compose.yml https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash title="Get .env file"
|
|
||||||
wget -O .env https://github.com/immich-app/immich/releases/latest/download/example.env
|
|
||||||
```
|
|
||||||
|
|
||||||
You can alternatively download these two files from your browser and move them to the directory that you created, in which case ensure that you rename `example.env` to `.env`.
|
|
||||||
|
|
||||||
### Step 2 - Populate the .env file with custom values
|
|
||||||
|
|
||||||
<CodeBlock language="bash" title="Default environmental variable content">
|
|
||||||
{ExampleEnv}
|
|
||||||
</CodeBlock>
|
|
||||||
|
|
||||||
- Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. It should be a new directory on the server with enough free space.
|
|
||||||
- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publicly exposed, so this password is only used for local authentication.
|
|
||||||
To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. `pwgen` is a handy utility for this.
|
|
||||||
- Set your timezone by uncommenting the `TZ=` line.
|
|
||||||
- Populate custom database information if necessary.
|
|
||||||
|
|
||||||
### Step 3 - Start the containers
|
|
||||||
|
|
||||||
From the directory you created in Step 1 (which should now contain your customized `docker-compose.yml` and `.env` files), run the following command to start Immich as a background service:
|
|
||||||
|
|
||||||
```bash title="Start the containers"
|
|
||||||
docker compose up -d
|
|
||||||
```
|
|
||||||
@@ -95,7 +95,7 @@ const config = {
|
|||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
to: '/docs/overview/welcome',
|
to: '/docs/overview/introduction',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
label: 'Docs',
|
label: 'Docs',
|
||||||
},
|
},
|
||||||
@@ -124,12 +124,6 @@ const config = {
|
|||||||
label: 'Discord',
|
label: 'Discord',
|
||||||
position: 'right',
|
position: 'right',
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: 'html',
|
|
||||||
position: 'right',
|
|
||||||
value:
|
|
||||||
'<a href="https://buy.immich.app" target="_blank" class="no-underline hover:no-underline"><button class="buy-button bg-immich-primary dark:bg-immich-dark-primary text-white dark:text-black rounded-xl">Buy Immich</button></a>',
|
|
||||||
},
|
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
footer: {
|
footer: {
|
||||||
@@ -140,7 +134,7 @@ const config = {
|
|||||||
items: [
|
items: [
|
||||||
{
|
{
|
||||||
label: 'Welcome',
|
label: 'Welcome',
|
||||||
to: '/docs/overview/welcome',
|
to: '/docs/overview/introduction',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Installation',
|
label: 'Installation',
|
||||||
|
|||||||
@@ -57,6 +57,6 @@
|
|||||||
"node": ">=20"
|
"node": ">=20"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -44,6 +44,11 @@ const projects: CommunityProjectProps[] = [
|
|||||||
'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.',
|
'Lightroom plugin to publish, export photos from Lightroom to Immich. Import from Immich to Lightroom is also supported.',
|
||||||
url: 'https://blog.fokuspunk.de/lrc-immich-plugin/',
|
url: 'https://blog.fokuspunk.de/lrc-immich-plugin/',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
title: 'Immich Duplicate Finder',
|
||||||
|
description: 'Webapp that uses machine learning to identify near-duplicate images.',
|
||||||
|
url: 'https://github.com/vale46n1/immich_duplicate_finder',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
title: 'Immich-Tiktok-Remover',
|
title: 'Immich-Tiktok-Remover',
|
||||||
description: 'Script to search for and remove TikTok videos from your Immich library.',
|
description: 'Script to search for and remove TikTok videos from your Immich library.',
|
||||||
|
|||||||
@@ -7,22 +7,14 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
@font-face {
|
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap');
|
||||||
font-family: 'Overpass';
|
|
||||||
src: url('/fonts/overpass/Overpass.ttf') format('truetype-variations');
|
|
||||||
font-weight: 1 999;
|
|
||||||
font-style: normal;
|
|
||||||
ascent-override: 106.25%;
|
|
||||||
size-adjust: 106.25%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@font-face {
|
body {
|
||||||
font-family: 'Overpass Mono';
|
font-family: 'Be Vietnam Pro', serif;
|
||||||
src: url('/fonts/overpass/OverpassMono.ttf') format('truetype-variations');
|
font-optical-sizing: auto;
|
||||||
font-weight: 1 999;
|
/* font-size: 1.125rem;
|
||||||
font-style: normal;
|
|
||||||
ascent-override: 106.25%;
|
ascent-override: 106.25%;
|
||||||
size-adjust: 106.25%;
|
size-adjust: 106.25%; */
|
||||||
}
|
}
|
||||||
|
|
||||||
.breadcrumbs__link {
|
.breadcrumbs__link {
|
||||||
@@ -37,7 +29,6 @@ img {
|
|||||||
|
|
||||||
/* You can override the default Infima variables here. */
|
/* You can override the default Infima variables here. */
|
||||||
:root {
|
:root {
|
||||||
font-family: 'Overpass', sans-serif;
|
|
||||||
--ifm-color-primary: #4250af;
|
--ifm-color-primary: #4250af;
|
||||||
--ifm-color-primary-dark: #4250af;
|
--ifm-color-primary-dark: #4250af;
|
||||||
--ifm-color-primary-darker: #4250af;
|
--ifm-color-primary-darker: #4250af;
|
||||||
@@ -68,12 +59,14 @@ div[class^='announcementBar_'] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.menu__link {
|
.menu__link {
|
||||||
padding: 10px 10px 10px 16px;
|
padding: 10px;
|
||||||
|
padding-left: 16px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu__list-item-collapsible {
|
.menu__list-item-collapsible {
|
||||||
|
border-radius: 10px;
|
||||||
margin-right: 16px;
|
margin-right: 16px;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
}
|
}
|
||||||
@@ -90,12 +83,3 @@ div[class*='navbar__items'] > li:has(a[class*='version-switcher-34ab39']) {
|
|||||||
code {
|
code {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
|
|
||||||
.buy-button {
|
|
||||||
padding: 8px 14px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
font-family: 'Overpass', sans-serif;
|
|
||||||
font-weight: 500;
|
|
||||||
cursor: pointer;
|
|
||||||
box-shadow: 0 0 5px 2px rgba(181, 206, 254, 0.4);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import {
|
|||||||
mdiBug,
|
mdiBug,
|
||||||
mdiCalendarToday,
|
mdiCalendarToday,
|
||||||
mdiCrosshairsOff,
|
mdiCrosshairsOff,
|
||||||
mdiCrop,
|
|
||||||
mdiDatabase,
|
mdiDatabase,
|
||||||
mdiLeadPencil,
|
mdiLeadPencil,
|
||||||
mdiLockOff,
|
mdiLockOff,
|
||||||
@@ -13,9 +12,6 @@ import {
|
|||||||
mdiTrashCan,
|
mdiTrashCan,
|
||||||
mdiWeb,
|
mdiWeb,
|
||||||
mdiWrap,
|
mdiWrap,
|
||||||
mdiCloudKeyOutline,
|
|
||||||
mdiRegex,
|
|
||||||
mdiCodeJson,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
@@ -26,53 +22,6 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri
|
|||||||
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
type Item = Omit<TimelineItem, 'done' | 'getDateLabel'> & { date: Date };
|
||||||
|
|
||||||
const items: Item[] = [
|
const items: Item[] = [
|
||||||
{
|
|
||||||
icon: mdiRegex,
|
|
||||||
iconColor: 'purple',
|
|
||||||
title: 'Zitadel Actions are cursed',
|
|
||||||
description:
|
|
||||||
"Zitadel is cursed because its custom scripting feature is executed with a JS engine that doesn't support regex named capture groups.",
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/dop251/goja',
|
|
||||||
text: 'Go JS engine',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 5, 4),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: mdiCloudKeyOutline,
|
|
||||||
iconColor: '#0078d4',
|
|
||||||
title: 'Entra is cursed',
|
|
||||||
description:
|
|
||||||
"Microsoft Entra supports PKCE, but doesn't include it in its OpenID discovery document. This leads to clients thinking PKCE isn't available.",
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/18725',
|
|
||||||
text: '#18725',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 4, 30),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: mdiCrop,
|
|
||||||
iconColor: 'tomato',
|
|
||||||
title: 'Image dimensions in EXIF metadata are cursed',
|
|
||||||
description:
|
|
||||||
'The dimensions in EXIF metadata can be different from the actual dimensions of the image, causing issues with cropping and resizing.',
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/17974',
|
|
||||||
text: '#17974',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 4, 5),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: mdiCodeJson,
|
|
||||||
iconColor: 'yellow',
|
|
||||||
title: 'YAML whitespace is cursed',
|
|
||||||
description: 'YAML whitespaces are often handled in unintuitive ways.',
|
|
||||||
link: {
|
|
||||||
url: 'https://github.com/immich-app/immich/pull/17309',
|
|
||||||
text: '#17309',
|
|
||||||
},
|
|
||||||
date: new Date(2025, 3, 1),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: mdiMicrosoftWindows,
|
icon: mdiMicrosoftWindows,
|
||||||
iconColor: '#357EC7',
|
iconColor: '#357EC7',
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import Layout from '@theme/Layout';
|
|||||||
import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
|
import { discordPath, discordViewBox } from '@site/src/components/svg-paths';
|
||||||
import ThemedImage from '@theme/ThemedImage';
|
import ThemedImage from '@theme/ThemedImage';
|
||||||
import Icon from '@mdi/react';
|
import Icon from '@mdi/react';
|
||||||
|
import { mdiAndroid } from '@mdi/js';
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
@@ -13,14 +13,11 @@ function HomepageHeader() {
|
|||||||
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
|
<div className="w-full h-[120vh] absolute left-0 top-0 backdrop-blur-3xl bg-immich-bg/40 dark:bg-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
|
<section className="text-center pt-12 sm:pt-24 bg-immich-bg/50 dark:bg-immich-dark-bg/80">
|
||||||
<a href="https://futo.org" target="_blank" rel="noopener noreferrer">
|
<ThemedImage
|
||||||
<ThemedImage
|
sources={{ dark: 'img/logomark-dark.svg', light: 'img/logomark-light.svg' }}
|
||||||
sources={{ dark: 'img/logomark-dark-with-futo.svg', light: 'img/logomark-light-with-futo.svg' }}
|
className="h-[115px] w-[115px] mb-2 antialiased rounded-none"
|
||||||
className="h-[125px] w-[125px] mb-2 antialiased rounded-none"
|
alt="Immich logo"
|
||||||
alt="Immich logo"
|
/>
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4">
|
<p className="text-3xl md:text-5xl sm:leading-tight mb-1 font-extrabold text-black/90 dark:text-white px-4">
|
||||||
Self-hosted{' '}
|
Self-hosted{' '}
|
||||||
@@ -31,7 +28,7 @@ function HomepageHeader() {
|
|||||||
solution<span className="block"></span>
|
solution<span className="block"></span>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="max-w-1/4 m-auto mt-4 px-4 text-lg text-gray-700 dark:text-gray-100">
|
<p className="max-w-1/4 m-auto mt-4 px-4">
|
||||||
Easily back up, organize, and manage your photos on your own server. Immich helps you
|
Easily back up, organize, and manage your photos on your own server. Immich helps you
|
||||||
<span className="sm:block"></span> browse, search and organize your photos and videos with ease, without
|
<span className="sm:block"></span> browse, search and organize your photos and videos with ease, without
|
||||||
sacrificing your privacy.
|
sacrificing your privacy.
|
||||||
@@ -39,21 +36,27 @@ function HomepageHeader() {
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
|
<div className="flex flex-col sm:flex-row place-items-center place-content-center mt-9 gap-4 ">
|
||||||
<Link
|
<Link
|
||||||
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold"
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary dark:bg-immich-dark-primary rounded-xl no-underline hover:no-underline text-white hover:text-gray-50 dark:text-immich-dark-bg font-bold uppercase"
|
||||||
to="docs/overview/quick-start"
|
to="docs/overview/introduction"
|
||||||
>
|
>
|
||||||
Get Started
|
Get started
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
className="flex place-items-center place-content-center py-3 px-8 border bg-white/90 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold"
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||||
to="https://demo.immich.app/"
|
to="https://demo.immich.app/"
|
||||||
>
|
>
|
||||||
Open Demo
|
Demo
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<Link
|
||||||
|
className="flex place-items-center place-content-center py-3 px-8 border bg-immich-primary/10 dark:bg-gray-300 rounded-xl hover:no-underline text-immich-primary dark:text-immich-dark-bg font-bold uppercase"
|
||||||
|
to="https://immich.store"
|
||||||
|
>
|
||||||
|
Buy Merch
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="my-12 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
||||||
<div className="my-8 flex gap-1 font-medium place-items-center place-content-center text-immich-primary dark:text-immich-dark-primary">
|
|
||||||
<Icon
|
<Icon
|
||||||
path={discordPath}
|
path={discordPath}
|
||||||
viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */
|
viewBox={discordViewBox} /* viewBox may show an error in your IDE but it is normal. */
|
||||||
@@ -116,7 +119,7 @@ export default function Home(): JSX.Element {
|
|||||||
<HomepageHeader />
|
<HomepageHeader />
|
||||||
<div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8">
|
<div className="flex flex-col place-items-center text-center place-content-center dark:bg-immich-dark-bg py-8">
|
||||||
<p>This project is available under GNU AGPL v3 license.</p>
|
<p>This project is available under GNU AGPL v3 license.</p>
|
||||||
<p className="text-sm">Privacy should not be a luxury</p>
|
<p className="text-xs">Privacy should not be a luxury</p>
|
||||||
</div>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import Link from '@docusaurus/Link';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
function HomepageHeader() {
|
function HomepageHeader() {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -78,14 +78,12 @@ import {
|
|||||||
mdiLinkEdit,
|
mdiLinkEdit,
|
||||||
mdiTagFaces,
|
mdiTagFaces,
|
||||||
mdiMovieOpenPlayOutline,
|
mdiMovieOpenPlayOutline,
|
||||||
mdiCast,
|
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import Layout from '@theme/Layout';
|
import Layout from '@theme/Layout';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Item, Timeline } from '../components/timeline';
|
import { Item, Timeline } from '../components/timeline';
|
||||||
|
|
||||||
const releases = {
|
const releases = {
|
||||||
'v1.133.0': new Date(2025, 4, 21),
|
|
||||||
'v1.130.0': new Date(2025, 2, 25),
|
'v1.130.0': new Date(2025, 2, 25),
|
||||||
'v1.127.0': new Date(2025, 1, 26),
|
'v1.127.0': new Date(2025, 1, 26),
|
||||||
'v1.122.0': new Date(2024, 11, 5),
|
'v1.122.0': new Date(2024, 11, 5),
|
||||||
@@ -218,6 +216,14 @@ const roadmap: Item[] = [
|
|||||||
iconColor: 'indianred',
|
iconColor: 'indianred',
|
||||||
title: 'Stable release',
|
title: 'Stable release',
|
||||||
description: 'Immich goes stable',
|
description: 'Immich goes stable',
|
||||||
|
getDateLabel: () => 'Planned for early 2025',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
done: false,
|
||||||
|
icon: mdiLockOutline,
|
||||||
|
iconColor: 'sandybrown',
|
||||||
|
title: 'Private/locked photos',
|
||||||
|
description: 'Private assets with extra protections',
|
||||||
getDateLabel: () => 'Planned for 2025',
|
getDateLabel: () => 'Planned for 2025',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -239,20 +245,6 @@ const roadmap: Item[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const milestones: Item[] = [
|
const milestones: Item[] = [
|
||||||
withRelease({
|
|
||||||
icon: mdiCast,
|
|
||||||
iconColor: 'aqua',
|
|
||||||
title: 'Google Cast (web)',
|
|
||||||
description: 'Cast assets to Google Cast/Chromecast compatible devices',
|
|
||||||
release: 'v1.133.0',
|
|
||||||
}),
|
|
||||||
withRelease({
|
|
||||||
icon: mdiLockOutline,
|
|
||||||
iconColor: 'sandybrown',
|
|
||||||
title: 'Private/locked photos',
|
|
||||||
description: 'Private assets with extra protections',
|
|
||||||
release: 'v1.133.0',
|
|
||||||
}),
|
|
||||||
withRelease({
|
withRelease({
|
||||||
icon: mdiFolderMultiple,
|
icon: mdiFolderMultiple,
|
||||||
iconColor: 'brown',
|
iconColor: 'brown',
|
||||||
|
|||||||
1
docs/static/_redirects
vendored
1
docs/static/_redirects
vendored
@@ -30,4 +30,3 @@
|
|||||||
/docs/guides/api-album-sync /docs/community-projects 307
|
/docs/guides/api-album-sync /docs/community-projects 307
|
||||||
/docs/guides/remove-offline-files /docs/community-projects 307
|
/docs/guides/remove-offline-files /docs/community-projects 307
|
||||||
/milestones /roadmap 307
|
/milestones /roadmap 307
|
||||||
/docs/overview/introduction /docs/overview/welcome 307
|
|
||||||
176
docs/static/archived-versions.json
vendored
176
docs/static/archived-versions.json
vendored
@@ -1,28 +1,52 @@
|
|||||||
[
|
[
|
||||||
{
|
|
||||||
"label": "v1.134.0",
|
|
||||||
"url": "https://v1.134.0.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "v1.133.1",
|
|
||||||
"url": "https://v1.133.1.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "v1.133.0",
|
|
||||||
"url": "https://v1.133.0.archive.immich.app"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"label": "v1.132.3",
|
"label": "v1.132.3",
|
||||||
"url": "https://v1.132.3.archive.immich.app"
|
"url": "https://v1.132.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.132.2",
|
||||||
|
"url": "https://v1.132.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.132.1",
|
||||||
|
"url": "https://v1.132.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.132.0",
|
||||||
|
"url": "https://v1.132.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.131.3",
|
"label": "v1.131.3",
|
||||||
"url": "https://v1.131.3.archive.immich.app"
|
"url": "https://v1.131.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.131.2",
|
||||||
|
"url": "https://v1.131.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.131.1",
|
||||||
|
"url": "https://v1.131.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.131.0",
|
||||||
|
"url": "https://v1.131.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.130.3",
|
"label": "v1.130.3",
|
||||||
"url": "https://v1.130.3.archive.immich.app"
|
"url": "https://v1.130.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.130.2",
|
||||||
|
"url": "https://v1.130.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.130.1",
|
||||||
|
"url": "https://v1.130.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.130.0",
|
||||||
|
"url": "https://v1.130.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.129.0",
|
"label": "v1.129.0",
|
||||||
"url": "https://v1.129.0.archive.immich.app"
|
"url": "https://v1.129.0.archive.immich.app"
|
||||||
@@ -39,14 +63,46 @@
|
|||||||
"label": "v1.126.1",
|
"label": "v1.126.1",
|
||||||
"url": "https://v1.126.1.archive.immich.app"
|
"url": "https://v1.126.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.126.0",
|
||||||
|
"url": "https://v1.126.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.125.7",
|
"label": "v1.125.7",
|
||||||
"url": "https://v1.125.7.archive.immich.app"
|
"url": "https://v1.125.7.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.6",
|
||||||
|
"url": "https://v1.125.6.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.5",
|
||||||
|
"url": "https://v1.125.5.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.3",
|
||||||
|
"url": "https://v1.125.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.2",
|
||||||
|
"url": "https://v1.125.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.125.1",
|
||||||
|
"url": "https://v1.125.1.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.124.2",
|
"label": "v1.124.2",
|
||||||
"url": "https://v1.124.2.archive.immich.app"
|
"url": "https://v1.124.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.124.1",
|
||||||
|
"url": "https://v1.124.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.124.0",
|
||||||
|
"url": "https://v1.124.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.123.0",
|
"label": "v1.123.0",
|
||||||
"url": "https://v1.123.0.archive.immich.app"
|
"url": "https://v1.123.0.archive.immich.app"
|
||||||
@@ -55,6 +111,18 @@
|
|||||||
"label": "v1.122.3",
|
"label": "v1.122.3",
|
||||||
"url": "https://v1.122.3.archive.immich.app"
|
"url": "https://v1.122.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.122.2",
|
||||||
|
"url": "https://v1.122.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.122.1",
|
||||||
|
"url": "https://v1.122.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.122.0",
|
||||||
|
"url": "https://v1.122.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.121.0",
|
"label": "v1.121.0",
|
||||||
"url": "https://v1.121.0.archive.immich.app"
|
"url": "https://v1.121.0.archive.immich.app"
|
||||||
@@ -63,14 +131,34 @@
|
|||||||
"label": "v1.120.2",
|
"label": "v1.120.2",
|
||||||
"url": "https://v1.120.2.archive.immich.app"
|
"url": "https://v1.120.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.120.1",
|
||||||
|
"url": "https://v1.120.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.120.0",
|
||||||
|
"url": "https://v1.120.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.119.1",
|
"label": "v1.119.1",
|
||||||
"url": "https://v1.119.1.archive.immich.app"
|
"url": "https://v1.119.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.119.0",
|
||||||
|
"url": "https://v1.119.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.118.2",
|
"label": "v1.118.2",
|
||||||
"url": "https://v1.118.2.archive.immich.app"
|
"url": "https://v1.118.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.118.1",
|
||||||
|
"url": "https://v1.118.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.118.0",
|
||||||
|
"url": "https://v1.118.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.117.0",
|
"label": "v1.117.0",
|
||||||
"url": "https://v1.117.0.archive.immich.app"
|
"url": "https://v1.117.0.archive.immich.app"
|
||||||
@@ -79,6 +167,14 @@
|
|||||||
"label": "v1.116.2",
|
"label": "v1.116.2",
|
||||||
"url": "https://v1.116.2.archive.immich.app"
|
"url": "https://v1.116.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.116.1",
|
||||||
|
"url": "https://v1.116.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.116.0",
|
||||||
|
"url": "https://v1.116.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.115.0",
|
"label": "v1.115.0",
|
||||||
"url": "https://v1.115.0.archive.immich.app"
|
"url": "https://v1.115.0.archive.immich.app"
|
||||||
@@ -91,10 +187,18 @@
|
|||||||
"label": "v1.113.1",
|
"label": "v1.113.1",
|
||||||
"url": "https://v1.113.1.archive.immich.app"
|
"url": "https://v1.113.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.113.0",
|
||||||
|
"url": "https://v1.113.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.112.1",
|
"label": "v1.112.1",
|
||||||
"url": "https://v1.112.1.archive.immich.app"
|
"url": "https://v1.112.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.112.0",
|
||||||
|
"url": "https://v1.112.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.111.0",
|
"label": "v1.111.0",
|
||||||
"url": "https://v1.111.0.archive.immich.app"
|
"url": "https://v1.111.0.archive.immich.app"
|
||||||
@@ -107,6 +211,14 @@
|
|||||||
"label": "v1.109.2",
|
"label": "v1.109.2",
|
||||||
"url": "https://v1.109.2.archive.immich.app"
|
"url": "https://v1.109.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.109.1",
|
||||||
|
"url": "https://v1.109.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.109.0",
|
||||||
|
"url": "https://v1.109.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.108.0",
|
"label": "v1.108.0",
|
||||||
"url": "https://v1.108.0.archive.immich.app"
|
"url": "https://v1.108.0.archive.immich.app"
|
||||||
@@ -115,14 +227,38 @@
|
|||||||
"label": "v1.107.2",
|
"label": "v1.107.2",
|
||||||
"url": "https://v1.107.2.archive.immich.app"
|
"url": "https://v1.107.2.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.107.1",
|
||||||
|
"url": "https://v1.107.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.107.0",
|
||||||
|
"url": "https://v1.107.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.106.4",
|
"label": "v1.106.4",
|
||||||
"url": "https://v1.106.4.archive.immich.app"
|
"url": "https://v1.106.4.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.106.3",
|
||||||
|
"url": "https://v1.106.3.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.106.2",
|
||||||
|
"url": "https://v1.106.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.106.1",
|
||||||
|
"url": "https://v1.106.1.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.105.1",
|
"label": "v1.105.1",
|
||||||
"url": "https://v1.105.1.archive.immich.app"
|
"url": "https://v1.105.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.105.0",
|
||||||
|
"url": "https://v1.105.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.104.0",
|
"label": "v1.104.0",
|
||||||
"url": "https://v1.104.0.archive.immich.app"
|
"url": "https://v1.104.0.archive.immich.app"
|
||||||
@@ -131,10 +267,26 @@
|
|||||||
"label": "v1.103.1",
|
"label": "v1.103.1",
|
||||||
"url": "https://v1.103.1.archive.immich.app"
|
"url": "https://v1.103.1.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.103.0",
|
||||||
|
"url": "https://v1.103.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.102.3",
|
"label": "v1.102.3",
|
||||||
"url": "https://v1.102.3.archive.immich.app"
|
"url": "https://v1.102.3.archive.immich.app"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.102.2",
|
||||||
|
"url": "https://v1.102.2.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.102.1",
|
||||||
|
"url": "https://v1.102.1.archive.immich.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "v1.102.0",
|
||||||
|
"url": "https://v1.102.0.archive.immich.app"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"label": "v1.101.0",
|
"label": "v1.101.0",
|
||||||
"url": "https://v1.101.0.archive.immich.app"
|
"url": "https://v1.101.0.archive.immich.app"
|
||||||
|
|||||||
BIN
docs/static/fonts/overpass/Overpass-Italic.ttf
vendored
BIN
docs/static/fonts/overpass/Overpass-Italic.ttf
vendored
Binary file not shown.
BIN
docs/static/fonts/overpass/Overpass.ttf
vendored
BIN
docs/static/fonts/overpass/Overpass.ttf
vendored
Binary file not shown.
BIN
docs/static/fonts/overpass/OverpassMono.ttf
vendored
BIN
docs/static/fonts/overpass/OverpassMono.ttf
vendored
Binary file not shown.
22
docs/static/img/logomark-dark-with-futo.svg
vendored
22
docs/static/img/logomark-dark-with-futo.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 75 KiB |
21
docs/static/img/logomark-light-with-futo.svg
vendored
21
docs/static/img/logomark-light-with-futo.svg
vendored
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 75 KiB |
@@ -1 +1 @@
|
|||||||
22.16.0
|
22.14.0
|
||||||
|
|||||||
@@ -28,28 +28,16 @@ services:
|
|||||||
extra_hosts:
|
extra_hosts:
|
||||||
- 'auth-server:host-gateway'
|
- 'auth-server:host-gateway'
|
||||||
depends_on:
|
depends_on:
|
||||||
redis:
|
- database
|
||||||
condition: service_started
|
|
||||||
database:
|
|
||||||
condition: service_healthy
|
|
||||||
ports:
|
ports:
|
||||||
- 2285:2285
|
- 2285:2285
|
||||||
|
|
||||||
redis:
|
|
||||||
image: redis:6.2-alpine@sha256:3211c33a618c457e5d241922c975dbc4f446d0bdb2dc75694f5573ef8e2d01fa
|
|
||||||
|
|
||||||
database:
|
database:
|
||||||
image: ghcr.io/immich-app/postgres:14-vectorchord0.3.0@sha256:9c704fb49ce27549df00f1b096cc93f8b0c959ef087507704d74954808f78a82
|
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:739cdd626151ff1f796dc95a6591b55a714f341c737e27f045019ceabf8e8c52
|
||||||
command: -c fsync=off -c shared_preload_libraries=vchord.so -c config_file=/var/lib/postgresql/data/postgresql.conf
|
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_PASSWORD: postgres
|
POSTGRES_PASSWORD: postgres
|
||||||
POSTGRES_USER: postgres
|
POSTGRES_USER: postgres
|
||||||
POSTGRES_DB: immich
|
POSTGRES_DB: immich
|
||||||
ports:
|
ports:
|
||||||
- 5435:5432
|
- 5435:5432
|
||||||
healthcheck:
|
|
||||||
test: ['CMD-SHELL', 'pg_isready -U postgres -d immich']
|
|
||||||
interval: 1s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 30
|
|
||||||
start_period: 10s
|
|
||||||
|
|||||||
1552
e2e/package-lock.json
generated
1552
e2e/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "immich-e2e",
|
"name": "immich-e2e",
|
||||||
"version": "1.134.0",
|
"version": "1.132.3",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -25,21 +25,21 @@
|
|||||||
"@immich/sdk": "file:../open-api/typescript-sdk",
|
"@immich/sdk": "file:../open-api/typescript-sdk",
|
||||||
"@playwright/test": "^1.44.1",
|
"@playwright/test": "^1.44.1",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.15.29",
|
"@types/node": "^22.14.1",
|
||||||
"@types/oidc-provider": "^9.0.0",
|
"@types/oidc-provider": "^8.5.1",
|
||||||
"@types/pg": "^8.15.1",
|
"@types/pg": "^8.11.0",
|
||||||
"@types/pngjs": "^6.0.4",
|
"@types/pngjs": "^6.0.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"@vitest/coverage-v8": "^3.0.0",
|
"@vitest/coverage-v8": "^3.0.0",
|
||||||
"eslint": "^9.14.0",
|
"eslint": "^9.14.0",
|
||||||
"eslint-config-prettier": "^10.0.0",
|
"eslint-config-prettier": "^10.0.0",
|
||||||
"eslint-plugin-prettier": "^5.1.3",
|
"eslint-plugin-prettier": "^5.1.3",
|
||||||
"eslint-plugin-unicorn": "^59.0.0",
|
"eslint-plugin-unicorn": "^57.0.0",
|
||||||
"exiftool-vendored": "^28.3.1",
|
"exiftool-vendored": "^28.3.1",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
"jose": "^5.6.3",
|
"jose": "^5.6.3",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"oidc-provider": "^9.0.0",
|
"oidc-provider": "^8.5.1",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"pngjs": "^7.0.0",
|
"pngjs": "^7.0.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
@@ -52,6 +52,6 @@
|
|||||||
"vitest": "^3.0.0"
|
"vitest": "^3.0.0"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.16.0"
|
"node": "22.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,6 +46,38 @@ describe('/activities', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /activities', () => {
|
describe('GET /activities', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/activities');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require an albumId', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/activities')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid albumId', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/activities')
|
||||||
|
.query({ albumId: uuidDto.invalid })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid assetId', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/activities')
|
||||||
|
.query({ albumId: uuidDto.notFound, assetId: uuidDto.invalid })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['assetId must be a UUID'])));
|
||||||
|
});
|
||||||
|
|
||||||
it('should start off empty', async () => {
|
it('should start off empty', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/activities')
|
.get('/activities')
|
||||||
@@ -160,6 +192,30 @@ describe('/activities', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /activities', () => {
|
describe('POST /activities', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/activities');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require an albumId', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/activities')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ albumId: uuidDto.invalid });
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expect.arrayContaining(['albumId must be a UUID'])));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a comment when type is comment', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/activities')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ albumId: uuidDto.notFound, type: 'comment', comment: null });
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['comment must be a string', 'comment should not be empty']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should add a comment to an album', async () => {
|
it('should add a comment to an album', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/activities')
|
.post('/activities')
|
||||||
@@ -274,6 +330,20 @@ describe('/activities', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /activities/:id', () => {
|
describe('DELETE /activities/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).delete(`/activities/${uuidDto.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/activities/${uuidDto.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should remove a comment from an album', async () => {
|
it('should remove a comment from an album', async () => {
|
||||||
const reaction = await createActivity({
|
const reaction = await createActivity({
|
||||||
albumId: album.id,
|
albumId: album.id,
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@@ -128,6 +128,28 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums', () => {
|
describe('GET /albums', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/albums');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid shared param', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?shared=invalid')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['shared must be a boolean value']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid assetId param', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/albums?assetId=invalid')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toEqual(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['assetId must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
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}?withoutAssets=false`)
|
||||||
@@ -301,6 +323,12 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums/:id', () => {
|
describe('GET /albums/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/albums/${user1Albums[0].id}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
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}?withoutAssets=false`)
|
||||||
@@ -393,6 +421,12 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /albums/statistics', () => {
|
describe('GET /albums/statistics', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/albums/statistics');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return total count of albums the user has access to', async () => {
|
it('should return total count of albums the user has access to', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/albums/statistics')
|
.get('/albums/statistics')
|
||||||
@@ -404,6 +438,12 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /albums', () => {
|
describe('POST /albums', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/albums').send({ albumName: 'New album' });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should create an album', async () => {
|
it('should create an album', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/albums')
|
.post('/albums')
|
||||||
@@ -428,18 +468,15 @@ describe('/albums', () => {
|
|||||||
order: AssetOrder.Desc,
|
order: AssetOrder.Desc,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not be able to share album with owner', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.post('/albums')
|
|
||||||
.send({ albumName: 'New album', albumUsers: [{ role: AlbumUserRole.Editor, userId: user1.userId }] })
|
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`);
|
|
||||||
expect(status).toBe(400);
|
|
||||||
expect(body).toEqual(errorDto.badRequest('Cannot share album with owner'));
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /albums/:id/assets', () => {
|
describe('PUT /albums/:id/assets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/assets`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to add own asset to own album', async () => {
|
it('should be able to add own asset to own album', async () => {
|
||||||
const asset = await utils.createAsset(user1.accessToken);
|
const asset = await utils.createAsset(user1.accessToken);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -489,6 +526,14 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PATCH /albums/:id', () => {
|
describe('PATCH /albums/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.patch(`/albums/${uuidDto.notFound}`)
|
||||||
|
.send({ albumName: 'New album name' });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should update an album', async () => {
|
it('should update an album', async () => {
|
||||||
const album = await utils.createAlbum(user1.accessToken, {
|
const album = await utils.createAlbum(user1.accessToken, {
|
||||||
albumName: 'New album',
|
albumName: 'New album',
|
||||||
@@ -531,6 +576,15 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /albums/:id/assets', () => {
|
describe('DELETE /albums/:id/assets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/albums/${user1Albums[0].id}/assets`)
|
||||||
|
.send({ ids: [user1Asset1.id] });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authorization', async () => {
|
it('should require authorization', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/albums/${user1Albums[1].id}/assets`)
|
.delete(`/albums/${user1Albums[1].id}/assets`)
|
||||||
@@ -625,6 +679,13 @@ describe('/albums', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/albums/${user1Albums[0].id}/users`).send({ sharedUserIds: [] });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should be able to add user to own album', async () => {
|
it('should be able to add user to own album', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/albums/${album.id}/users`)
|
.put(`/albums/${album.id}/users`)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
|
import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, utils } from 'src/utils';
|
import { app, asBearerAuth, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
@@ -24,6 +24,12 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /api-keys', () => {
|
describe('POST /api-keys', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not work without permission', async () => {
|
it('should not work without permission', async () => {
|
||||||
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
|
const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]);
|
||||||
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
|
const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' });
|
||||||
@@ -93,6 +99,12 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /api-keys', () => {
|
describe('GET /api-keys', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/api-keys');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should start off empty', async () => {
|
it('should start off empty', async () => {
|
||||||
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
|
const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toEqual([]);
|
expect(body).toEqual([]);
|
||||||
@@ -113,6 +125,12 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /api-keys/:id', () => {
|
describe('GET /api-keys/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authorization', async () => {
|
it('should require authorization', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -122,6 +140,14 @@ describe('/api-keys', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/api-keys/${uuidDto.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should get api key details', async () => {
|
it('should get api key details', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -139,30 +165,42 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /api-keys/:id', () => {
|
describe('PUT /api-keys/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authorization', async () => {
|
it('should require authorization', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/api-keys/${apiKey.id}`)
|
.put(`/api-keys/${apiKey.id}`)
|
||||||
.send({ name: 'new name', permissions: [Permission.All] })
|
.send({ name: 'new name' })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/api-keys/${uuidDto.invalid}`)
|
||||||
|
.send({ name: 'new name' })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should update api key details', async () => {
|
it('should update api key details', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/api-keys/${apiKey.id}`)
|
.put(`/api-keys/${apiKey.id}`)
|
||||||
.send({
|
.send({ name: 'new name' })
|
||||||
name: 'new name',
|
|
||||||
permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate],
|
|
||||||
})
|
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`);
|
.set('Authorization', `Bearer ${user.accessToken}`);
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual({
|
||||||
id: expect.any(String),
|
id: expect.any(String),
|
||||||
name: 'new name',
|
name: 'new name',
|
||||||
permissions: [Permission.ActivityCreate, Permission.ActivityRead, Permission.ActivityUpdate],
|
permissions: [Permission.All],
|
||||||
createdAt: expect.any(String),
|
createdAt: expect.any(String),
|
||||||
updatedAt: expect.any(String),
|
updatedAt: expect.any(String),
|
||||||
});
|
});
|
||||||
@@ -170,6 +208,12 @@ describe('/api-keys', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /api-keys/:id', () => {
|
describe('DELETE /api-keys/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require authorization', async () => {
|
it('should require authorization', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
@@ -179,6 +223,14 @@ describe('/api-keys', () => {
|
|||||||
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
expect(body).toEqual(errorDto.badRequest('API Key not found'));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/api-keys/${uuidDto.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should delete an api key', async () => {
|
it('should delete an api key', async () => {
|
||||||
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
const { apiKey } = await create(user.accessToken, [Permission.All]);
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
AssetMediaStatus,
|
AssetMediaStatus,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
AssetVisibility,
|
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
getMyUser,
|
getMyUser,
|
||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
@@ -23,9 +22,27 @@ import { app, asBearerAuth, tempDir, TEN_TIMES, testAssetDir, utils } from 'src/
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const makeUploadDto = (options?: { omit: string }): Record<string, any> => {
|
||||||
|
const dto: Record<string, any> = {
|
||||||
|
deviceAssetId: 'example-image',
|
||||||
|
deviceId: 'TEST',
|
||||||
|
fileCreatedAt: new Date().toISOString(),
|
||||||
|
fileModifiedAt: new Date().toISOString(),
|
||||||
|
isFavorite: 'testing',
|
||||||
|
duration: '0:00:00.000000',
|
||||||
|
};
|
||||||
|
|
||||||
|
const omit = options?.omit;
|
||||||
|
if (omit) {
|
||||||
|
delete dto[omit];
|
||||||
|
}
|
||||||
|
|
||||||
|
return dto;
|
||||||
|
};
|
||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||||
const facesAssetDir = `${testAssetDir}/metadata/faces`;
|
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
const filepath = join(tempDir, filename);
|
const filepath = join(tempDir, filename);
|
||||||
@@ -120,9 +137,9 @@ describe('/asset', () => {
|
|||||||
// stats
|
// stats
|
||||||
utils.createAsset(statsUser.accessToken),
|
utils.createAsset(statsUser.accessToken),
|
||||||
utils.createAsset(statsUser.accessToken, { isFavorite: true }),
|
utils.createAsset(statsUser.accessToken, { isFavorite: true }),
|
||||||
utils.createAsset(statsUser.accessToken, { visibility: AssetVisibility.Archive }),
|
utils.createAsset(statsUser.accessToken, { isArchived: true }),
|
||||||
utils.createAsset(statsUser.accessToken, {
|
utils.createAsset(statsUser.accessToken, {
|
||||||
visibility: AssetVisibility.Archive,
|
isArchived: true,
|
||||||
isFavorite: true,
|
isFavorite: true,
|
||||||
assetData: { filename: 'example.mp4' },
|
assetData: { filename: 'example.mp4' },
|
||||||
}),
|
}),
|
||||||
@@ -143,6 +160,13 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/:id/original', () => {
|
describe('GET /assets/:id/original', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should download the file', async () => {
|
it('should download the file', async () => {
|
||||||
const response = await request(app)
|
const response = await request(app)
|
||||||
.get(`/assets/${user1Assets[0].id}/original`)
|
.get(`/assets/${user1Assets[0].id}/original`)
|
||||||
@@ -154,6 +178,20 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/:id', () => {
|
describe('GET /assets/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid id', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/assets/${uuidDto.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should require access', async () => {
|
it('should require access', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/assets/${user2Assets[0].id}`)
|
.get(`/assets/${user2Assets[0].id}`)
|
||||||
@@ -186,22 +224,31 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('faces', () => {
|
it('should get the asset faces', async () => {
|
||||||
const metadataFaceTests = [
|
const config = await utils.getSystemConfig(admin.accessToken);
|
||||||
{
|
config.metadata.faces.import = true;
|
||||||
description: 'without orientation',
|
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
|
// asset faces
|
||||||
|
const facesAsset = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
filename: 'portrait.jpg',
|
filename: 'portrait.jpg',
|
||||||
|
bytes: await readFile(facesAssetFilepath),
|
||||||
},
|
},
|
||||||
{
|
});
|
||||||
description: 'adjusting face regions to orientation',
|
|
||||||
filename: 'portrait-orientation-6.jpg',
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
|
||||||
},
|
|
||||||
];
|
const { status, body } = await request(app)
|
||||||
// should produce same resulting face region coordinates for any orientation
|
.get(`/assets/${facesAsset.id}`)
|
||||||
const expectedFaces = [
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body.id).toEqual(facesAsset.id);
|
||||||
|
expect(body.people).toMatchObject([
|
||||||
{
|
{
|
||||||
name: 'Marie Curie',
|
name: 'Marie Curie',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
|
thumbnailPath: '',
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
faces: [
|
faces: [
|
||||||
{
|
{
|
||||||
@@ -218,6 +265,7 @@ describe('/asset', () => {
|
|||||||
{
|
{
|
||||||
name: 'Pierre Curie',
|
name: 'Pierre Curie',
|
||||||
birthDate: null,
|
birthDate: null,
|
||||||
|
thumbnailPath: '',
|
||||||
isHidden: false,
|
isHidden: false,
|
||||||
faces: [
|
faces: [
|
||||||
{
|
{
|
||||||
@@ -231,30 +279,7 @@ describe('/asset', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
]);
|
||||||
|
|
||||||
it.each(metadataFaceTests)('should get the asset faces from $filename $description', async ({ filename }) => {
|
|
||||||
const config = await utils.getSystemConfig(admin.accessToken);
|
|
||||||
config.metadata.faces.import = true;
|
|
||||||
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
|
|
||||||
const facesAsset = await utils.createAsset(admin.accessToken, {
|
|
||||||
assetData: {
|
|
||||||
filename,
|
|
||||||
bytes: await readFile(`${facesAssetDir}/${filename}`),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
|
|
||||||
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get(`/assets/${facesAsset.id}`)
|
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
expect(body.id).toEqual(facesAsset.id);
|
|
||||||
expect(body.people).toMatchObject(expectedFaces);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
it('should work with a shared link', async () => {
|
||||||
@@ -308,7 +333,7 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('disallows viewing archived assets', async () => {
|
it('disallows viewing archived assets', async () => {
|
||||||
const asset = await utils.createAsset(user1.accessToken, { visibility: AssetVisibility.Archive });
|
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
|
||||||
|
|
||||||
const { status } = await request(app)
|
const { status } = await request(app)
|
||||||
.get(`/assets/${asset.id}`)
|
.get(`/assets/${asset.id}`)
|
||||||
@@ -329,6 +354,13 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/statistics', () => {
|
describe('GET /assets/statistics', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/assets/statistics');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return stats of all assets', async () => {
|
it('should return stats of all assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/assets/statistics')
|
.get('/assets/statistics')
|
||||||
@@ -352,7 +384,7 @@ describe('/asset', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/assets/statistics')
|
.get('/assets/statistics')
|
||||||
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
||||||
.query({ visibility: AssetVisibility.Archive });
|
.query({ isArchived: true });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
expect(body).toEqual({ images: 1, videos: 1, total: 2 });
|
||||||
@@ -362,7 +394,7 @@ describe('/asset', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/assets/statistics')
|
.get('/assets/statistics')
|
||||||
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
||||||
.query({ isFavorite: true, visibility: AssetVisibility.Archive });
|
.query({ isFavorite: true, isArchived: true });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({ images: 0, videos: 1, total: 1 });
|
expect(body).toEqual({ images: 0, videos: 1, total: 1 });
|
||||||
@@ -372,7 +404,7 @@ describe('/asset', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/assets/statistics')
|
.get('/assets/statistics')
|
||||||
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
.set('Authorization', `Bearer ${statsUser.accessToken}`)
|
||||||
.query({ isFavorite: false, visibility: AssetVisibility.Timeline });
|
.query({ isFavorite: false, isArchived: false });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
|
expect(body).toEqual({ images: 1, videos: 0, total: 1 });
|
||||||
@@ -393,6 +425,13 @@ describe('/asset', () => {
|
|||||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/assets/random');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/assets/random')
|
.get('/assets/random')
|
||||||
@@ -428,9 +467,31 @@ describe('/asset', () => {
|
|||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
expect(body).toEqual([expect.objectContaining({ id: user2Assets[0].id })]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return error', async () => {
|
||||||
|
const { status } = await request(app)
|
||||||
|
.get('/assets/random?count=ABC')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /assets/:id', () => {
|
describe('PUT /assets/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/assets/:${uuidDto.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid id', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${uuidDto.invalid}`)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
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}`)
|
||||||
@@ -458,7 +519,7 @@ describe('/asset', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/assets/${user1Assets[0].id}`)
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
.set('Authorization', `Bearer ${user1.accessToken}`)
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
.send({ visibility: AssetVisibility.Archive });
|
.send({ isArchived: true });
|
||||||
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
|
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
|
||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
@@ -558,6 +619,28 @@ describe('/asset', () => {
|
|||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject invalid gps coordinates', async () => {
|
||||||
|
for (const test of [
|
||||||
|
{ latitude: 12 },
|
||||||
|
{ longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: 'abc' },
|
||||||
|
{ latitude: 'abc', longitude: 12 },
|
||||||
|
{ latitude: null, longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: null },
|
||||||
|
{ latitude: 91, longitude: 12 },
|
||||||
|
{ latitude: -91, longitude: 12 },
|
||||||
|
{ latitude: 12, longitude: -181 },
|
||||||
|
{ latitude: 12, longitude: 181 },
|
||||||
|
]) {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.send(test)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should update gps data', async () => {
|
it('should update gps data', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/assets/${user1Assets[0].id}`)
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
@@ -629,6 +712,17 @@ describe('/asset', () => {
|
|||||||
expect(status).toEqual(200);
|
expect(status).toEqual(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reject invalid rating', async () => {
|
||||||
|
for (const test of [{ rating: 7 }, { rating: 3.5 }, { rating: null }]) {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
|
.send(test)
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('should return tagged people', async () => {
|
it('should return tagged people', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/assets/${user1Assets[0].id}`)
|
.put(`/assets/${user1Assets[0].id}`)
|
||||||
@@ -652,6 +746,25 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('DELETE /assets', () => {
|
describe('DELETE /assets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/assets`)
|
||||||
|
.send({ ids: [uuidDto.notFound] });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should require a valid uuid', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.delete(`/assets`)
|
||||||
|
.send({ ids: [uuidDto.invalid] })
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(['each value in ids must be a UUID']));
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error when the id is not found', async () => {
|
it('should throw an error when the id is not found', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.delete(`/assets`)
|
.delete(`/assets`)
|
||||||
@@ -764,6 +877,13 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/:id/thumbnail', () => {
|
describe('GET /assets/:id/thumbnail', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not include gps data for webp thumbnails', async () => {
|
it('should not include gps data for webp thumbnails', async () => {
|
||||||
await utils.waitForWebsocketEvent({
|
await utils.waitForWebsocketEvent({
|
||||||
event: 'assetUpload',
|
event: 'assetUpload',
|
||||||
@@ -799,6 +919,13 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /assets/:id/original', () => {
|
describe('GET /assets/:id/original', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should download the original', async () => {
|
it('should download the original', async () => {
|
||||||
const { status, body, type } = await request(app)
|
const { status, body, type } = await request(app)
|
||||||
.get(`/assets/${locationAsset.id}/original`)
|
.get(`/assets/${locationAsset.id}/original`)
|
||||||
@@ -819,9 +946,43 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('PUT /assets', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put('/assets');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /assets', () => {
|
describe('POST /assets', () => {
|
||||||
beforeAll(setupTests, 30_000);
|
beforeAll(setupTests, 30_000);
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/assets`);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
{ should: 'require `deviceAssetId`', dto: { ...makeUploadDto({ omit: 'deviceAssetId' }) } },
|
||||||
|
{ should: 'require `deviceId`', dto: { ...makeUploadDto({ omit: 'deviceId' }) } },
|
||||||
|
{ should: 'require `fileCreatedAt`', dto: { ...makeUploadDto({ omit: 'fileCreatedAt' }) } },
|
||||||
|
{ should: 'require `fileModifiedAt`', dto: { ...makeUploadDto({ omit: 'fileModifiedAt' }) } },
|
||||||
|
{ should: 'require `duration`', dto: { ...makeUploadDto({ omit: 'duration' }) } },
|
||||||
|
{ should: 'throw if `isFavorite` is not a boolean', dto: { ...makeUploadDto(), isFavorite: 'not-a-boolean' } },
|
||||||
|
{ should: 'throw if `isVisible` is not a boolean', dto: { ...makeUploadDto(), isVisible: 'not-a-boolean' } },
|
||||||
|
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
|
||||||
|
])('should $should', async ({ dto }) => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/assets')
|
||||||
|
.set('Authorization', `Bearer ${user1.accessToken}`)
|
||||||
|
.attach('assetData', makeRandomImage(), 'example.png')
|
||||||
|
.field(dto);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
const tests = [
|
const tests = [
|
||||||
{
|
{
|
||||||
input: 'formats/avif/8bit-sRGB.avif',
|
input: 'formats/avif/8bit-sRGB.avif',
|
||||||
@@ -1083,21 +1244,31 @@ describe('/asset', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
it.each(tests)(`should upload and generate a thumbnail for different file types`, async ({ input, expected }) => {
|
it(`should upload and generate a thumbnail for different file types`, async () => {
|
||||||
const filepath = join(testAssetDir, input);
|
// upload in parallel
|
||||||
const response = await utils.createAsset(admin.accessToken, {
|
const assets = await Promise.all(
|
||||||
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
tests.map(async ({ input }) => {
|
||||||
});
|
const filepath = join(testAssetDir, input);
|
||||||
|
return utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
expect(response.status).toBe(AssetMediaStatus.Created);
|
for (const { id, status } of assets) {
|
||||||
const id = response.id;
|
expect(status).toBe(AssetMediaStatus.Created);
|
||||||
// longer timeout as the thumbnail generation from full-size raw files can take a while
|
// longer timeout as the thumbnail generation from full-size raw files can take a while
|
||||||
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
|
||||||
|
}
|
||||||
|
|
||||||
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
for (const [i, { id }] of assets.entries()) {
|
||||||
expect(asset.exifInfo).toBeDefined();
|
const { expected } = tests[i];
|
||||||
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
const asset = await utils.getAssetInfo(admin.accessToken, id);
|
||||||
expect(asset).toMatchObject(expected);
|
|
||||||
|
expect(asset.exifInfo).toBeDefined();
|
||||||
|
expect(asset.exifInfo).toMatchObject(expected.exifInfo);
|
||||||
|
expect(asset).toMatchObject(expected);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle a duplicate', async () => {
|
it('should handle a duplicate', async () => {
|
||||||
|
|||||||
@@ -19,6 +19,17 @@ describe(`/auth/admin-sign-up`, () => {
|
|||||||
expect(body).toEqual(signupResponseDto.admin);
|
expect(body).toEqual(signupResponseDto.admin);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should sign up the admin with a local domain', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/admin-sign-up')
|
||||||
|
.send({ ...signupDto.admin, email: 'admin@local' });
|
||||||
|
expect(status).toEqual(201);
|
||||||
|
expect(body).toEqual({
|
||||||
|
...signupResponseDto.admin,
|
||||||
|
email: 'admin@local',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should not allow a second admin to sign up', async () => {
|
it('should not allow a second admin to sign up', async () => {
|
||||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||||
|
|
||||||
@@ -46,6 +57,22 @@ describe('/auth/*', () => {
|
|||||||
expect(body).toEqual(errorDto.incorrectLogin);
|
expect(body).toEqual(errorDto.incorrectLogin);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const key of Object.keys(loginDto.admin)) {
|
||||||
|
it(`should not allow null ${key}`, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/auth/login')
|
||||||
|
.send({ ...loginDto.admin, [key]: null });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest());
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reject an invalid email', async () => {
|
||||||
|
const { status, body } = await request(app).post('/auth/login').send({ email: [], password });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.invalidEmail);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it('should accept a correct password', async () => {
|
it('should accept a correct password', async () => {
|
||||||
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
const { status, body, headers } = await request(app).post('/auth/login').send({ email, password });
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
@@ -100,6 +127,14 @@ describe('/auth/*', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /auth/change-password', () => {
|
describe('POST /auth/change-password', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/auth/change-password`)
|
||||||
|
.send({ password, newPassword: 'Password1234' });
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should require the current password', async () => {
|
it('should require the current password', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/auth/change-password`)
|
.post(`/auth/change-password`)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
|
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
|
||||||
import { readFile, writeFile } from 'node:fs/promises';
|
import { readFile, writeFile } from 'node:fs/promises';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
import { app, tempDir, utils } from 'src/utils';
|
import { app, tempDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, describe, expect, it } from 'vitest';
|
import { beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -16,6 +17,15 @@ describe('/download', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /download/info', () => {
|
describe('POST /download/info', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/download/info`)
|
||||||
|
.send({ assetIds: [asset1.id] });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should download info', async () => {
|
it('should download info', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/download/info')
|
.post('/download/info')
|
||||||
@@ -32,6 +42,15 @@ describe('/download', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /download/archive', () => {
|
describe('POST /download/archive', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/download/archive`)
|
||||||
|
.send({ assetIds: [asset1.id, asset2.id] });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should download an archive', async () => {
|
it('should download an archive', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/download/archive')
|
.post('/download/archive')
|
||||||
|
|||||||
@@ -78,7 +78,7 @@ describe('/jobs', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
await utils.jobCommand(admin.accessToken, JobName.MetadataExtraction, {
|
||||||
command: JobCommand.Empty,
|
command: JobCommand.Clear,
|
||||||
force: false,
|
force: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -160,7 +160,7 @@ describe('/jobs', () => {
|
|||||||
expect(assetBefore.thumbhash).toBeNull();
|
expect(assetBefore.thumbhash).toBeNull();
|
||||||
|
|
||||||
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
await utils.jobCommand(admin.accessToken, JobName.ThumbnailGeneration, {
|
||||||
command: JobCommand.Empty,
|
command: JobCommand.Clear,
|
||||||
force: false,
|
force: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AssetVisibility, LoginResponseDto } from '@immich/sdk';
|
import { LoginResponseDto } from '@immich/sdk';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { basename, join } from 'node:path';
|
import { basename, join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
@@ -44,7 +44,7 @@ describe('/map', () => {
|
|||||||
it('should get map markers for all non-archived assets', async () => {
|
it('should get map markers for all non-archived assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/map/markers')
|
.get('/map/markers')
|
||||||
.query({ visibility: AssetVisibility.Timeline })
|
.query({ isArchived: false })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
|
|||||||
@@ -5,6 +5,22 @@ import { app, asBearerAuth, utils } from 'src/utils';
|
|||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
const invalidBirthday = [
|
||||||
|
{
|
||||||
|
birthDate: 'false',
|
||||||
|
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
birthDate: '123567',
|
||||||
|
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
birthDate: 123_567,
|
||||||
|
response: ['birthDate must be a string in the format yyyy-MM-dd', 'Birth date cannot be in the future'],
|
||||||
|
},
|
||||||
|
{ birthDate: '9999-01-01', response: ['Birth date cannot be in the future'] },
|
||||||
|
];
|
||||||
|
|
||||||
describe('/people', () => {
|
describe('/people', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
let visiblePerson: PersonResponseDto;
|
let visiblePerson: PersonResponseDto;
|
||||||
@@ -42,6 +58,14 @@ describe('/people', () => {
|
|||||||
|
|
||||||
describe('GET /people', () => {
|
describe('GET /people', () => {
|
||||||
beforeEach(async () => {});
|
beforeEach(async () => {});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/people');
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return all people (including hidden)', async () => {
|
it('should return all people (including hidden)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/people')
|
.get('/people')
|
||||||
@@ -93,6 +117,13 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /people/:id', () => {
|
describe('GET /people/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/people/${uuidDto.notFound}`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw error if person with id does not exist', async () => {
|
it('should throw error if person with id does not exist', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/people/${uuidDto.notFound}`)
|
.get(`/people/${uuidDto.notFound}`)
|
||||||
@@ -113,6 +144,13 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /people/:id/statistics', () => {
|
describe('GET /people/:id/statistics', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get(`/people/${multipleAssetsPerson.id}/statistics`);
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw error if person with id does not exist', async () => {
|
it('should throw error if person with id does not exist', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get(`/people/${uuidDto.notFound}/statistics`)
|
.get(`/people/${uuidDto.notFound}/statistics`)
|
||||||
@@ -133,6 +171,23 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /people', () => {
|
describe('POST /people', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/people`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { birthDate, response } of invalidBirthday) {
|
||||||
|
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post(`/people`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ birthDate });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(response));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it('should create a person', async () => {
|
it('should create a person', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/people`)
|
.post(`/people`)
|
||||||
@@ -168,6 +223,39 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('PUT /people/:id', () => {
|
describe('PUT /people/:id', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).put(`/people/${uuidDto.notFound}`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const { key, type } of [
|
||||||
|
{ key: 'name', type: 'string' },
|
||||||
|
{ key: 'featureFaceAssetId', type: 'string' },
|
||||||
|
{ key: 'isHidden', type: 'boolean value' },
|
||||||
|
{ key: 'isFavorite', type: 'boolean value' },
|
||||||
|
]) {
|
||||||
|
it(`should not allow null ${key}`, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/people/${visiblePerson.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ [key]: null });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest([`${key} must be a ${type}`]));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const { birthDate, response } of invalidBirthday) {
|
||||||
|
it(`should not accept an invalid birth date [${birthDate}]`, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.put(`/people/${visiblePerson.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send({ birthDate });
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(response));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
it('should update a date of birth', async () => {
|
it('should update a date of birth', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/people/${visiblePerson.id}`)
|
.put(`/people/${visiblePerson.id}`)
|
||||||
@@ -224,6 +312,12 @@ describe('/people', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /people/:id/merge', () => {
|
describe('POST /people/:id/merge', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post(`/people/${uuidDto.notFound}/merge`);
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should not supporting merging a person into themselves', async () => {
|
it('should not supporting merging a person into themselves', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/people/${visiblePerson.id}/merge`)
|
.post(`/people/${visiblePerson.id}/merge`)
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import {
|
import { AssetMediaResponseDto, AssetResponseDto, deleteAssets, LoginResponseDto, updateAsset } from '@immich/sdk';
|
||||||
AssetMediaResponseDto,
|
|
||||||
AssetResponseDto,
|
|
||||||
AssetVisibility,
|
|
||||||
deleteAssets,
|
|
||||||
LoginResponseDto,
|
|
||||||
updateAsset,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { readFile } from 'node:fs/promises';
|
import { readFile } from 'node:fs/promises';
|
||||||
import { join } from 'node:path';
|
import { join } from 'node:path';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
|
import { errorDto } from 'src/responses';
|
||||||
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
import { app, asBearerAuth, TEN_TIMES, testAssetDir, utils } from 'src/utils';
|
||||||
import request from 'supertest';
|
import request from 'supertest';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
@@ -56,7 +50,7 @@ describe('/search', () => {
|
|||||||
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
|
{ filename: '/formats/motionphoto/samsung-one-ui-6.heic' },
|
||||||
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
|
{ filename: '/formats/motionphoto/samsung-one-ui-5.jpg' },
|
||||||
|
|
||||||
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { visibility: AssetVisibility.Archive } },
|
{ filename: '/metadata/gps-position/thompson-springs.jpg', dto: { isArchived: true } },
|
||||||
|
|
||||||
// used for search suggestions
|
// used for search suggestions
|
||||||
{ filename: '/formats/png/density_plot.png' },
|
{ filename: '/formats/png/density_plot.png' },
|
||||||
@@ -147,6 +141,65 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /search/metadata', () => {
|
describe('POST /search/metadata', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/metadata');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
|
const badTests = [
|
||||||
|
{
|
||||||
|
should: 'should reject page as a string',
|
||||||
|
dto: { page: 'abc' },
|
||||||
|
expected: ['page must not be less than 1', 'page must be an integer number'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
should: 'should reject page as a decimal',
|
||||||
|
dto: { page: 1.5 },
|
||||||
|
expected: ['page must be an integer number'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
should: 'should reject page as a negative number',
|
||||||
|
dto: { page: -10 },
|
||||||
|
expected: ['page must not be less than 1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
should: 'should reject page as 0',
|
||||||
|
dto: { page: 0 },
|
||||||
|
expected: ['page must not be less than 1'],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
should: 'should reject size as a string',
|
||||||
|
dto: { size: 'abc' },
|
||||||
|
expected: [
|
||||||
|
'size must not be greater than 1000',
|
||||||
|
'size must not be less than 1',
|
||||||
|
'size must be an integer number',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
should: 'should reject an invalid size',
|
||||||
|
dto: { size: -1.5 },
|
||||||
|
expected: ['size must not be less than 1', 'size must be an integer number'],
|
||||||
|
},
|
||||||
|
...['isArchived', 'isFavorite', 'isEncoded', 'isOffline', 'isMotion', 'isVisible'].map((value) => ({
|
||||||
|
should: `should reject ${value} not a boolean`,
|
||||||
|
dto: { [value]: 'immich' },
|
||||||
|
expected: [`${value} must be a boolean value`],
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const { should, dto, expected } of badTests) {
|
||||||
|
it(should, async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.post('/search/metadata')
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
||||||
|
.send(dto);
|
||||||
|
expect(status).toBe(400);
|
||||||
|
expect(body).toEqual(errorDto.badRequest(expected));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const searchTests = [
|
const searchTests = [
|
||||||
{
|
{
|
||||||
should: 'should get my assets',
|
should: 'should get my assets',
|
||||||
@@ -178,12 +231,12 @@ describe('/search', () => {
|
|||||||
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
|
deferred: () => ({ dto: { size: 1, isFavorite: false }, assets: [assetLast] }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
should: 'should search by visibility (AssetVisibility.Archive)',
|
should: 'should search by isArchived (true)',
|
||||||
deferred: () => ({ dto: { visibility: AssetVisibility.Archive }, assets: [assetSprings] }),
|
deferred: () => ({ dto: { isArchived: true }, assets: [assetSprings] }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
should: 'should search by visibility (AssetVisibility.Timeline)',
|
should: 'should search by isArchived (false)',
|
||||||
deferred: () => ({ dto: { size: 1, visibility: AssetVisibility.Timeline }, assets: [assetLast] }),
|
deferred: () => ({ dto: { size: 1, isArchived: false }, assets: [assetLast] }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
should: 'should search by type (image)',
|
should: 'should search by type (image)',
|
||||||
@@ -192,7 +245,7 @@ describe('/search', () => {
|
|||||||
{
|
{
|
||||||
should: 'should search by type (video)',
|
should: 'should search by type (video)',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: { type: 'VIDEO', visibility: AssetVisibility.Hidden },
|
dto: { type: 'VIDEO' },
|
||||||
assets: [
|
assets: [
|
||||||
// the three live motion photos
|
// the three live motion photos
|
||||||
{ id: expect.any(String) },
|
{ id: expect.any(String) },
|
||||||
@@ -236,6 +289,13 @@ describe('/search', () => {
|
|||||||
should: 'should search by takenAfter (no results)',
|
should: 'should search by takenAfter (no results)',
|
||||||
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
|
deferred: () => ({ dto: { takenAfter: today.plus({ hour: 1 }).toJSDate() }, assets: [] }),
|
||||||
},
|
},
|
||||||
|
// {
|
||||||
|
// should: 'should search by originalPath',
|
||||||
|
// deferred: () => ({
|
||||||
|
// dto: { originalPath: asset1.originalPath },
|
||||||
|
// assets: [asset1],
|
||||||
|
// }),
|
||||||
|
// },
|
||||||
{
|
{
|
||||||
should: 'should search by originalFilename',
|
should: 'should search by originalFilename',
|
||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
@@ -265,7 +325,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
city: '',
|
city: '',
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
assets: [assetLast],
|
assets: [assetLast],
|
||||||
@@ -276,7 +336,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
city: null,
|
city: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
assets: [assetLast],
|
assets: [assetLast],
|
||||||
@@ -297,7 +357,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
state: '',
|
state: '',
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
withExif: true,
|
withExif: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
@@ -309,7 +369,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
state: null,
|
state: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
assets: [assetLast, assetNotocactus],
|
assets: [assetLast, assetNotocactus],
|
||||||
@@ -330,7 +390,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
country: '',
|
country: '',
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
assets: [assetLast],
|
assets: [assetLast],
|
||||||
@@ -341,7 +401,7 @@ describe('/search', () => {
|
|||||||
deferred: () => ({
|
deferred: () => ({
|
||||||
dto: {
|
dto: {
|
||||||
country: null,
|
country: null,
|
||||||
visibility: AssetVisibility.Timeline,
|
isVisible: true,
|
||||||
includeNull: true,
|
includeNull: true,
|
||||||
},
|
},
|
||||||
assets: [assetLast],
|
assets: [assetLast],
|
||||||
@@ -394,6 +454,14 @@ describe('/search', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('POST /search/smart', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/smart');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('POST /search/random', () => {
|
describe('POST /search/random', () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
@@ -408,6 +476,13 @@ describe('/search', () => {
|
|||||||
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).post('/search/random').send({ size: 1 });
|
||||||
|
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
it.each(TEN_TIMES)('should return 1 random assets', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post('/search/random')
|
.post('/search/random')
|
||||||
@@ -437,6 +512,12 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/explore', () => {
|
describe('GET /search/explore', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/explore');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should get explore data', async () => {
|
it('should get explore data', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/explore')
|
.get('/search/explore')
|
||||||
@@ -447,6 +528,12 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/places', () => {
|
describe('GET /search/places', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/places');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should get relevant places', async () => {
|
it('should get relevant places', async () => {
|
||||||
const name = 'Paris';
|
const name = 'Paris';
|
||||||
|
|
||||||
@@ -465,6 +552,12 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/cities', () => {
|
describe('GET /search/cities', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/cities');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should get all cities', async () => {
|
it('should get all cities', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/cities')
|
.get('/search/cities')
|
||||||
@@ -483,6 +576,12 @@ describe('/search', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /search/suggestions', () => {
|
describe('GET /search/suggestions', () => {
|
||||||
|
it('should require authentication', async () => {
|
||||||
|
const { status, body } = await request(app).get('/search/suggestions');
|
||||||
|
expect(status).toBe(401);
|
||||||
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
|
});
|
||||||
|
|
||||||
it('should get suggestions for country (including null)', async () => {
|
it('should get suggestions for country (including null)', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/search/suggestions?type=country&includeNull=true')
|
.get('/search/suggestions?type=country&includeNull=true')
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
|
||||||
AssetMediaResponseDto,
|
|
||||||
AssetVisibility,
|
|
||||||
LoginResponseDto,
|
|
||||||
SharedLinkType,
|
|
||||||
TimeBucketAssetResponseDto,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { createUserDto } from 'src/fixtures';
|
import { createUserDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -25,8 +19,7 @@ describe('/timeline', () => {
|
|||||||
let user: LoginResponseDto;
|
let user: LoginResponseDto;
|
||||||
let timeBucketUser: LoginResponseDto;
|
let timeBucketUser: LoginResponseDto;
|
||||||
|
|
||||||
let user1Assets: AssetMediaResponseDto[];
|
let userAssets: AssetMediaResponseDto[];
|
||||||
let user2Assets: AssetMediaResponseDto[];
|
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
@@ -36,7 +29,7 @@ describe('/timeline', () => {
|
|||||||
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
user1Assets = await Promise.all([
|
userAssets = await Promise.all([
|
||||||
utils.createAsset(user.accessToken),
|
utils.createAsset(user.accessToken),
|
||||||
utils.createAsset(user.accessToken),
|
utils.createAsset(user.accessToken),
|
||||||
utils.createAsset(user.accessToken, {
|
utils.createAsset(user.accessToken, {
|
||||||
@@ -49,20 +42,17 @@ describe('/timeline', () => {
|
|||||||
utils.createAsset(user.accessToken),
|
utils.createAsset(user.accessToken),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
user2Assets = await Promise.all([
|
await Promise.all([
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-01-01').toISOString() }),
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-10').toISOString() }),
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-11').toISOString() }),
|
||||||
utils.createAsset(timeBucketUser.accessToken, { fileCreatedAt: new Date('1970-02-12').toISOString() }),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
await utils.deleteAssets(timeBucketUser.accessToken, [user2Assets[4].id]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('GET /timeline/buckets', () => {
|
describe('GET /timeline/buckets', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/timeline/buckets');
|
const { status, body } = await request(app).get('/timeline/buckets').query({ size: TimeBucketSize.Month });
|
||||||
expect(status).toBe(401);
|
expect(status).toBe(401);
|
||||||
expect(body).toEqual(errorDto.unauthorized);
|
expect(body).toEqual(errorDto.unauthorized);
|
||||||
});
|
});
|
||||||
@@ -70,13 +60,14 @@ describe('/timeline', () => {
|
|||||||
it('should get time buckets by month', async () => {
|
it('should get time buckets by month', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Month });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual(
|
expect(body).toEqual(
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
{ count: 3, timeBucket: '1970-02-01' },
|
{ count: 3, timeBucket: '1970-02-01T00:00:00.000Z' },
|
||||||
{ count: 1, timeBucket: '1970-01-01' },
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -84,20 +75,36 @@ describe('/timeline', () => {
|
|||||||
it('should not allow access for unrelated shared links', async () => {
|
it('should not allow access for unrelated shared links', async () => {
|
||||||
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
const sharedLink = await utils.createSharedLink(user.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
assetIds: user1Assets.map(({ id }) => id),
|
assetIds: userAssets.map(({ id }) => id),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { status, body } = await request(app).get('/timeline/buckets').query({ key: sharedLink.key });
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.query({ key: sharedLink.key, size: TimeBucketSize.Month });
|
||||||
|
|
||||||
expect(status).toBe(400);
|
expect(status).toBe(400);
|
||||||
expect(body).toEqual(errorDto.noPermission);
|
expect(body).toEqual(errorDto.noPermission);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get time buckets by day', async () => {
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get('/timeline/buckets')
|
||||||
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
|
.query({ size: TimeBucketSize.Day });
|
||||||
|
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body).toEqual([
|
||||||
|
{ count: 2, timeBucket: '1970-02-11T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-02-10T00:00:00.000Z' },
|
||||||
|
{ count: 1, timeBucket: '1970-01-01T00:00:00.000Z' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
it('should return error if time bucket is requested with partners asset and archived', async () => {
|
||||||
const req1 = await request(app)
|
const req1 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ withPartners: true, visibility: AssetVisibility.Archive });
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: true });
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
expect(req1.status).toBe(400);
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
@@ -105,7 +112,7 @@ describe('/timeline', () => {
|
|||||||
const req2 = await request(app)
|
const req2 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.query({ withPartners: true, visibility: undefined });
|
.query({ size: TimeBucketSize.Month, withPartners: true, isArchived: undefined });
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
expect(req2.status).toBe(400);
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
@@ -115,7 +122,7 @@ describe('/timeline', () => {
|
|||||||
const req1 = await request(app)
|
const req1 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ withPartners: true, isFavorite: true });
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: true });
|
||||||
|
|
||||||
expect(req1.status).toBe(400);
|
expect(req1.status).toBe(400);
|
||||||
expect(req1.body).toEqual(errorDto.badRequest());
|
expect(req1.body).toEqual(errorDto.badRequest());
|
||||||
@@ -123,7 +130,7 @@ describe('/timeline', () => {
|
|||||||
const req2 = await request(app)
|
const req2 = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ withPartners: true, isFavorite: false });
|
.query({ size: TimeBucketSize.Month, withPartners: true, isFavorite: false });
|
||||||
|
|
||||||
expect(req2.status).toBe(400);
|
expect(req2.status).toBe(400);
|
||||||
expect(req2.body).toEqual(errorDto.badRequest());
|
expect(req2.body).toEqual(errorDto.badRequest());
|
||||||
@@ -133,7 +140,7 @@ describe('/timeline', () => {
|
|||||||
const req = await request(app)
|
const req = await request(app)
|
||||||
.get('/timeline/buckets')
|
.get('/timeline/buckets')
|
||||||
.set('Authorization', `Bearer ${user.accessToken}`)
|
.set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
.query({ withPartners: true, isTrashed: true });
|
.query({ size: TimeBucketSize.Month, withPartners: true, isTrashed: true });
|
||||||
|
|
||||||
expect(req.status).toBe(400);
|
expect(req.status).toBe(400);
|
||||||
expect(req.body).toEqual(errorDto.badRequest());
|
expect(req.body).toEqual(errorDto.badRequest());
|
||||||
@@ -143,6 +150,7 @@ describe('/timeline', () => {
|
|||||||
describe('GET /timeline/bucket', () => {
|
describe('GET /timeline/bucket', () => {
|
||||||
it('should require authentication', async () => {
|
it('should require authentication', async () => {
|
||||||
const { status, body } = await request(app).get('/timeline/bucket').query({
|
const { status, body } = await request(app).get('/timeline/bucket').query({
|
||||||
|
size: TimeBucketSize.Month,
|
||||||
timeBucket: '1900-01-01',
|
timeBucket: '1900-01-01',
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -153,28 +161,11 @@ describe('/timeline', () => {
|
|||||||
it('should handle 5 digit years', async () => {
|
it('should handle 5 digit years', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/bucket')
|
.get('/timeline/bucket')
|
||||||
.query({ timeBucket: '012345-01-01' })
|
.query({ size: TimeBucketSize.Month, timeBucket: '012345-01-01' })
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual([]);
|
||||||
city: [],
|
|
||||||
country: [],
|
|
||||||
duration: [],
|
|
||||||
id: [],
|
|
||||||
visibility: [],
|
|
||||||
isFavorite: [],
|
|
||||||
isImage: [],
|
|
||||||
isTrashed: [],
|
|
||||||
livePhotoVideoId: [],
|
|
||||||
fileCreatedAt: [],
|
|
||||||
localOffsetHours: [],
|
|
||||||
ownerId: [],
|
|
||||||
projectionType: [],
|
|
||||||
ratio: [],
|
|
||||||
status: [],
|
|
||||||
thumbhash: [],
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO enable date string validation while still accepting 5 digit years
|
// TODO enable date string validation while still accepting 5 digit years
|
||||||
@@ -182,7 +173,7 @@ describe('/timeline', () => {
|
|||||||
// const { status, body } = await request(app)
|
// const { status, body } = await request(app)
|
||||||
// .get('/timeline/bucket')
|
// .get('/timeline/bucket')
|
||||||
// .set('Authorization', `Bearer ${user.accessToken}`)
|
// .set('Authorization', `Bearer ${user.accessToken}`)
|
||||||
// .query({ timeBucket: 'foo' });
|
// .query({ size: TimeBucketSize.Month, timeBucket: 'foo' });
|
||||||
|
|
||||||
// expect(status).toBe(400);
|
// expect(status).toBe(400);
|
||||||
// expect(body).toEqual(errorDto.badRequest);
|
// expect(body).toEqual(errorDto.badRequest);
|
||||||
@@ -192,39 +183,10 @@ describe('/timeline', () => {
|
|||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.get('/timeline/bucket')
|
.get('/timeline/bucket')
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
||||||
.query({ timeBucket: '1970-02-10' });
|
.query({ size: TimeBucketSize.Month, timeBucket: '1970-02-10' });
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toEqual({
|
expect(body).toEqual([]);
|
||||||
city: [],
|
|
||||||
country: [],
|
|
||||||
duration: [],
|
|
||||||
id: [],
|
|
||||||
visibility: [],
|
|
||||||
isFavorite: [],
|
|
||||||
isImage: [],
|
|
||||||
isTrashed: [],
|
|
||||||
livePhotoVideoId: [],
|
|
||||||
fileCreatedAt: [],
|
|
||||||
localOffsetHours: [],
|
|
||||||
ownerId: [],
|
|
||||||
projectionType: [],
|
|
||||||
ratio: [],
|
|
||||||
status: [],
|
|
||||||
thumbhash: [],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return time bucket in trash', async () => {
|
|
||||||
const { status, body } = await request(app)
|
|
||||||
.get('/timeline/bucket')
|
|
||||||
.set('Authorization', `Bearer ${timeBucketUser.accessToken}`)
|
|
||||||
.query({ timeBucket: '1970-02-01T00:00:00.000Z', isTrashed: true });
|
|
||||||
|
|
||||||
expect(status).toBe(200);
|
|
||||||
|
|
||||||
const timeBucket: TimeBucketAssetResponseDto = body;
|
|
||||||
expect(timeBucket.isTrashed).toEqual([true]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -118,7 +118,7 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should accept `isAdmin`', async () => {
|
it('should ignore `isAdmin`', async () => {
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.post(`/admin/users`)
|
.post(`/admin/users`)
|
||||||
.send({
|
.send({
|
||||||
@@ -130,7 +130,7 @@ describe('/admin/users', () => {
|
|||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
expect(body).toMatchObject({
|
expect(body).toMatchObject({
|
||||||
email: 'user5@immich.cloud',
|
email: 'user5@immich.cloud',
|
||||||
isAdmin: true,
|
isAdmin: false,
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
});
|
});
|
||||||
expect(status).toBe(201);
|
expect(status).toBe(201);
|
||||||
@@ -163,15 +163,14 @@ describe('/admin/users', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
it('should allow a non-admin to become an admin', async () => {
|
it('should not allow a non-admin to become an admin', async () => {
|
||||||
const user = await utils.userSetup(admin.accessToken, createUserDto.create('admin2'));
|
|
||||||
const { status, body } = await request(app)
|
const { status, body } = await request(app)
|
||||||
.put(`/admin/users/${user.userId}`)
|
.put(`/admin/users/${nonAdmin.userId}`)
|
||||||
.send({ isAdmin: true })
|
.send({ isAdmin: true })
|
||||||
.set('Authorization', `Bearer ${admin.accessToken}`);
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
|
||||||
expect(status).toBe(200);
|
expect(status).toBe(200);
|
||||||
expect(body).toMatchObject({ isAdmin: true });
|
expect(body).toMatchObject({ isAdmin: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('ignores updates to profileImagePath', async () => {
|
it('ignores updates to profileImagePath', async () => {
|
||||||
|
|||||||
@@ -7,44 +7,6 @@ describe(`immich-admin`, () => {
|
|||||||
await utils.adminSetup();
|
await utils.adminSetup();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('revoke-admin', () => {
|
|
||||||
it('should revoke admin privileges from a user', async () => {
|
|
||||||
const { child, promise } = immichAdmin(['revoke-admin']);
|
|
||||||
|
|
||||||
let data = '';
|
|
||||||
child.stdout.on('data', (chunk) => {
|
|
||||||
data += chunk;
|
|
||||||
if (data.includes('Please enter the user email:')) {
|
|
||||||
child.stdin.end('admin@immich.cloud\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { stdout, exitCode } = await promise;
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
|
|
||||||
expect(stdout).toContain('Admin access has been revoked from');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('grant-admin', () => {
|
|
||||||
it('should grant admin privileges to a user', async () => {
|
|
||||||
const { child, promise } = immichAdmin(['grant-admin']);
|
|
||||||
|
|
||||||
let data = '';
|
|
||||||
child.stdout.on('data', (chunk) => {
|
|
||||||
data += chunk;
|
|
||||||
if (data.includes('Please enter the user email:')) {
|
|
||||||
child.stdin.end('admin@immich.cloud\n');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const { stdout, exitCode } = await promise;
|
|
||||||
expect(exitCode).toBe(0);
|
|
||||||
|
|
||||||
expect(stdout).toContain('Admin access has been granted to');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('list-users', () => {
|
describe('list-users', () => {
|
||||||
it('should list the admin user', async () => {
|
it('should list the admin user', async () => {
|
||||||
const { stdout, exitCode } = await immichAdmin(['list-users']).promise;
|
const { stdout, exitCode } = await immichAdmin(['list-users']).promise;
|
||||||
|
|||||||
@@ -103,7 +103,6 @@ export const loginResponseDto = {
|
|||||||
accessToken: expect.any(String),
|
accessToken: expect.any(String),
|
||||||
name: 'Immich Admin',
|
name: 'Immich Admin',
|
||||||
isAdmin: true,
|
isAdmin: true,
|
||||||
isOnboarded: false,
|
|
||||||
profileImagePath: '',
|
profileImagePath: '',
|
||||||
shouldChangePassword: true,
|
shouldChangePassword: true,
|
||||||
userEmail: 'admin@immich.cloud',
|
userEmail: 'admin@immich.cloud',
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
AssetMediaCreateDto,
|
AssetMediaCreateDto,
|
||||||
AssetMediaResponseDto,
|
AssetMediaResponseDto,
|
||||||
AssetResponseDto,
|
AssetResponseDto,
|
||||||
AssetVisibility,
|
|
||||||
CheckExistingAssetsDto,
|
CheckExistingAssetsDto,
|
||||||
CreateAlbumDto,
|
CreateAlbumDto,
|
||||||
CreateLibraryDto,
|
CreateLibraryDto,
|
||||||
@@ -430,10 +429,7 @@ export const utils = {
|
|||||||
},
|
},
|
||||||
|
|
||||||
archiveAssets: (accessToken: string, ids: string[]) =>
|
archiveAssets: (accessToken: string, ids: string[]) =>
|
||||||
updateAssets(
|
updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }),
|
||||||
{ assetBulkUpdateDto: { ids, visibility: AssetVisibility.Archive } },
|
|
||||||
{ headers: asBearerAuth(accessToken) },
|
|
||||||
),
|
|
||||||
|
|
||||||
deleteAssets: (accessToken: string, ids: string[]) =>
|
deleteAssets: (accessToken: string, ids: string[]) =>
|
||||||
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
|
||||||
|
|||||||
@@ -33,9 +33,7 @@ test.describe('Registration', () => {
|
|||||||
// onboarding
|
// onboarding
|
||||||
await expect(page).toHaveURL('/auth/onboarding');
|
await expect(page).toHaveURL('/auth/onboarding');
|
||||||
await page.getByRole('button', { name: 'Theme' }).click();
|
await page.getByRole('button', { name: 'Theme' }).click();
|
||||||
await page.getByRole('button', { name: 'Language' }).click();
|
await page.getByRole('button', { name: 'Privacy' }).click();
|
||||||
await page.getByRole('button', { name: 'Server Privacy' }).click();
|
|
||||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Storage Template' }).click();
|
await page.getByRole('button', { name: 'Storage Template' }).click();
|
||||||
await page.getByRole('button', { name: 'Done' }).click();
|
await page.getByRole('button', { name: 'Done' }).click();
|
||||||
|
|
||||||
@@ -79,13 +77,6 @@ test.describe('Registration', () => {
|
|||||||
await page.getByLabel('Password').fill('new-password');
|
await page.getByLabel('Password').fill('new-password');
|
||||||
await page.getByRole('button', { name: 'Login' }).click();
|
await page.getByRole('button', { name: 'Login' }).click();
|
||||||
|
|
||||||
// onboarding
|
|
||||||
await expect(page).toHaveURL('/auth/onboarding');
|
|
||||||
await page.getByRole('button', { name: 'Theme' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Language' }).click();
|
|
||||||
await page.getByRole('button', { name: 'User Privacy' }).click();
|
|
||||||
await page.getByRole('button', { name: 'Done' }).click();
|
|
||||||
|
|
||||||
// success
|
// success
|
||||||
await expect(page).toHaveURL(/\/photos/);
|
await expect(page).toHaveURL(/\/photos/);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,13 +47,15 @@ test.describe('Shared Links', () => {
|
|||||||
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
await page.locator(`[data-asset-id="${asset.id}"]`).hover();
|
||||||
await page.waitForSelector('[data-group] svg');
|
await page.waitForSelector('[data-group] svg');
|
||||||
await page.getByRole('checkbox').click();
|
await page.getByRole('checkbox').click();
|
||||||
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
|
await page.waitForEvent('download');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('download all from shared link', async ({ page }) => {
|
test('download all from shared link', async ({ page }) => {
|
||||||
await page.goto(`/share/${sharedLink.key}`);
|
await page.goto(`/share/${sharedLink.key}`);
|
||||||
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
|
||||||
await Promise.all([page.waitForEvent('download'), page.getByRole('button', { name: 'Download' }).click()]);
|
await page.getByRole('button', { name: 'Download' }).click();
|
||||||
|
await page.waitForEvent('download');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('enter password for a shared link', async ({ page }) => {
|
test('enter password for a shared link', async ({ page }) => {
|
||||||
|
|||||||
@@ -1,89 +0,0 @@
|
|||||||
import { getUserAdmin } from '@immich/sdk';
|
|
||||||
import { expect, test } from '@playwright/test';
|
|
||||||
import { asBearerAuth, utils } from 'src/utils';
|
|
||||||
|
|
||||||
test.describe('User Administration', () => {
|
|
||||||
test.beforeAll(() => {
|
|
||||||
utils.initSdk();
|
|
||||||
});
|
|
||||||
|
|
||||||
test.beforeEach(async () => {
|
|
||||||
await utils.resetDatabase();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('validate admin/users link', async ({ context, page }) => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
|
||||||
|
|
||||||
// Navigate to user management page and verify title and header
|
|
||||||
await page.goto(`/admin/users`);
|
|
||||||
await expect(page).toHaveTitle(/User Management/);
|
|
||||||
await expect(page.getByText('User Management')).toBeVisible();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('create user', async ({ context, page }) => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
|
||||||
|
|
||||||
// Create a new user
|
|
||||||
await page.goto('/admin/users');
|
|
||||||
await page.getByRole('button', { name: 'Create user' }).click();
|
|
||||||
await page.getByLabel('Email').fill('user@immich.cloud');
|
|
||||||
await page.getByLabel('Password', { exact: true }).fill('password');
|
|
||||||
await page.getByLabel('Confirm Password').fill('password');
|
|
||||||
await page.getByLabel('Name').fill('Immich User');
|
|
||||||
await page.getByRole('button', { name: 'Create', exact: true }).click();
|
|
||||||
|
|
||||||
// Verify the user exists in the user list
|
|
||||||
await page.getByRole('row', { name: 'user@immich.cloud' });
|
|
||||||
});
|
|
||||||
|
|
||||||
test('promote to admin', async ({ context, page }) => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
|
||||||
|
|
||||||
const user = await utils.userSetup(admin.accessToken, {
|
|
||||||
name: 'Admin 2',
|
|
||||||
email: 'admin2@immich.cloud',
|
|
||||||
password: 'password',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.isAdmin).toBe(false);
|
|
||||||
|
|
||||||
await page.goto(`/admin/users/${user.userId}`);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit user' }).click();
|
|
||||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
|
||||||
await page.getByText('Admin User').click();
|
|
||||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
|
||||||
|
|
||||||
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(updated.isAdmin).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('revoke admin access', async ({ context, page }) => {
|
|
||||||
const admin = await utils.adminSetup();
|
|
||||||
await utils.setAuthCookies(context, admin.accessToken);
|
|
||||||
|
|
||||||
const user = await utils.userSetup(admin.accessToken, {
|
|
||||||
name: 'Admin 2',
|
|
||||||
email: 'admin2@immich.cloud',
|
|
||||||
password: 'password',
|
|
||||||
isAdmin: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(user.isAdmin).toBe(true);
|
|
||||||
|
|
||||||
await page.goto(`/admin/users/${user.userId}`);
|
|
||||||
|
|
||||||
await page.getByRole('button', { name: 'Edit user' }).click();
|
|
||||||
await expect(page.getByLabel('Admin User')).toBeChecked();
|
|
||||||
await page.getByText('Admin User').click();
|
|
||||||
await expect(page.getByLabel('Admin User')).not.toBeChecked();
|
|
||||||
await page.getByRole('button', { name: 'Confirm' }).click();
|
|
||||||
|
|
||||||
const updated = await getUserAdmin({ id: user.userId }, { headers: asBearerAuth(admin.accessToken) });
|
|
||||||
expect(updated.isAdmin).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
Submodule e2e/test-assets updated: 8885d6d01c...9e3b964b08
@@ -40,6 +40,7 @@
|
|||||||
"backup_keep_last_amount": "Aantal vorige rugsteune om te hou",
|
"backup_keep_last_amount": "Aantal vorige rugsteune om te hou",
|
||||||
"backup_settings": "Rugsteun instellings",
|
"backup_settings": "Rugsteun instellings",
|
||||||
"backup_settings_description": "Bestuur databasis rugsteun instellings",
|
"backup_settings_description": "Bestuur databasis rugsteun instellings",
|
||||||
|
"check_all": "Kies Alles",
|
||||||
"cleared_jobs": "Poste gevee vir: {job}",
|
"cleared_jobs": "Poste gevee vir: {job}",
|
||||||
"config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel",
|
"config_set_by_file": "Config word tans deur 'n konfigurasielêer gestel",
|
||||||
"confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?",
|
"confirm_delete_library": "Is jy seker jy wil {library}-biblioteek uitvee?",
|
||||||
@@ -54,10 +55,12 @@
|
|||||||
"disable_login": "Deaktiveer aanmelding",
|
"disable_login": "Deaktiveer aanmelding",
|
||||||
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search",
|
"duplicate_detection_job_description": "Begin masjienleer op bates om soortgelyke beelde op te spoor. Maak staat op Smart Search",
|
||||||
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
|
"exclusion_pattern_description": "Met uitsluitingspatrone kan jy lêers en vouers ignoreer wanneer jy jou biblioteek skandeer. Dit is nuttig as jy vouers het wat lêers bevat wat jy nie wil invoer nie, soos RAW-lêers.",
|
||||||
|
"external_library_created_at": "Eksterne biblioteek (geskep op {date})",
|
||||||
"external_library_management": "Eksterne Biblioteekbestuur",
|
"external_library_management": "Eksterne Biblioteekbestuur",
|
||||||
"face_detection": "Gesig deteksie",
|
"face_detection": "Gesig deteksie",
|
||||||
"failed_job_command": "Opdrag {command} het misluk vir werk: {job}",
|
"failed_job_command": "Opdrag {command} het misluk vir werk: {job}",
|
||||||
"force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.",
|
"force_delete_user_warning": "WAARSKUWING: Dit sal onmiddellik die gebruiker en alle bates verwyder. Dit kan nie ontdoen word nie en die lêers kan nie herstel word nie.",
|
||||||
|
"forcing_refresh_library_files": "Forseer herlaai van alle biblioteeklêers",
|
||||||
"image_format": "Formaat",
|
"image_format": "Formaat",
|
||||||
"image_format_description": "WebP produseer kleiner lêers as JPEG, maar is stadiger om te enkodeer.",
|
"image_format_description": "WebP produseer kleiner lêers as JPEG, maar is stadiger om te enkodeer.",
|
||||||
"image_prefer_embedded_preview": "Verkies ingebedde voorskou",
|
"image_prefer_embedded_preview": "Verkies ingebedde voorskou",
|
||||||
|
|||||||
236
i18n/ar.json
236
i18n/ar.json
@@ -14,6 +14,7 @@
|
|||||||
"add_a_location": "إضافة موقع",
|
"add_a_location": "إضافة موقع",
|
||||||
"add_a_name": "إضافة إسم",
|
"add_a_name": "إضافة إسم",
|
||||||
"add_a_title": "إضافة عنوان",
|
"add_a_title": "إضافة عنوان",
|
||||||
|
"add_endpoint": "Add endpoint",
|
||||||
"add_exclusion_pattern": "إضافة نمط إستثناء",
|
"add_exclusion_pattern": "إضافة نمط إستثناء",
|
||||||
"add_import_path": "إضافة مسار الإستيراد",
|
"add_import_path": "إضافة مسار الإستيراد",
|
||||||
"add_location": "إضافة موقع",
|
"add_location": "إضافة موقع",
|
||||||
@@ -43,6 +44,8 @@
|
|||||||
"backup_keep_last_amount": "مقدار النسخ الاحتياطية السابقة للاحتفاظ بها",
|
"backup_keep_last_amount": "مقدار النسخ الاحتياطية السابقة للاحتفاظ بها",
|
||||||
"backup_settings": "إعدادات النسخ الاحتياطي",
|
"backup_settings": "إعدادات النسخ الاحتياطي",
|
||||||
"backup_settings_description": "إدارة إعدادات النسخ الاحتياطي لقاعدة البيانات",
|
"backup_settings_description": "إدارة إعدادات النسخ الاحتياطي لقاعدة البيانات",
|
||||||
|
"check_all": "اختر الكل",
|
||||||
|
"cleanup": "تنظيف",
|
||||||
"cleared_jobs": "تم إخلاء مهام: {job}",
|
"cleared_jobs": "تم إخلاء مهام: {job}",
|
||||||
"config_set_by_file": "الإعدادات حاليًا معينة عن طريق ملف الاعدادات",
|
"config_set_by_file": "الإعدادات حاليًا معينة عن طريق ملف الاعدادات",
|
||||||
"confirm_delete_library": "هل أنت متأكد أنك تريد حذف مكتبة {library}؟",
|
"confirm_delete_library": "هل أنت متأكد أنك تريد حذف مكتبة {library}؟",
|
||||||
@@ -57,12 +60,14 @@
|
|||||||
"disable_login": "تعطيل تسجيل الدخول",
|
"disable_login": "تعطيل تسجيل الدخول",
|
||||||
"duplicate_detection_job_description": "بدء التعلم الآلي على المحتوى للعثور على الصور المتشابهة. يعتمد على البحث الذكي",
|
"duplicate_detection_job_description": "بدء التعلم الآلي على المحتوى للعثور على الصور المتشابهة. يعتمد على البحث الذكي",
|
||||||
"exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
|
"exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.",
|
||||||
|
"external_library_created_at": "مكتبة خارجية (أُنشئت في {date})",
|
||||||
"external_library_management": "إدارة المكتبة الخارجية",
|
"external_library_management": "إدارة المكتبة الخارجية",
|
||||||
"face_detection": "إكتشاف الوجوه",
|
"face_detection": "إكتشاف الوجوه",
|
||||||
"face_detection_description": "اكتشف الوجوه في الأصول باستخدام التعلم الآلي. بالنسبة لمقاطع الفيديو، يتم اعتبار الصورة المصغرة فقط. \"تحديث\" (إعادة) معالجة جميع الأصول. \"إعادة تعيين\" تمسح أيضًا جميع بيانات الوجوه الحالية. \"مفقود\" يضع الأصول التي لم تتم معالجتها بعد في قائمة الانتظار. سيتم وضع الوجوه المكتشفة في قائمة الانتظار للتعرف على الوجه بعد اكتمال اكتشاف الوجه، وتجميعها في أشخاص موجودين أو جدد.",
|
"face_detection_description": "اكتشف الوجوه في الأصول باستخدام التعلم الآلي. بالنسبة لمقاطع الفيديو، يتم اعتبار الصورة المصغرة فقط. \"تحديث\" (إعادة) معالجة جميع الأصول. \"إعادة تعيين\" تمسح أيضًا جميع بيانات الوجوه الحالية. \"مفقود\" يضع الأصول التي لم تتم معالجتها بعد في قائمة الانتظار. سيتم وضع الوجوه المكتشفة في قائمة الانتظار للتعرف على الوجه بعد اكتمال اكتشاف الوجه، وتجميعها في أشخاص موجودين أو جدد.",
|
||||||
"facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"إعادة التعيين\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
|
"facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"إعادة التعيين\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.",
|
||||||
"failed_job_command": "فشل الأمر {command} للمهمة: {job}",
|
"failed_job_command": "فشل الأمر {command} للمهمة: {job}",
|
||||||
"force_delete_user_warning": "تحذير: سيؤدي ذلك إلى إزالة المستخدم وجميع محتوياته على الفور. لا يمكن التراجع عن هذا الإجراء ولا يمكن استرداد الملفات.",
|
"force_delete_user_warning": "تحذير: سيؤدي ذلك إلى إزالة المستخدم وجميع محتوياته على الفور. لا يمكن التراجع عن هذا الإجراء ولا يمكن استرداد الملفات.",
|
||||||
|
"forcing_refresh_library_files": "إجبار التحديث لجميع ملفات المكتبة",
|
||||||
"image_format": "التنسيق",
|
"image_format": "التنسيق",
|
||||||
"image_format_description": "يُنتج WebP ملفات أصغر حجمًا من ملفات JPEG، ولكنه أبطأ في عملية الترميز.",
|
"image_format_description": "يُنتج WebP ملفات أصغر حجمًا من ملفات JPEG، ولكنه أبطأ في عملية الترميز.",
|
||||||
"image_prefer_embedded_preview": "تفضيل المعاينة المدمجة",
|
"image_prefer_embedded_preview": "تفضيل المعاينة المدمجة",
|
||||||
@@ -182,19 +187,28 @@
|
|||||||
"oauth_auto_register": "التسجيل التلقائي",
|
"oauth_auto_register": "التسجيل التلقائي",
|
||||||
"oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth",
|
"oauth_auto_register_description": "التسجيل التلقائي للمستخدمين الجدد بعد تسجيل الدخول باستخدام OAuth",
|
||||||
"oauth_button_text": "نص الزر",
|
"oauth_button_text": "نص الزر",
|
||||||
|
"oauth_client_id": "معرف العميل",
|
||||||
|
"oauth_client_secret": "الرمز السري للعميل",
|
||||||
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
|
"oauth_enable_description": "تسجيل الدخول باستخدام OAuth",
|
||||||
|
"oauth_issuer_url": "عنوان URL الخاص بجهة الإصدار",
|
||||||
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
|
"oauth_mobile_redirect_uri": "عنوان URI لإعادة التوجيه على الهاتف",
|
||||||
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
|
"oauth_mobile_redirect_uri_override": "تجاوز عنوان URI لإعادة التوجيه على الهاتف",
|
||||||
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل ''{callback}''",
|
"oauth_mobile_redirect_uri_override_description": "قم بتفعيله عندما لا يسمح موفر OAuth بمعرف URI للجوال، مثل '{callback}'",
|
||||||
|
"oauth_profile_signing_algorithm": "خوارزمية توقيع الملف الشخصي",
|
||||||
|
"oauth_profile_signing_algorithm_description": "الخوارزمية المستخدمة للتوقيع على ملف تعريف المستخدم.",
|
||||||
|
"oauth_scope": "النطاق",
|
||||||
"oauth_settings": "OAuth",
|
"oauth_settings": "OAuth",
|
||||||
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
|
"oauth_settings_description": "إدارة إعدادات تسجيل الدخول OAuth",
|
||||||
"oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.",
|
"oauth_settings_more_details": "لمزيد من التفاصيل حول هذه الميزة، يرجى الرجوع إلى <link>الوثائق</link>.",
|
||||||
|
"oauth_signing_algorithm": "خوارزمية التوقيع",
|
||||||
"oauth_storage_label_claim": "المطالبة بتصنيف التخزين",
|
"oauth_storage_label_claim": "المطالبة بتصنيف التخزين",
|
||||||
"oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.",
|
"oauth_storage_label_claim_description": "قم تلقائيًا بتعيين تصنيف التخزين الخاص بالمستخدم على قيمة هذه المطالبة.",
|
||||||
"oauth_storage_quota_claim": "المطالبة بحصة التخزين",
|
"oauth_storage_quota_claim": "المطالبة بحصة التخزين",
|
||||||
"oauth_storage_quota_claim_description": "قم تلقائيًا بتعيين حصة التخزين للمستخدم على قيمة هذه المطالبة.",
|
"oauth_storage_quota_claim_description": "قم تلقائيًا بتعيين حصة التخزين للمستخدم على قيمة هذه المطالبة.",
|
||||||
"oauth_storage_quota_default": "حصة التخزين الافتراضية (جيجابايت)",
|
"oauth_storage_quota_default": "حصة التخزين الافتراضية (جيجابايت)",
|
||||||
"oauth_storage_quota_default_description": "الحصة بالجيجابايت التي سيتم استخدامها عندما لا يتم توفير مطالبة (أدخل 0 لحصة غير محدودة).",
|
"oauth_storage_quota_default_description": "الحصة بالجيجابايت التي سيتم استخدامها عندما لا يتم توفير مطالبة (أدخل 0 لحصة غير محدودة).",
|
||||||
|
"offline_paths": "مسارات غير متصلة",
|
||||||
|
"offline_paths_description": "قد تكون هذه النتائج ناتجة عن حذف يدوي لملفات لا تتبع لمكتبة خارجية.",
|
||||||
"password_enable_description": "تسجيل الدخول باستخدام البريد الكتروني وكلمة المرور",
|
"password_enable_description": "تسجيل الدخول باستخدام البريد الكتروني وكلمة المرور",
|
||||||
"password_settings": "تسجيل الدخول بكلمة المرور",
|
"password_settings": "تسجيل الدخول بكلمة المرور",
|
||||||
"password_settings_description": "إدارة تسجيل الدخول بكلمة المرور",
|
"password_settings_description": "إدارة تسجيل الدخول بكلمة المرور",
|
||||||
@@ -204,6 +218,9 @@
|
|||||||
"refreshing_all_libraries": "تحديث كافة المكتبات",
|
"refreshing_all_libraries": "تحديث كافة المكتبات",
|
||||||
"registration": "تسجيل المدير",
|
"registration": "تسجيل المدير",
|
||||||
"registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
|
"registration_description": "بما أنك أول مستخدم في النظام، سيتم تعيينك كمسؤول وستكون مسؤولًا عن المهام الإدارية، وسيتم إنشاء مستخدمين إضافيين بواسطتك.",
|
||||||
|
"repair_all": "إصلاح الكل",
|
||||||
|
"repair_matched_items": "تمت مطابقة {count, plural, one {# عنصر} other {# عناصر}}",
|
||||||
|
"repaired_items": "تم إصلاح {count, plural, one {# عنصر} other {# عناصر}}",
|
||||||
"require_password_change_on_login": "الطلب من المستخدم تغيير كلمة المرور عند تسجيل الدخول الأول",
|
"require_password_change_on_login": "الطلب من المستخدم تغيير كلمة المرور عند تسجيل الدخول الأول",
|
||||||
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
|
"reset_settings_to_default": "إعادة ضبط الإعدادات إلى الوضع الافتراضي",
|
||||||
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
|
"reset_settings_to_recent_saved": "إعادة ضبط الإعدادات إلى الإعدادات المحفوظة مؤخرًا",
|
||||||
@@ -244,6 +261,7 @@
|
|||||||
"template_email_invite_album": "قالب دعوة الألبوم",
|
"template_email_invite_album": "قالب دعوة الألبوم",
|
||||||
"template_email_preview": "عرض مسبق",
|
"template_email_preview": "عرض مسبق",
|
||||||
"template_email_settings": "نماذج البريد الالكتروني",
|
"template_email_settings": "نماذج البريد الالكتروني",
|
||||||
|
"template_email_settings_description": "إدارة قوالب إشعارات البريد الإلكتروني المخصصة",
|
||||||
"template_email_update_album": "تحديث قالب الألبوم",
|
"template_email_update_album": "تحديث قالب الألبوم",
|
||||||
"template_email_welcome": "قالب البريد الإلكتروني الترحيبي",
|
"template_email_welcome": "قالب البريد الإلكتروني الترحيبي",
|
||||||
"template_settings": "قوالب الإشعارات",
|
"template_settings": "قوالب الإشعارات",
|
||||||
@@ -252,6 +270,7 @@
|
|||||||
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
|
"theme_custom_css_settings_description": "أوراق الأنماط المتتالية تسمح بتخصيص تصميم Immich.",
|
||||||
"theme_settings": "إعدادات السمة",
|
"theme_settings": "إعدادات السمة",
|
||||||
"theme_settings_description": "إدارة تخصيص واجهة ويب Immich",
|
"theme_settings_description": "إدارة تخصيص واجهة ويب Immich",
|
||||||
|
"these_files_matched_by_checksum": "تتم مطابقة هذه الملفات من خلال المجاميع الاختبارية الخاصة بهم",
|
||||||
"thumbnail_generation_job": "إنشاء الصور المصغرة",
|
"thumbnail_generation_job": "إنشاء الصور المصغرة",
|
||||||
"thumbnail_generation_job_description": "إنشاء صور مصغرة كبيرة وصغيرة وغير واضحة لكل أصل، بالإضافة إلى صور مصغرة لكل شخص",
|
"thumbnail_generation_job_description": "إنشاء صور مصغرة كبيرة وصغيرة وغير واضحة لكل أصل، بالإضافة إلى صور مصغرة لكل شخص",
|
||||||
"transcoding_acceleration_api": "واجهة برمجة التطبيقات للتسريع",
|
"transcoding_acceleration_api": "واجهة برمجة التطبيقات للتسريع",
|
||||||
@@ -282,6 +301,7 @@
|
|||||||
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
|
"transcoding_hardware_acceleration_description": "تجريبي؛ أسرع بكثير، ولكن ستكون جودتها أقل عند نفس معدل البت",
|
||||||
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
|
"transcoding_hardware_decoding": "فك تشفير الأجهزة",
|
||||||
"transcoding_hardware_decoding_setting_description": "ينطبق ذلك فقط على NVENC، QSV، و RKMPP. يمكن التسريع من طرف لطرف بدلاً من تسريع الترميز فقط. قد لا يعمل على جميع مقاطع الفيديو.",
|
"transcoding_hardware_decoding_setting_description": "ينطبق ذلك فقط على NVENC، QSV، و RKMPP. يمكن التسريع من طرف لطرف بدلاً من تسريع الترميز فقط. قد لا يعمل على جميع مقاطع الفيديو.",
|
||||||
|
"transcoding_hevc_codec": "كود HEVC",
|
||||||
"transcoding_max_b_frames": "أقصى عدد من الإطارات B",
|
"transcoding_max_b_frames": "أقصى عدد من الإطارات B",
|
||||||
"transcoding_max_b_frames_description": "القيم الأعلى تعزز كفاءة الضغط، ولكنها تبطئ عملية الترميز. قد لا تكون متوافقة مع التسريع العتادي على الأجهزة القديمة. قيمة 0 تعطل إطارات B، بينما تضبط القيمة -1 هذا القيمة تلقائيًا.",
|
"transcoding_max_b_frames_description": "القيم الأعلى تعزز كفاءة الضغط، ولكنها تبطئ عملية الترميز. قد لا تكون متوافقة مع التسريع العتادي على الأجهزة القديمة. قيمة 0 تعطل إطارات B، بينما تضبط القيمة -1 هذا القيمة تلقائيًا.",
|
||||||
"transcoding_max_bitrate": "الحد الأقصى لمعدل البت",
|
"transcoding_max_bitrate": "الحد الأقصى لمعدل البت",
|
||||||
@@ -319,6 +339,8 @@
|
|||||||
"trash_number_of_days_description": "عدد أيام الاحتفاظ بالمحتويات في سلة المهملات قبل حذفها نهائيًا",
|
"trash_number_of_days_description": "عدد أيام الاحتفاظ بالمحتويات في سلة المهملات قبل حذفها نهائيًا",
|
||||||
"trash_settings": "إعدادات سلة المهملات",
|
"trash_settings": "إعدادات سلة المهملات",
|
||||||
"trash_settings_description": "إدارة إعدادات سلة المهملات",
|
"trash_settings_description": "إدارة إعدادات سلة المهملات",
|
||||||
|
"untracked_files": "الملفات التي لم يتم تعقبها",
|
||||||
|
"untracked_files_description": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجة لعمليات نقل فاشلة، أو عمليات تحميل متقطعة، أو يتم تركها في الخلف بسبب خطأ ما",
|
||||||
"user_cleanup_job": "تنظيف المستخدم",
|
"user_cleanup_job": "تنظيف المستخدم",
|
||||||
"user_delete_delay": "سيتم جدولة حساب <b>{user}</b> ومحتوياته للحذف النهائي في غضون {delay, plural, one {# يوم} other {# أيام}}.",
|
"user_delete_delay": "سيتم جدولة حساب <b>{user}</b> ومحتوياته للحذف النهائي في غضون {delay, plural, one {# يوم} other {# أيام}}.",
|
||||||
"user_delete_delay_settings": "فترة التأخير قبل الحذف",
|
"user_delete_delay_settings": "فترة التأخير قبل الحذف",
|
||||||
@@ -344,8 +366,12 @@
|
|||||||
"admin_password": "كلمة سر المشرف",
|
"admin_password": "كلمة سر المشرف",
|
||||||
"administration": "الإدارة",
|
"administration": "الإدارة",
|
||||||
"advanced": "متقدم",
|
"advanced": "متقدم",
|
||||||
|
"advanced_settings_log_level_title": "Log level: {}",
|
||||||
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
|
"advanced_settings_prefer_remote_subtitle": "تكون بعض الأجهزة بطيئة للغاية في تحميل الصور المصغرة من الأصول الموجودة على الجهاز. قم بتنشيط هذا الإعداد لتحميل الصور البعيدة بدلاً من ذلك.",
|
||||||
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
|
"advanced_settings_prefer_remote_title": "تفضل الصور البعيدة",
|
||||||
|
"advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request",
|
||||||
|
"advanced_settings_proxy_headers_title": "Proxy Headers",
|
||||||
|
"advanced_settings_self_signed_ssl_subtitle": "Skips SSL certificate verification for the server endpoint. Required for self-signed certificates.",
|
||||||
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
|
"advanced_settings_self_signed_ssl_title": "السماح بشهادات SSL الموقعة ذاتيًا",
|
||||||
"advanced_settings_tile_subtitle": "إعدادات المستخدم المتقدمة",
|
"advanced_settings_tile_subtitle": "إعدادات المستخدم المتقدمة",
|
||||||
"advanced_settings_troubleshooting_subtitle": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها",
|
"advanced_settings_troubleshooting_subtitle": "تمكين الميزات الإضافية لاستكشاف الأخطاء وإصلاحها",
|
||||||
@@ -368,6 +394,10 @@
|
|||||||
"album_remove_user": "هل ترغب في إزالة المستخدم؟",
|
"album_remove_user": "هل ترغب في إزالة المستخدم؟",
|
||||||
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
|
"album_remove_user_confirmation": "هل أنت متأكد أنك تريد إزالة {user}؟",
|
||||||
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
|
"album_share_no_users": "يبدو أنك قمت بمشاركة هذا الألبوم مع جميع المستخدمين أو ليس لديك أي مستخدم للمشاركة معه.",
|
||||||
|
"album_thumbnail_card_item": "عنصر واحد",
|
||||||
|
"album_thumbnail_card_items": "{} items",
|
||||||
|
"album_thumbnail_card_shared": " · . مشترك",
|
||||||
|
"album_thumbnail_shared_by": "Shared by {}",
|
||||||
"album_updated": "تم تحديث الألبوم",
|
"album_updated": "تم تحديث الألبوم",
|
||||||
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
|
"album_updated_setting_description": "تلقي إشعارًا عبر البريد الإلكتروني عندما يحتوي الألبوم المشترك على محتويات جديدة",
|
||||||
"album_user_left": "تم ترك {album}",
|
"album_user_left": "تم ترك {album}",
|
||||||
@@ -405,8 +435,10 @@
|
|||||||
"archive": "الأرشيف",
|
"archive": "الأرشيف",
|
||||||
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
|
"archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها",
|
||||||
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
|
"archive_page_no_archived_assets": "لم يتم العثور على الأصول المؤرشفة",
|
||||||
|
"archive_page_title": "Archive ({})",
|
||||||
"archive_size": "حجم الأرشيف",
|
"archive_size": "حجم الأرشيف",
|
||||||
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
|
"archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)",
|
||||||
|
"archived": "Archived",
|
||||||
"archived_count": "{count, plural, other {الأرشيف #}}",
|
"archived_count": "{count, plural, other {الأرشيف #}}",
|
||||||
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
|
"are_these_the_same_person": "هل هؤلاء هم نفس الشخص؟",
|
||||||
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
|
"are_you_sure_to_do_this": "هل انت متأكد من أنك تريد أن تفعل هذا؟",
|
||||||
@@ -428,26 +460,39 @@
|
|||||||
"asset_list_settings_title": "شبكة الصور",
|
"asset_list_settings_title": "شبكة الصور",
|
||||||
"asset_offline": "المحتوى غير اتصال",
|
"asset_offline": "المحتوى غير اتصال",
|
||||||
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
|
"asset_offline_description": "لم يعد هذا الأصل الخارجي موجودًا على القرص. يرجى الاتصال بمسؤول Immich للحصول على المساعدة.",
|
||||||
|
"asset_restored_successfully": "Asset restored successfully",
|
||||||
"asset_skipped": "تم تخطيه",
|
"asset_skipped": "تم تخطيه",
|
||||||
"asset_skipped_in_trash": "في سلة المهملات",
|
"asset_skipped_in_trash": "في سلة المهملات",
|
||||||
"asset_uploaded": "تم الرفع",
|
"asset_uploaded": "تم الرفع",
|
||||||
"asset_uploading": "جارٍ الرفع…",
|
"asset_uploading": "جارٍ الرفع…",
|
||||||
|
"asset_viewer_settings_subtitle": "Manage your gallery viewer settings",
|
||||||
"asset_viewer_settings_title": "عارض الأصول",
|
"asset_viewer_settings_title": "عارض الأصول",
|
||||||
"assets": "المحتويات",
|
"assets": "المحتويات",
|
||||||
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
|
"assets_added_count": "تمت إضافة {count, plural, one {# محتوى} other {# محتويات}}",
|
||||||
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
|
"assets_added_to_album_count": "تمت إضافة {count, plural, one {# الأصل} other {# الأصول}} إلى الألبوم",
|
||||||
"assets_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}",
|
"assets_added_to_name_count": "تم إضافة {count, plural, one {# محتوى} other {# محتويات }} إلى {hasName, select, true {<b>{name}</b>} other {ألبوم جديد}}",
|
||||||
"assets_count": "{count, plural, one {# محتوى} other {# محتويات}}",
|
"assets_count": "{count, plural, one {# محتوى} other {# محتويات}}",
|
||||||
|
"assets_deleted_permanently": "{} asset(s) deleted permanently",
|
||||||
|
"assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server",
|
||||||
"assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
|
"assets_moved_to_trash_count": "تم نقل {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
|
||||||
"assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم",
|
"assets_permanently_deleted_count": "تم حذف {count, plural, one {# هذا المحتوى} other {# هذه المحتويات}} بشكل دائم",
|
||||||
"assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}",
|
"assets_removed_count": "تمت إزالة {count, plural, one {# محتوى} other {# محتويات}}",
|
||||||
|
"assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device",
|
||||||
"assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.",
|
"assets_restore_confirmation": "هل أنت متأكد من أنك تريد استعادة جميع الأصول المحذوفة؟ لا يمكنك التراجع عن هذا الإجراء! لاحظ أنه لا يمكن استعادة أي أصول غير متصلة بهذه الطريقة.",
|
||||||
"assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}",
|
"assets_restored_count": "تمت استعادة {count, plural, one {# محتوى} other {# محتويات}}",
|
||||||
|
"assets_restored_successfully": "{} asset(s) restored successfully",
|
||||||
|
"assets_trashed": "{} asset(s) trashed",
|
||||||
"assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
|
"assets_trashed_count": "تم إرسال {count, plural, one {# محتوى} other {# محتويات}} إلى سلة المهملات",
|
||||||
|
"assets_trashed_from_server": "{} asset(s) trashed from the Immich server",
|
||||||
"assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
|
"assets_were_part_of_album_count": "{count, plural, one {هذا المحتوى} other {هذه المحتويات}} في الألبوم بالفعل",
|
||||||
"authorized_devices": "الأجهزه المخولة",
|
"authorized_devices": "الأجهزه المخولة",
|
||||||
|
"automatic_endpoint_switching_subtitle": "Connect locally over designated Wi-Fi when available and use alternative connections elsewhere",
|
||||||
|
"automatic_endpoint_switching_title": "Automatic URL switching",
|
||||||
"back": "خلف",
|
"back": "خلف",
|
||||||
"back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد",
|
"back_close_deselect": "الرجوع أو الإغلاق أو إلغاء التحديد",
|
||||||
|
"background_location_permission": "Background location permission",
|
||||||
|
"background_location_permission_content": "In order to switch networks when running in the background, Immich must *always* have precise location access so the app can read the Wi-Fi network's name",
|
||||||
|
"backup_album_selection_page_albums_device": "Albums on device ({})",
|
||||||
"backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
|
"backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء",
|
||||||
"backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
|
"backup_album_selection_page_assets_scatter": "يمكن أن تنتشر الأصول عبر ألبومات متعددة. وبالتالي، يمكن تضمين الألبومات أو استبعادها أثناء عملية النسخ الاحتياطي.",
|
||||||
"backup_album_selection_page_select_albums": "حدد الألبومات",
|
"backup_album_selection_page_select_albums": "حدد الألبومات",
|
||||||
@@ -456,9 +501,11 @@
|
|||||||
"backup_all": "الجميع",
|
"backup_all": "الجميع",
|
||||||
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
|
"backup_background_service_backup_failed_message": "فشل في النسخ الاحتياطي للأصول. جارٍ إعادة المحاولة...",
|
||||||
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
|
"backup_background_service_connection_failed_message": "فشل في الاتصال بالخادم. جارٍ إعادة المحاولة...",
|
||||||
|
"backup_background_service_current_upload_notification": "Uploading {}",
|
||||||
"backup_background_service_default_notification": "التحقق من الأصول الجديدة ...",
|
"backup_background_service_default_notification": "التحقق من الأصول الجديدة ...",
|
||||||
"backup_background_service_error_title": "خطأ في النسخ الاحتياطي",
|
"backup_background_service_error_title": "خطأ في النسخ الاحتياطي",
|
||||||
"backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...",
|
"backup_background_service_in_progress_notification": "النسخ الاحتياطي للأصول الخاصة بك...",
|
||||||
|
"backup_background_service_upload_failure_notification": "Failed to upload {}",
|
||||||
"backup_controller_page_albums": "ألبومات احتياطية",
|
"backup_controller_page_albums": "ألبومات احتياطية",
|
||||||
"backup_controller_page_background_app_refresh_disabled_content": "قم بتمكين تحديث تطبيق الخلفية في الإعدادات > عام > تحديث تطبيق الخلفية لاستخدام النسخ الاحتياطي في الخلفية.",
|
"backup_controller_page_background_app_refresh_disabled_content": "قم بتمكين تحديث تطبيق الخلفية في الإعدادات > عام > تحديث تطبيق الخلفية لاستخدام النسخ الاحتياطي في الخلفية.",
|
||||||
"backup_controller_page_background_app_refresh_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية",
|
"backup_controller_page_background_app_refresh_disabled_title": "تم تعطيل تحديث التطبيق في الخلفية",
|
||||||
@@ -469,6 +516,7 @@
|
|||||||
"backup_controller_page_background_battery_info_title": "تحسين البطارية",
|
"backup_controller_page_background_battery_info_title": "تحسين البطارية",
|
||||||
"backup_controller_page_background_charging": "فقط أثناء الشحن",
|
"backup_controller_page_background_charging": "فقط أثناء الشحن",
|
||||||
"backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية",
|
"backup_controller_page_background_configure_error": "فشل في تكوين خدمة الخلفية",
|
||||||
|
"backup_controller_page_background_delay": "Delay new assets backup: {}",
|
||||||
"backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق",
|
"backup_controller_page_background_description": "قم بتشغيل خدمة الخلفية لإجراء نسخ احتياطي لأي أصول جديدة تلقائيًا دون الحاجة إلى فتح التطبيق",
|
||||||
"backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية",
|
"backup_controller_page_background_is_off": "تم إيقاف النسخ الاحتياطي التلقائي للخلفية",
|
||||||
"backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
|
"backup_controller_page_background_is_on": "النسخ الاحتياطي التلقائي للخلفية قيد التشغيل",
|
||||||
@@ -478,8 +526,12 @@
|
|||||||
"backup_controller_page_backup": "دعم",
|
"backup_controller_page_backup": "دعم",
|
||||||
"backup_controller_page_backup_selected": "المحدد: ",
|
"backup_controller_page_backup_selected": "المحدد: ",
|
||||||
"backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو",
|
"backup_controller_page_backup_sub": "النسخ الاحتياطي للصور ومقاطع الفيديو",
|
||||||
|
"backup_controller_page_created": "Created on: {}",
|
||||||
"backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.",
|
"backup_controller_page_desc_backup": "قم بتشغيل النسخ الاحتياطي الأمامي لتحميل الأصول الجديدة تلقائيًا إلى الخادم عند فتح التطبيق.",
|
||||||
"backup_controller_page_excluded": "مستبعد: ",
|
"backup_controller_page_excluded": "مستبعد: ",
|
||||||
|
"backup_controller_page_failed": "Failed ({})",
|
||||||
|
"backup_controller_page_filename": "File name: {} [{}]",
|
||||||
|
"backup_controller_page_id": "ID: {}",
|
||||||
"backup_controller_page_info": "معلومات النسخ الاحتياطي",
|
"backup_controller_page_info": "معلومات النسخ الاحتياطي",
|
||||||
"backup_controller_page_none_selected": "لم يتم التحديد",
|
"backup_controller_page_none_selected": "لم يتم التحديد",
|
||||||
"backup_controller_page_remainder": "بقية",
|
"backup_controller_page_remainder": "بقية",
|
||||||
@@ -488,6 +540,7 @@
|
|||||||
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
|
"backup_controller_page_start_backup": "بدء النسخ الاحتياطي",
|
||||||
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
|
"backup_controller_page_status_off": "النسخة الاحتياطية التلقائية غير فعالة",
|
||||||
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
|
"backup_controller_page_status_on": "النسخة الاحتياطية التلقائية فعالة",
|
||||||
|
"backup_controller_page_storage_format": "{} of {} used",
|
||||||
"backup_controller_page_to_backup": "الألبومات الاحتياطية",
|
"backup_controller_page_to_backup": "الألبومات الاحتياطية",
|
||||||
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
|
"backup_controller_page_total_sub": "جميع الصور ومقاطع الفيديو الفريدة من ألبومات مختارة",
|
||||||
"backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة",
|
"backup_controller_page_turn_off": "قم بإيقاف تشغيل النسخ الاحتياطي المقدمة",
|
||||||
@@ -500,6 +553,7 @@
|
|||||||
"backup_manual_success": "نجاح",
|
"backup_manual_success": "نجاح",
|
||||||
"backup_manual_title": "حالة التحميل",
|
"backup_manual_title": "حالة التحميل",
|
||||||
"backup_options_page_title": "خيارات النسخ الاحتياطي",
|
"backup_options_page_title": "خيارات النسخ الاحتياطي",
|
||||||
|
"backup_setting_subtitle": "Manage background and foreground upload settings",
|
||||||
"backward": "الى الوراء",
|
"backward": "الى الوراء",
|
||||||
"birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
|
"birthdate_saved": "تم حفظ تاريخ الميلاد بنجاح",
|
||||||
"birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
|
"birthdate_set_description": "يتم استخدام تاريخ الميلاد لحساب عمر هذا الشخص وقت التقاط الصورة.",
|
||||||
@@ -511,16 +565,21 @@
|
|||||||
"bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.",
|
"bulk_keep_duplicates_confirmation": "هل أنت متأكد من أنك تريد الاحتفاظ بـ {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}}؟ سيؤدي هذا إلى حل جميع مجموعات النسخ المكررة دون حذف أي شيء.",
|
||||||
"bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.",
|
"bulk_trash_duplicates_confirmation": "هل أنت متأكد من أنك تريد إرسال {count, plural, one {# محتوى مكرر} other {# محتويات مكررة}} إلى سلة المهملات ؟ سيحتفظ هذا بأكبر محتوى من كل مجموعة ويرسل جميع النسخ المكررة الأخرى إلى سلة المهملات.",
|
||||||
"buy": "شراء immich",
|
"buy": "شراء immich",
|
||||||
|
"cache_settings_album_thumbnails": "Library page thumbnails ({} assets)",
|
||||||
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
|
"cache_settings_clear_cache_button": "مسح ذاكرة التخزين المؤقت",
|
||||||
"cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.",
|
"cache_settings_clear_cache_button_title": "يقوم بمسح ذاكرة التخزين المؤقت للتطبيق.سيؤثر هذا بشكل كبير على أداء التطبيق حتى إعادة بناء ذاكرة التخزين المؤقت.",
|
||||||
"cache_settings_duplicated_assets_clear_button": "واضح",
|
"cache_settings_duplicated_assets_clear_button": "واضح",
|
||||||
"cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق",
|
"cache_settings_duplicated_assets_subtitle": "الصور ومقاطع الفيديو اللتي تم تجاهلها المدرجة في التطبيق",
|
||||||
|
"cache_settings_duplicated_assets_title": "Duplicated Assets ({})",
|
||||||
|
"cache_settings_image_cache_size": "Image cache size ({} assets)",
|
||||||
"cache_settings_statistics_album": "مكتبه الصور المصغره",
|
"cache_settings_statistics_album": "مكتبه الصور المصغره",
|
||||||
|
"cache_settings_statistics_assets": "{} assets ({})",
|
||||||
"cache_settings_statistics_full": "صور كاملة",
|
"cache_settings_statistics_full": "صور كاملة",
|
||||||
"cache_settings_statistics_shared": "صورة ألبوم مشتركة",
|
"cache_settings_statistics_shared": "صورة ألبوم مشتركة",
|
||||||
"cache_settings_statistics_thumbnail": "الصورة المصغرة",
|
"cache_settings_statistics_thumbnail": "الصورة المصغرة",
|
||||||
"cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت",
|
"cache_settings_statistics_title": "استخدام ذاكرة التخزين المؤقت",
|
||||||
"cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.",
|
"cache_settings_subtitle": "تحكم في سلوك التخزين المؤقت لتطبيق الجوال.",
|
||||||
|
"cache_settings_thumbnail_size": "Thumbnail cache size ({} assets)",
|
||||||
"cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
|
"cache_settings_tile_subtitle": "التحكم في سلوك التخزين المحلي",
|
||||||
"cache_settings_tile_title": "التخزين المحلي",
|
"cache_settings_tile_title": "التخزين المحلي",
|
||||||
"cache_settings_title": "إعدادات التخزين المؤقت",
|
"cache_settings_title": "إعدادات التخزين المؤقت",
|
||||||
@@ -529,10 +588,12 @@
|
|||||||
"camera_model": "طراز الكاميرا",
|
"camera_model": "طراز الكاميرا",
|
||||||
"cancel": "إلغاء",
|
"cancel": "إلغاء",
|
||||||
"cancel_search": "الغي البحث",
|
"cancel_search": "الغي البحث",
|
||||||
|
"canceled": "Canceled",
|
||||||
"cannot_merge_people": "لا يمكن دمج الأشخاص",
|
"cannot_merge_people": "لا يمكن دمج الأشخاص",
|
||||||
"cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
|
"cannot_undo_this_action": "لا يمكنك التراجع عن هذا الإجراء!",
|
||||||
"cannot_update_the_description": "لا يمكن تحديث الوصف",
|
"cannot_update_the_description": "لا يمكن تحديث الوصف",
|
||||||
"change_date": "غيّر التاريخ",
|
"change_date": "غيّر التاريخ",
|
||||||
|
"change_display_order": "Change display order",
|
||||||
"change_expiration_time": "تغيير وقت انتهاء الصلاحية",
|
"change_expiration_time": "تغيير وقت انتهاء الصلاحية",
|
||||||
"change_location": "غيّر الموقع",
|
"change_location": "غيّر الموقع",
|
||||||
"change_name": "تغيير الإسم",
|
"change_name": "تغيير الإسم",
|
||||||
@@ -544,9 +605,12 @@
|
|||||||
"change_password_form_new_password": "كلمة المرور الجديدة",
|
"change_password_form_new_password": "كلمة المرور الجديدة",
|
||||||
"change_password_form_password_mismatch": "كلمة المرور غير مطابقة",
|
"change_password_form_password_mismatch": "كلمة المرور غير مطابقة",
|
||||||
"change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة",
|
"change_password_form_reenter_new_password": "أعد إدخال كلمة مرور جديدة",
|
||||||
"change_pin_code": "تغيير الرقم السري",
|
|
||||||
"change_your_password": "غير كلمة المرور الخاصة بك",
|
"change_your_password": "غير كلمة المرور الخاصة بك",
|
||||||
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
|
"changed_visibility_successfully": "تم تغيير الرؤية بنجاح",
|
||||||
|
"check_all": "تحقق من الكل",
|
||||||
|
"check_corrupt_asset_backup": "Check for corrupt asset backups",
|
||||||
|
"check_corrupt_asset_backup_button": "Perform check",
|
||||||
|
"check_corrupt_asset_backup_description": "Run this check only over Wi-Fi and once all assets have been backed-up. The procedure might take a few minutes.",
|
||||||
"check_logs": "تحقق من السجلات",
|
"check_logs": "تحقق من السجلات",
|
||||||
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
|
"choose_matching_people_to_merge": "اختر الأشخاص المتطابقين لدمجهم",
|
||||||
"city": "المدينة",
|
"city": "المدينة",
|
||||||
@@ -555,6 +619,14 @@
|
|||||||
"clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة",
|
"clear_all_recent_searches": "مسح جميع عمليات البحث الأخيرة",
|
||||||
"clear_message": "إخلاء الرسالة",
|
"clear_message": "إخلاء الرسالة",
|
||||||
"clear_value": "إخلاء القيمة",
|
"clear_value": "إخلاء القيمة",
|
||||||
|
"client_cert_dialog_msg_confirm": "OK",
|
||||||
|
"client_cert_enter_password": "Enter Password",
|
||||||
|
"client_cert_import": "Import",
|
||||||
|
"client_cert_import_success_msg": "Client certificate is imported",
|
||||||
|
"client_cert_invalid_msg": "Invalid certificate file or wrong password",
|
||||||
|
"client_cert_remove_msg": "Client certificate is removed",
|
||||||
|
"client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login",
|
||||||
|
"client_cert_title": "SSL Client Certificate",
|
||||||
"clockwise": "باتجاه عقارب الساعة",
|
"clockwise": "باتجاه عقارب الساعة",
|
||||||
"close": "إغلاق",
|
"close": "إغلاق",
|
||||||
"collapse": "طي",
|
"collapse": "طي",
|
||||||
@@ -567,21 +639,23 @@
|
|||||||
"comments_are_disabled": "التعليقات معطلة",
|
"comments_are_disabled": "التعليقات معطلة",
|
||||||
"common_create_new_album": "إنشاء ألبوم جديد",
|
"common_create_new_album": "إنشاء ألبوم جديد",
|
||||||
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
|
"common_server_error": "يرجى التحقق من اتصال الشبكة الخاص بك ، والتأكد من أن الجهاز قابل للوصول وإصدارات التطبيق/الجهاز متوافقة.",
|
||||||
|
"completed": "Completed",
|
||||||
"confirm": "تأكيد",
|
"confirm": "تأكيد",
|
||||||
"confirm_admin_password": "تأكيد كلمة مرور المسؤول",
|
"confirm_admin_password": "تأكيد كلمة مرور المسؤول",
|
||||||
"confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟",
|
"confirm_delete_face": "هل أنت متأكد من حذف وجه {name} من الأصول؟",
|
||||||
"confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟",
|
"confirm_delete_shared_link": "هل أنت متأكد أنك تريد حذف هذا الرابط المشترك؟",
|
||||||
"confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟",
|
"confirm_keep_this_delete_others": "سيتم حذف جميع الأصول الأخرى في المجموعة باستثناء هذا الأصل. هل أنت متأكد من أنك تريد المتابعة؟",
|
||||||
"confirm_new_pin_code": "ثبت الرقم السري الجديد",
|
|
||||||
"confirm_password": "تأكيد كلمة المرور",
|
"confirm_password": "تأكيد كلمة المرور",
|
||||||
"contain": "محتواة",
|
"contain": "محتواة",
|
||||||
"context": "السياق",
|
"context": "السياق",
|
||||||
"continue": "متابعة",
|
"continue": "متابعة",
|
||||||
|
"control_bottom_app_bar_album_info_shared": "{} items · Shared",
|
||||||
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
|
"control_bottom_app_bar_create_new_album": "إنشاء ألبوم جديد",
|
||||||
"control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق",
|
"control_bottom_app_bar_delete_from_immich": " حذف منال تطبيق",
|
||||||
"control_bottom_app_bar_delete_from_local": "حذف من الجهاز",
|
"control_bottom_app_bar_delete_from_local": "حذف من الجهاز",
|
||||||
"control_bottom_app_bar_edit_location": "تحديد الوجهة",
|
"control_bottom_app_bar_edit_location": "تحديد الوجهة",
|
||||||
"control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت",
|
"control_bottom_app_bar_edit_time": "تحرير التاريخ والوقت",
|
||||||
|
"control_bottom_app_bar_share_link": "Share Link",
|
||||||
"control_bottom_app_bar_share_to": "مشاركة إلى",
|
"control_bottom_app_bar_share_to": "مشاركة إلى",
|
||||||
"control_bottom_app_bar_trash_from_immich": "حذفه ونقله في سله المهملات",
|
"control_bottom_app_bar_trash_from_immich": "حذفه ونقله في سله المهملات",
|
||||||
"copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.",
|
"copied_image_to_clipboard": "تم نسخ الصورة إلى الحافظة.",
|
||||||
@@ -603,6 +677,7 @@
|
|||||||
"create_link": "إنشاء رابط",
|
"create_link": "إنشاء رابط",
|
||||||
"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_person": "إنشاء شخص جديد",
|
||||||
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
|
"create_new_person_hint": "تعيين المحتويات المحددة لشخص جديد",
|
||||||
"create_new_user": "إنشاء مستخدم جديد",
|
"create_new_user": "إنشاء مستخدم جديد",
|
||||||
@@ -612,9 +687,10 @@
|
|||||||
"create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.",
|
"create_tag_description": "أنشئ علامة جديدة. بالنسبة للعلامات المتداخلة، يرجى إدخال المسار الكامل للعلامة بما في ذلك الخطوط المائلة للأمام.",
|
||||||
"create_user": "إنشاء مستخدم",
|
"create_user": "إنشاء مستخدم",
|
||||||
"created": "تم الإنشاء",
|
"created": "تم الإنشاء",
|
||||||
|
"crop": "Crop",
|
||||||
"curated_object_page_title": "أشياء",
|
"curated_object_page_title": "أشياء",
|
||||||
"current_device": "الجهاز الحالي",
|
"current_device": "الجهاز الحالي",
|
||||||
"current_pin_code": "الرقم السري الحالي",
|
"current_server_address": "Current server address",
|
||||||
"custom_locale": "لغة مخصصة",
|
"custom_locale": "لغة مخصصة",
|
||||||
"custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة",
|
"custom_locale_description": "تنسيق التواريخ والأرقام بناءً على اللغة والمنطقة",
|
||||||
"daily_title_text_date": "E ، MMM DD",
|
"daily_title_text_date": "E ، MMM DD",
|
||||||
@@ -665,6 +741,7 @@
|
|||||||
"direction": "الإتجاه",
|
"direction": "الإتجاه",
|
||||||
"disabled": "معطل",
|
"disabled": "معطل",
|
||||||
"disallow_edits": "منع التعديلات",
|
"disallow_edits": "منع التعديلات",
|
||||||
|
"discord": "Discord",
|
||||||
"discover": "اكتشف",
|
"discover": "اكتشف",
|
||||||
"dismiss_all_errors": "تجاهل كافة الأخطاء",
|
"dismiss_all_errors": "تجاهل كافة الأخطاء",
|
||||||
"dismiss_error": "تجاهل الخطأ",
|
"dismiss_error": "تجاهل الخطأ",
|
||||||
@@ -676,12 +753,26 @@
|
|||||||
"documentation": "الوثائق",
|
"documentation": "الوثائق",
|
||||||
"done": "تم",
|
"done": "تم",
|
||||||
"download": "تنزيل",
|
"download": "تنزيل",
|
||||||
|
"download_canceled": "Download canceled",
|
||||||
|
"download_complete": "Download complete",
|
||||||
|
"download_enqueue": "Download enqueued",
|
||||||
|
"download_error": "Download Error",
|
||||||
|
"download_failed": "Download failed",
|
||||||
|
"download_filename": "file: {}",
|
||||||
|
"download_finished": "Download finished",
|
||||||
"download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة",
|
"download_include_embedded_motion_videos": "مقاطع الفيديو المدمجة",
|
||||||
"download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل",
|
"download_include_embedded_motion_videos_description": "تضمين مقاطع الفيديو المضمنة في الصور المتحركة كملف منفصل",
|
||||||
|
"download_notfound": "Download not found",
|
||||||
|
"download_paused": "Download paused",
|
||||||
"download_settings": "التنزيلات",
|
"download_settings": "التنزيلات",
|
||||||
"download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات",
|
"download_settings_description": "إدارة الإعدادات المتعلقة بتنزيل المحتويات",
|
||||||
|
"download_started": "Download started",
|
||||||
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
|
"download_waiting_to_retry": "Waiting to retry",
|
||||||
"downloading": "جارٍ التنزيل",
|
"downloading": "جارٍ التنزيل",
|
||||||
"downloading_asset_filename": "{filename} قيد التنزيل",
|
"downloading_asset_filename": "{filename} قيد التنزيل",
|
||||||
|
"downloading_media": "Downloading media",
|
||||||
"drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها",
|
"drop_files_to_upload": "قم بإسقاط الملفات في أي مكان لرفعها",
|
||||||
"duplicates": "التكرارات",
|
"duplicates": "التكرارات",
|
||||||
"duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
|
"duplicates_description": "قم بحل كل مجموعة من خلال الإشارة إلى التكرارات، إن وجدت",
|
||||||
@@ -711,15 +802,19 @@
|
|||||||
"editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
|
"editor_crop_tool_h2_aspect_ratios": "نسب العرض إلى الارتفاع",
|
||||||
"editor_crop_tool_h2_rotation": "التدوير",
|
"editor_crop_tool_h2_rotation": "التدوير",
|
||||||
"email": "البريد الإلكتروني",
|
"email": "البريد الإلكتروني",
|
||||||
|
"empty_folder": "This folder is empty",
|
||||||
"empty_trash": "أفرغ سلة المهملات",
|
"empty_trash": "أفرغ سلة المهملات",
|
||||||
"empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
|
"empty_trash_confirmation": "هل أنت متأكد أنك تريد إفراغ سلة المهملات؟ سيؤدي هذا إلى إزالة جميع المحتويات الموجودة في سلة المهملات بشكل نهائي من Immich.\nلا يمكنك التراجع عن هذا الإجراء!",
|
||||||
"enable": "تفعيل",
|
"enable": "تفعيل",
|
||||||
"enabled": "مفعل",
|
"enabled": "مفعل",
|
||||||
"end_date": "تاريخ الإنتهاء",
|
"end_date": "تاريخ الإنتهاء",
|
||||||
|
"enqueued": "Enqueued",
|
||||||
"enter_wifi_name": "Enter WiFi name",
|
"enter_wifi_name": "Enter WiFi name",
|
||||||
"error": "خطأ",
|
"error": "خطأ",
|
||||||
|
"error_change_sort_album": "Failed to change album sort order",
|
||||||
"error_delete_face": "حدث خطأ في حذف الوجه من الأصول",
|
"error_delete_face": "حدث خطأ في حذف الوجه من الأصول",
|
||||||
"error_loading_image": "حدث خطأ أثناء تحميل الصورة",
|
"error_loading_image": "حدث خطأ أثناء تحميل الصورة",
|
||||||
|
"error_saving_image": "Error: {}",
|
||||||
"error_title": "خطأ - حدث خللٌ ما",
|
"error_title": "خطأ - حدث خللٌ ما",
|
||||||
"errors": {
|
"errors": {
|
||||||
"cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي",
|
"cannot_navigate_next_asset": "لا يمكن الانتقال إلى المحتوى التالي",
|
||||||
@@ -732,6 +827,7 @@
|
|||||||
"cant_get_number_of_comments": "لا يمكن الحصول على عدد التعليقات",
|
"cant_get_number_of_comments": "لا يمكن الحصول على عدد التعليقات",
|
||||||
"cant_search_people": "لا يمكن البحث عن الناس",
|
"cant_search_people": "لا يمكن البحث عن الناس",
|
||||||
"cant_search_places": "لا يمكن البحث عن الأماكن",
|
"cant_search_places": "لا يمكن البحث عن الأماكن",
|
||||||
|
"cleared_jobs": "اُخليت المهام لـ: {job}",
|
||||||
"error_adding_assets_to_album": "حدث خطأٌ أثناء إضافة المحتويات إلى الألبوم",
|
"error_adding_assets_to_album": "حدث خطأٌ أثناء إضافة المحتويات إلى الألبوم",
|
||||||
"error_adding_users_to_album": "حدث خطأٌ أثناء إضافة المستخدمين إلى الألبوم",
|
"error_adding_users_to_album": "حدث خطأٌ أثناء إضافة المستخدمين إلى الألبوم",
|
||||||
"error_deleting_shared_user": "حدث خطأٌ أثناء حذف المستخدم المشترك",
|
"error_deleting_shared_user": "حدث خطأٌ أثناء حذف المستخدم المشترك",
|
||||||
@@ -740,6 +836,7 @@
|
|||||||
"error_removing_assets_from_album": "خطأٌّ في إزالة المحتويات من الألبوم، تحقق من وحدة التحكم للحصول على مزيدٍ من التفاصيل",
|
"error_removing_assets_from_album": "خطأٌّ في إزالة المحتويات من الألبوم، تحقق من وحدة التحكم للحصول على مزيدٍ من التفاصيل",
|
||||||
"error_selecting_all_assets": "خطأٌ في تحديد جميع المحتويات",
|
"error_selecting_all_assets": "خطأٌ في تحديد جميع المحتويات",
|
||||||
"exclusion_pattern_already_exists": "نمط الاستبعاد هذا موجود مسبقًا.",
|
"exclusion_pattern_already_exists": "نمط الاستبعاد هذا موجود مسبقًا.",
|
||||||
|
"failed_job_command": "فشل الأمر {command} لوظيفة: {job}",
|
||||||
"failed_to_create_album": "فشل إنشاء الألبوم",
|
"failed_to_create_album": "فشل إنشاء الألبوم",
|
||||||
"failed_to_create_shared_link": "فشل إنشاء رابط مشترك",
|
"failed_to_create_shared_link": "فشل إنشاء رابط مشترك",
|
||||||
"failed_to_edit_shared_link": "فشل تعديل الرابط المشترك",
|
"failed_to_edit_shared_link": "فشل تعديل الرابط المشترك",
|
||||||
@@ -756,6 +853,7 @@
|
|||||||
"paths_validation_failed": "فشل في التحقق من {paths, plural, one {# مسار} other {# مسارات}}",
|
"paths_validation_failed": "فشل في التحقق من {paths, plural, one {# مسار} other {# مسارات}}",
|
||||||
"profile_picture_transparent_pixels": "لا يمكن أن تحتوي صور الملف الشخصي على أجزاء/بكسلات شفافة. يرجى التكبير و/أو تحريك الصورة.",
|
"profile_picture_transparent_pixels": "لا يمكن أن تحتوي صور الملف الشخصي على أجزاء/بكسلات شفافة. يرجى التكبير و/أو تحريك الصورة.",
|
||||||
"quota_higher_than_disk_size": "لقد قمت بتعيين حصة نسبية أعلى من حجم القرص",
|
"quota_higher_than_disk_size": "لقد قمت بتعيين حصة نسبية أعلى من حجم القرص",
|
||||||
|
"repair_unable_to_check_items": "تعذر التحقق من {count, select, one {عنصر} other {عناصر}}",
|
||||||
"unable_to_add_album_users": "تعذر إضافة مستخدمين إلى الألبوم",
|
"unable_to_add_album_users": "تعذر إضافة مستخدمين إلى الألبوم",
|
||||||
"unable_to_add_assets_to_shared_link": "تعذر إضافة المحتويات إلى الرابط المشترك",
|
"unable_to_add_assets_to_shared_link": "تعذر إضافة المحتويات إلى الرابط المشترك",
|
||||||
"unable_to_add_comment": "تعذر إضافة التعليق",
|
"unable_to_add_comment": "تعذر إضافة التعليق",
|
||||||
@@ -773,6 +871,7 @@
|
|||||||
"unable_to_change_visibility": "غير قادر على تغيير الظهور لـ {count, plural, one {# شخص} other {# أشخاص}}",
|
"unable_to_change_visibility": "غير قادر على تغيير الظهور لـ {count, plural, one {# شخص} other {# أشخاص}}",
|
||||||
"unable_to_complete_oauth_login": "غير قادر على إكمال تسجيل الدخول عبر OAuth",
|
"unable_to_complete_oauth_login": "غير قادر على إكمال تسجيل الدخول عبر OAuth",
|
||||||
"unable_to_connect": "غير قادر على الإتصال",
|
"unable_to_connect": "غير قادر على الإتصال",
|
||||||
|
"unable_to_connect_to_server": "غير قادر على الإتصال بالسيرفر",
|
||||||
"unable_to_copy_to_clipboard": "لا يمكن النسخ إلى الحافظة، تأكد من استخدامك للصفحة عبر https",
|
"unable_to_copy_to_clipboard": "لا يمكن النسخ إلى الحافظة، تأكد من استخدامك للصفحة عبر https",
|
||||||
"unable_to_create_admin_account": "غير قادر على إنشاء حساب المسؤول",
|
"unable_to_create_admin_account": "غير قادر على إنشاء حساب المسؤول",
|
||||||
"unable_to_create_api_key": "غير قادر على إنشاء مفتاح API جديد",
|
"unable_to_create_api_key": "غير قادر على إنشاء مفتاح API جديد",
|
||||||
@@ -796,6 +895,10 @@
|
|||||||
"unable_to_hide_person": "غير قادر على إخفاء الشخص",
|
"unable_to_hide_person": "غير قادر على إخفاء الشخص",
|
||||||
"unable_to_link_motion_video": "غير قادر على ربط فيديو الحركة",
|
"unable_to_link_motion_video": "غير قادر على ربط فيديو الحركة",
|
||||||
"unable_to_link_oauth_account": "غير قادر على ربط حساب OAuth",
|
"unable_to_link_oauth_account": "غير قادر على ربط حساب OAuth",
|
||||||
|
"unable_to_load_album": "غير قادر على تحميل الألبوم",
|
||||||
|
"unable_to_load_asset_activity": "غير قادر على تحميل نشاط المحتويات",
|
||||||
|
"unable_to_load_items": "غير قادر على تحميل العناصر",
|
||||||
|
"unable_to_load_liked_status": "غير قادر على تحميل حالة الإعجاب",
|
||||||
"unable_to_log_out_all_devices": "غير قادر على تسجيل الخروج من جميع الأجهزة",
|
"unable_to_log_out_all_devices": "غير قادر على تسجيل الخروج من جميع الأجهزة",
|
||||||
"unable_to_log_out_device": "غير قادر على تسجيل الخروج من الجهاز",
|
"unable_to_log_out_device": "غير قادر على تسجيل الخروج من الجهاز",
|
||||||
"unable_to_login_with_oauth": "غير قادر على تسجيل الدخول باستخدام OAuth",
|
"unable_to_login_with_oauth": "غير قادر على تسجيل الدخول باستخدام OAuth",
|
||||||
@@ -806,9 +909,11 @@
|
|||||||
"unable_to_remove_album_users": "تعذر إزالة المستخدمين من الألبوم",
|
"unable_to_remove_album_users": "تعذر إزالة المستخدمين من الألبوم",
|
||||||
"unable_to_remove_api_key": "تعذر إزالة مفتاح API",
|
"unable_to_remove_api_key": "تعذر إزالة مفتاح API",
|
||||||
"unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
|
"unable_to_remove_assets_from_shared_link": "غير قادر على إزالة المحتويات من الرابط المشترك",
|
||||||
|
"unable_to_remove_deleted_assets": "غير قادر على إزالة الملفات غير المتصلة",
|
||||||
"unable_to_remove_library": "غير قادر على إزالة المكتبة",
|
"unable_to_remove_library": "غير قادر على إزالة المكتبة",
|
||||||
"unable_to_remove_partner": "غير قادر على إزالة الشريك",
|
"unable_to_remove_partner": "غير قادر على إزالة الشريك",
|
||||||
"unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
|
"unable_to_remove_reaction": "غير قادر على إزالة رد الفعل",
|
||||||
|
"unable_to_repair_items": "غير قادر على إصلاح العناصر",
|
||||||
"unable_to_reset_password": "غير قادر على إعادة تعيين كلمة المرور",
|
"unable_to_reset_password": "غير قادر على إعادة تعيين كلمة المرور",
|
||||||
"unable_to_resolve_duplicate": "غير قادر على حل التكرارات",
|
"unable_to_resolve_duplicate": "غير قادر على حل التكرارات",
|
||||||
"unable_to_restore_assets": "غير قادر على استعادة المحتويات",
|
"unable_to_restore_assets": "غير قادر على استعادة المحتويات",
|
||||||
@@ -843,6 +948,10 @@
|
|||||||
"exif_bottom_sheet_location": "موقع",
|
"exif_bottom_sheet_location": "موقع",
|
||||||
"exif_bottom_sheet_people": "الناس",
|
"exif_bottom_sheet_people": "الناس",
|
||||||
"exif_bottom_sheet_person_add_person": "اضف اسما",
|
"exif_bottom_sheet_person_add_person": "اضف اسما",
|
||||||
|
"exif_bottom_sheet_person_age": "Age {}",
|
||||||
|
"exif_bottom_sheet_person_age_months": "Age {} months",
|
||||||
|
"exif_bottom_sheet_person_age_year_months": "Age 1 year, {} months",
|
||||||
|
"exif_bottom_sheet_person_age_years": "Age {}",
|
||||||
"exit_slideshow": "خروج من العرض التقديمي",
|
"exit_slideshow": "خروج من العرض التقديمي",
|
||||||
"expand_all": "توسيع الكل",
|
"expand_all": "توسيع الكل",
|
||||||
"experimental_settings_new_asset_list_subtitle": "أعمال جارية",
|
"experimental_settings_new_asset_list_subtitle": "أعمال جارية",
|
||||||
@@ -859,9 +968,12 @@
|
|||||||
"extension": "الإمتداد",
|
"extension": "الإمتداد",
|
||||||
"external": "خارجي",
|
"external": "خارجي",
|
||||||
"external_libraries": "المكتبات الخارجية",
|
"external_libraries": "المكتبات الخارجية",
|
||||||
|
"external_network": "External network",
|
||||||
"external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
"external_network_sheet_info": "When not on the preferred WiFi network, the app will connect to the server through the first of the below URLs it can reach, starting from top to bottom",
|
||||||
"face_unassigned": "غير معين",
|
"face_unassigned": "غير معين",
|
||||||
|
"failed": "Failed",
|
||||||
"failed_to_load_assets": "فشل تحميل الأصول",
|
"failed_to_load_assets": "فشل تحميل الأصول",
|
||||||
|
"failed_to_load_folder": "Failed to load folder",
|
||||||
"favorite": "مفضل",
|
"favorite": "مفضل",
|
||||||
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
|
"favorite_or_unfavorite_photo": "تفضيل أو إلغاء تفضيل الصورة",
|
||||||
"favorites": "المفضلة",
|
"favorites": "المفضلة",
|
||||||
@@ -873,18 +985,23 @@
|
|||||||
"file_name_or_extension": "اسم الملف أو امتداده",
|
"file_name_or_extension": "اسم الملف أو امتداده",
|
||||||
"filename": "اسم الملف",
|
"filename": "اسم الملف",
|
||||||
"filetype": "نوع الملف",
|
"filetype": "نوع الملف",
|
||||||
|
"filter": "Filter",
|
||||||
"filter_people": "تصفية الاشخاص",
|
"filter_people": "تصفية الاشخاص",
|
||||||
"find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
|
"find_them_fast": "يمكنك العثور عليها بسرعة بالاسم من خلال البحث",
|
||||||
"fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
|
"fix_incorrect_match": "إصلاح المطابقة غير الصحيحة",
|
||||||
|
"folder": "Folder",
|
||||||
|
"folder_not_found": "Folder not found",
|
||||||
"folders": "المجلدات",
|
"folders": "المجلدات",
|
||||||
"folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
|
"folders_feature_description": "تصفح عرض المجلد للصور ومقاطع الفيديو الموجودة على نظام الملفات",
|
||||||
"forward": "إلى الأمام",
|
"forward": "إلى الأمام",
|
||||||
"general": "عام",
|
"general": "عام",
|
||||||
"get_help": "الحصول على المساعدة",
|
"get_help": "الحصول على المساعدة",
|
||||||
|
"get_wifiname_error": "Could not get Wi-Fi name. Make sure you have granted the necessary permissions and are connected to a Wi-Fi network",
|
||||||
"getting_started": "البدء",
|
"getting_started": "البدء",
|
||||||
"go_back": "الرجوع للخلف",
|
"go_back": "الرجوع للخلف",
|
||||||
"go_to_folder": "اذهب إلى المجلد",
|
"go_to_folder": "اذهب إلى المجلد",
|
||||||
"go_to_search": "اذهب إلى البحث",
|
"go_to_search": "اذهب إلى البحث",
|
||||||
|
"grant_permission": "Grant permission",
|
||||||
"group_albums_by": "تجميع الألبومات حسب...",
|
"group_albums_by": "تجميع الألبومات حسب...",
|
||||||
"group_country": "مجموعة البلد",
|
"group_country": "مجموعة البلد",
|
||||||
"group_no": "بدون تجميع",
|
"group_no": "بدون تجميع",
|
||||||
@@ -894,6 +1011,12 @@
|
|||||||
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
|
"haptic_feedback_switch": "تمكين ردود الفعل اللمسية",
|
||||||
"haptic_feedback_title": "ردود فعل لمسية",
|
"haptic_feedback_title": "ردود فعل لمسية",
|
||||||
"has_quota": "محدد بحصة",
|
"has_quota": "محدد بحصة",
|
||||||
|
"header_settings_add_header_tip": "Add Header",
|
||||||
|
"header_settings_field_validator_msg": "Value cannot be empty",
|
||||||
|
"header_settings_header_name_input": "Header name",
|
||||||
|
"header_settings_header_value_input": "Header value",
|
||||||
|
"headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request",
|
||||||
|
"headers_settings_tile_title": "Custom proxy headers",
|
||||||
"hi_user": "مرحبا {name} ({email})",
|
"hi_user": "مرحبا {name} ({email})",
|
||||||
"hide_all_people": "إخفاء جميع الأشخاص",
|
"hide_all_people": "إخفاء جميع الأشخاص",
|
||||||
"hide_gallery": "اخفاء المعرض",
|
"hide_gallery": "اخفاء المعرض",
|
||||||
@@ -917,6 +1040,8 @@
|
|||||||
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
|
"home_page_upload_err_limit": "لا يمكن إلا تحميل 30 أحد الأصول في وقت واحد ، سوف يتخطى",
|
||||||
"host": "المضيف",
|
"host": "المضيف",
|
||||||
"hour": "ساعة",
|
"hour": "ساعة",
|
||||||
|
"ignore_icloud_photos": "Ignore iCloud photos",
|
||||||
|
"ignore_icloud_photos_description": "Photos that are stored on iCloud will not be uploaded to the Immich server",
|
||||||
"image": "صورة",
|
"image": "صورة",
|
||||||
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}",
|
"image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}",
|
||||||
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}",
|
"image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}",
|
||||||
@@ -928,6 +1053,7 @@
|
|||||||
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}",
|
"image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}",
|
||||||
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}",
|
"image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}",
|
||||||
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
|
"image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}",
|
||||||
|
"image_saved_successfully": "Image saved",
|
||||||
"image_viewer_page_state_provider_download_started": "بدأ التنزيل",
|
"image_viewer_page_state_provider_download_started": "بدأ التنزيل",
|
||||||
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
|
"image_viewer_page_state_provider_download_success": "تم التنزيل بنجاح",
|
||||||
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
|
"image_viewer_page_state_provider_share_error": "خطأ في المشاركة",
|
||||||
@@ -949,6 +1075,8 @@
|
|||||||
"night_at_midnight": "كل ليلة عند منتصف الليل",
|
"night_at_midnight": "كل ليلة عند منتصف الليل",
|
||||||
"night_at_twoam": "كل ليلة الساعة 2 صباحا"
|
"night_at_twoam": "كل ليلة الساعة 2 صباحا"
|
||||||
},
|
},
|
||||||
|
"invalid_date": "Invalid date",
|
||||||
|
"invalid_date_format": "Invalid date format",
|
||||||
"invite_people": "دعوة الأشخاص",
|
"invite_people": "دعوة الأشخاص",
|
||||||
"invite_to_album": "دعوة إلى الألبوم",
|
"invite_to_album": "دعوة إلى الألبوم",
|
||||||
"items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
|
"items_count": "{count, plural, one {# عنصر} other {# عناصر}}",
|
||||||
@@ -984,6 +1112,9 @@
|
|||||||
"list": "قائمة",
|
"list": "قائمة",
|
||||||
"loading": "تحميل",
|
"loading": "تحميل",
|
||||||
"loading_search_results_failed": "فشل تحميل نتائج البحث",
|
"loading_search_results_failed": "فشل تحميل نتائج البحث",
|
||||||
|
"local_network": "Local network",
|
||||||
|
"local_network_sheet_info": "The app will connect to the server through this URL when using the specified Wi-Fi network",
|
||||||
|
"location_permission": "Location permission",
|
||||||
"location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
|
"location_permission_content": "In order to use the auto-switching feature, Immich needs precise location permission so it can read the current WiFi network's name",
|
||||||
"location_picker_choose_on_map": "اختر على الخريطة",
|
"location_picker_choose_on_map": "اختر على الخريطة",
|
||||||
"location_picker_latitude_error": "أدخل خط عرض صالح",
|
"location_picker_latitude_error": "أدخل خط عرض صالح",
|
||||||
@@ -999,6 +1130,7 @@
|
|||||||
"login_form_api_exception": " استثناء برمجة التطبيقات. يرجى التحقق من عنوان الخادم والمحاولة مرة أخرى ",
|
"login_form_api_exception": " استثناء برمجة التطبيقات. يرجى التحقق من عنوان الخادم والمحاولة مرة أخرى ",
|
||||||
"login_form_back_button_text": "الرجوع للخلف",
|
"login_form_back_button_text": "الرجوع للخلف",
|
||||||
"login_form_email_hint": "yoursemail@email.com",
|
"login_form_email_hint": "yoursemail@email.com",
|
||||||
|
"login_form_endpoint_hint": "http://your-server-ip:port",
|
||||||
"login_form_endpoint_url": "url نقطة نهاية الخادم",
|
"login_form_endpoint_url": "url نقطة نهاية الخادم",
|
||||||
"login_form_err_http": "يرجى تحديد http:// أو https://",
|
"login_form_err_http": "يرجى تحديد http:// أو https://",
|
||||||
"login_form_err_invalid_email": "بريد إلكتروني خاطئ",
|
"login_form_err_invalid_email": "بريد إلكتروني خاطئ",
|
||||||
@@ -1032,6 +1164,8 @@
|
|||||||
"manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها",
|
"manage_your_devices": "إدارة الأجهزة التي تم تسجيل الدخول إليها",
|
||||||
"manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك",
|
"manage_your_oauth_connection": "إدارة اتصال OAuth الخاص بك",
|
||||||
"map": "الخريطة",
|
"map": "الخريطة",
|
||||||
|
"map_assets_in_bound": "{} photo",
|
||||||
|
"map_assets_in_bounds": "{} photos",
|
||||||
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
|
"map_cannot_get_user_location": "لا يمكن الحصول على موقع المستخدم",
|
||||||
"map_location_dialog_yes": "نعم",
|
"map_location_dialog_yes": "نعم",
|
||||||
"map_location_picker_page_use_location": "استخدم هذا الموقع",
|
"map_location_picker_page_use_location": "استخدم هذا الموقع",
|
||||||
@@ -1045,7 +1179,9 @@
|
|||||||
"map_settings": "إعدادات الخريطة",
|
"map_settings": "إعدادات الخريطة",
|
||||||
"map_settings_dark_mode": "الوضع المظلم",
|
"map_settings_dark_mode": "الوضع المظلم",
|
||||||
"map_settings_date_range_option_day": "24 ساعة الماضية",
|
"map_settings_date_range_option_day": "24 ساعة الماضية",
|
||||||
|
"map_settings_date_range_option_days": "Past {} days",
|
||||||
"map_settings_date_range_option_year": "السنة الفائتة",
|
"map_settings_date_range_option_year": "السنة الفائتة",
|
||||||
|
"map_settings_date_range_option_years": "Past {} years",
|
||||||
"map_settings_dialog_title": "إعدادات الخريطة",
|
"map_settings_dialog_title": "إعدادات الخريطة",
|
||||||
"map_settings_include_show_archived": "تشمل الأرشفة",
|
"map_settings_include_show_archived": "تشمل الأرشفة",
|
||||||
"map_settings_include_show_partners": "تضمين الشركاء",
|
"map_settings_include_show_partners": "تضمين الشركاء",
|
||||||
@@ -1060,6 +1196,8 @@
|
|||||||
"memories_setting_description": "إدارة ما تراه في ذكرياتك",
|
"memories_setting_description": "إدارة ما تراه في ذكرياتك",
|
||||||
"memories_start_over": "ابدأ من جديد",
|
"memories_start_over": "ابدأ من جديد",
|
||||||
"memories_swipe_to_close": "اسحب لأعلى للإغلاق",
|
"memories_swipe_to_close": "اسحب لأعلى للإغلاق",
|
||||||
|
"memories_year_ago": "A year ago",
|
||||||
|
"memories_years_ago": "{} years ago",
|
||||||
"memory": "ذكرى",
|
"memory": "ذكرى",
|
||||||
"memory_lane_title": "ذكرياتٌ من {title}",
|
"memory_lane_title": "ذكرياتٌ من {title}",
|
||||||
"menu": "القائمة",
|
"menu": "القائمة",
|
||||||
@@ -1083,12 +1221,13 @@
|
|||||||
"my_albums": "ألبوماتي",
|
"my_albums": "ألبوماتي",
|
||||||
"name": "الاسم",
|
"name": "الاسم",
|
||||||
"name_or_nickname": "الاسم أو اللقب",
|
"name_or_nickname": "الاسم أو اللقب",
|
||||||
|
"networking_settings": "Networking",
|
||||||
|
"networking_subtitle": "Manage the server endpoint settings",
|
||||||
"never": "أبداً",
|
"never": "أبداً",
|
||||||
"new_album": "البوم جديد",
|
"new_album": "البوم جديد",
|
||||||
"new_api_key": "مفتاح API جديد",
|
"new_api_key": "مفتاح API جديد",
|
||||||
"new_password": "كلمة المرور الجديدة",
|
"new_password": "كلمة المرور الجديدة",
|
||||||
"new_person": "شخص جديد",
|
"new_person": "شخص جديد",
|
||||||
"new_pin_code": "الرقم السري الجديد",
|
|
||||||
"new_user_created": "تم إنشاء مستخدم جديد",
|
"new_user_created": "تم إنشاء مستخدم جديد",
|
||||||
"new_version_available": "إصدار جديد متاح",
|
"new_version_available": "إصدار جديد متاح",
|
||||||
"newest_first": "الأحدث أولاً",
|
"newest_first": "الأحدث أولاً",
|
||||||
@@ -1112,6 +1251,7 @@
|
|||||||
"no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية",
|
"no_results_description": "جرب كلمة رئيسية مرادفة أو أكثر عمومية",
|
||||||
"no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك",
|
"no_shared_albums_message": "قم بإنشاء ألبوم لمشاركة الصور ومقاطع الفيديو مع الأشخاص في شبكتك",
|
||||||
"not_in_any_album": "ليست في أي ألبوم",
|
"not_in_any_album": "ليست في أي ألبوم",
|
||||||
|
"not_selected": "Not selected",
|
||||||
"note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل",
|
"note_apply_storage_label_to_previously_uploaded assets": "ملاحظة: لتطبيق تسمية التخزين على المحتويات التي تم رفعها مسبقًا، قم بتشغيل",
|
||||||
"notes": "ملاحظات",
|
"notes": "ملاحظات",
|
||||||
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
|
"notification_permission_dialog_content": "لتمكين الإخطارات ، انتقل إلى الإعدادات و اختار السماح.",
|
||||||
@@ -1121,13 +1261,18 @@
|
|||||||
"notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني",
|
"notification_toggle_setting_description": "تفعيل إشعارات البريد الإلكتروني",
|
||||||
"notifications": "إشعارات",
|
"notifications": "إشعارات",
|
||||||
"notifications_setting_description": "إدارة الإشعارات",
|
"notifications_setting_description": "إدارة الإشعارات",
|
||||||
|
"oauth": "OAuth",
|
||||||
"official_immich_resources": "الموارد الرسمية لشركة Immich",
|
"official_immich_resources": "الموارد الرسمية لشركة Immich",
|
||||||
"offline": "غير متصل",
|
"offline": "غير متصل",
|
||||||
|
"offline_paths": "مسارات غير متصلة",
|
||||||
|
"offline_paths_description": "قد تكون هذه النتائج بسبب الحذف اليدوي للملفات التي لا تشكل جزءًا من مكتبة خارجية.",
|
||||||
"ok": "نعم",
|
"ok": "نعم",
|
||||||
"oldest_first": "الأقدم أولا",
|
"oldest_first": "الأقدم أولا",
|
||||||
|
"on_this_device": "On this device",
|
||||||
"onboarding": "الإعداد الأولي",
|
"onboarding": "الإعداد الأولي",
|
||||||
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
|
"onboarding_privacy_description": "تعتمد الميزات التالية (اختياري) على خدمات خارجية، ويمكن تعطيلها في أي وقت في إعدادات الإدارة.",
|
||||||
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
|
"onboarding_theme_description": "اختر نسق الألوان للنسخة الخاصة بك. يمكنك تغيير ذلك لاحقًا في إعداداتك.",
|
||||||
|
"onboarding_welcome_description": "لنقم بإعداد نسختك باستخدام بعض الإعدادات الشائعة.",
|
||||||
"onboarding_welcome_user": "مرحبا، {user}",
|
"onboarding_welcome_user": "مرحبا، {user}",
|
||||||
"online": "متصل",
|
"online": "متصل",
|
||||||
"only_favorites": "المفضلة فقط",
|
"only_favorites": "المفضلة فقط",
|
||||||
@@ -1147,12 +1292,14 @@
|
|||||||
"partner_can_access": "يستطيع {partner} الوصول",
|
"partner_can_access": "يستطيع {partner} الوصول",
|
||||||
"partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة",
|
"partner_can_access_assets": "جميع الصور ومقاطع الفيديو الخاصة بك باستثناء تلك الموجودة في المؤرشفة والمحذوفة",
|
||||||
"partner_can_access_location": "الموقع الذي تم التقاط صورك فيه",
|
"partner_can_access_location": "الموقع الذي تم التقاط صورك فيه",
|
||||||
|
"partner_list_user_photos": "{user}'s photos",
|
||||||
"partner_list_view_all": "عرض الكل",
|
"partner_list_view_all": "عرض الكل",
|
||||||
"partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.",
|
"partner_page_empty_message": "لم يتم مشاركة صورك بعد مع أي شريك.",
|
||||||
"partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة",
|
"partner_page_no_more_users": "لا مزيد من المستخدمين لإضافة",
|
||||||
"partner_page_partner_add_failed": "فشل في إضافة شريك",
|
"partner_page_partner_add_failed": "فشل في إضافة شريك",
|
||||||
"partner_page_select_partner": "حدد شريكًا",
|
"partner_page_select_partner": "حدد شريكًا",
|
||||||
"partner_page_shared_to_title": "مشترك ل",
|
"partner_page_shared_to_title": "مشترك ل",
|
||||||
|
"partner_page_stop_sharing_content": "{} will no longer be able to access your photos.",
|
||||||
"partner_sharing": "مشاركة الشركاء",
|
"partner_sharing": "مشاركة الشركاء",
|
||||||
"partners": "الشركاء",
|
"partners": "الشركاء",
|
||||||
"password": "كلمة المرور",
|
"password": "كلمة المرور",
|
||||||
@@ -1198,9 +1345,6 @@
|
|||||||
"photos_count": "{count, plural, one {{count, number} صورة} other {{count, number} صور}}",
|
"photos_count": "{count, plural, one {{count, number} صورة} other {{count, number} صور}}",
|
||||||
"photos_from_previous_years": "صور من السنوات السابقة",
|
"photos_from_previous_years": "صور من السنوات السابقة",
|
||||||
"pick_a_location": "اختر موقعًا",
|
"pick_a_location": "اختر موقعًا",
|
||||||
"pin_code_changed_successfully": "تم تغير الرقم السري",
|
|
||||||
"pin_code_reset_successfully": "تم اعادة تعيين الرقم السري",
|
|
||||||
"pin_code_setup_successfully": "تم انشاء رقم سري",
|
|
||||||
"place": "مكان",
|
"place": "مكان",
|
||||||
"places": "الأماكن",
|
"places": "الأماكن",
|
||||||
"places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}",
|
"places_count": "{count, plural, one {{count, number} مكان} other {{count, number} أماكن}}",
|
||||||
@@ -1209,6 +1353,7 @@
|
|||||||
"play_motion_photo": "تشغيل الصور المتحركة",
|
"play_motion_photo": "تشغيل الصور المتحركة",
|
||||||
"play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
|
"play_or_pause_video": "تشغيل الفيديو أو إيقافه مؤقتًا",
|
||||||
"port": "المنفذ",
|
"port": "المنفذ",
|
||||||
|
"preferences_settings_subtitle": "Manage the app's preferences",
|
||||||
"preferences_settings_title": "التفضيلات",
|
"preferences_settings_title": "التفضيلات",
|
||||||
"preset": "الإعداد المسبق",
|
"preset": "الإعداد المسبق",
|
||||||
"preview": "معاينة",
|
"preview": "معاينة",
|
||||||
@@ -1230,7 +1375,7 @@
|
|||||||
"public_share": "مشاركة عامة",
|
"public_share": "مشاركة عامة",
|
||||||
"purchase_account_info": "داعم",
|
"purchase_account_info": "داعم",
|
||||||
"purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر",
|
"purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر",
|
||||||
"purchase_activated_time": "تم التفعيل في {date}",
|
"purchase_activated_time": "تم التفعيل في {date, date}",
|
||||||
"purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح",
|
"purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح",
|
||||||
"purchase_button_activate": "تنشيط",
|
"purchase_button_activate": "تنشيط",
|
||||||
"purchase_button_buy": "شراء",
|
"purchase_button_buy": "شراء",
|
||||||
@@ -1273,6 +1418,7 @@
|
|||||||
"recent": "حديث",
|
"recent": "حديث",
|
||||||
"recent-albums": "ألبومات الحديثة",
|
"recent-albums": "ألبومات الحديثة",
|
||||||
"recent_searches": "عمليات البحث الأخيرة",
|
"recent_searches": "عمليات البحث الأخيرة",
|
||||||
|
"recently_added": "Recently added",
|
||||||
"recently_added_page_title": "أضيف مؤخرا",
|
"recently_added_page_title": "أضيف مؤخرا",
|
||||||
"refresh": "تحديث",
|
"refresh": "تحديث",
|
||||||
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
|
"refresh_encoded_videos": "تحديث مقاطع الفيديو المشفرة",
|
||||||
@@ -1330,6 +1476,7 @@
|
|||||||
"role_editor": "المحرر",
|
"role_editor": "المحرر",
|
||||||
"role_viewer": "العارض",
|
"role_viewer": "العارض",
|
||||||
"save": "حفظ",
|
"save": "حفظ",
|
||||||
|
"save_to_gallery": "Save to gallery",
|
||||||
"saved_api_key": "تم حفظ مفتاح الـ API",
|
"saved_api_key": "تم حفظ مفتاح الـ API",
|
||||||
"saved_profile": "تم حفظ الملف",
|
"saved_profile": "تم حفظ الملف",
|
||||||
"saved_settings": "تم حفظ الإعدادات",
|
"saved_settings": "تم حفظ الإعدادات",
|
||||||
@@ -1351,17 +1498,31 @@
|
|||||||
"search_city": "البحث حسب المدينة...",
|
"search_city": "البحث حسب المدينة...",
|
||||||
"search_country": "البحث حسب الدولة...",
|
"search_country": "البحث حسب الدولة...",
|
||||||
"search_filter_apply": "اختار الفلتر ",
|
"search_filter_apply": "اختار الفلتر ",
|
||||||
|
"search_filter_camera_title": "Select camera type",
|
||||||
|
"search_filter_date": "Date",
|
||||||
|
"search_filter_date_interval": "{start} to {end}",
|
||||||
|
"search_filter_date_title": "Select a date range",
|
||||||
"search_filter_display_option_not_in_album": "ليس في الألبوم",
|
"search_filter_display_option_not_in_album": "ليس في الألبوم",
|
||||||
|
"search_filter_display_options": "Display Options",
|
||||||
|
"search_filter_filename": "Search by file name",
|
||||||
|
"search_filter_location": "Location",
|
||||||
|
"search_filter_location_title": "Select location",
|
||||||
|
"search_filter_media_type": "Media Type",
|
||||||
|
"search_filter_media_type_title": "Select media type",
|
||||||
|
"search_filter_people_title": "Select people",
|
||||||
"search_for": "البحث عن",
|
"search_for": "البحث عن",
|
||||||
"search_for_existing_person": "البحث عن شخص موجود",
|
"search_for_existing_person": "البحث عن شخص موجود",
|
||||||
|
"search_no_more_result": "No more results",
|
||||||
"search_no_people": "لا يوجد أشخاص",
|
"search_no_people": "لا يوجد أشخاص",
|
||||||
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
|
"search_no_people_named": "لا يوجد أشخاص بالاسم \"{name}\"",
|
||||||
|
"search_no_result": "No results found, try a different search term or combination",
|
||||||
"search_options": "خيارات البحث",
|
"search_options": "خيارات البحث",
|
||||||
"search_page_categories": "فئات",
|
"search_page_categories": "فئات",
|
||||||
"search_page_motion_photos": "الصور المتحركه",
|
"search_page_motion_photos": "الصور المتحركه",
|
||||||
"search_page_no_objects": "لا توجد معلومات عن أشياء متاحة",
|
"search_page_no_objects": "لا توجد معلومات عن أشياء متاحة",
|
||||||
"search_page_no_places": "لا توجد معلومات متوفرة للأماكن",
|
"search_page_no_places": "لا توجد معلومات متوفرة للأماكن",
|
||||||
"search_page_screenshots": "لقطات الشاشة",
|
"search_page_screenshots": "لقطات الشاشة",
|
||||||
|
"search_page_search_photos_videos": "Search for your photos and videos",
|
||||||
"search_page_selfies": " صور ذاتيه",
|
"search_page_selfies": " صور ذاتيه",
|
||||||
"search_page_things": "أشياء",
|
"search_page_things": "أشياء",
|
||||||
"search_page_view_all_button": "عرض الكل",
|
"search_page_view_all_button": "عرض الكل",
|
||||||
@@ -1400,6 +1561,7 @@
|
|||||||
"selected_count": "{count, plural, other {# محددة }}",
|
"selected_count": "{count, plural, other {# محددة }}",
|
||||||
"send_message": "إرسال رسالة",
|
"send_message": "إرسال رسالة",
|
||||||
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
|
"send_welcome_email": "إرسال بريدًا إلكترونيًا ترحيبيًا",
|
||||||
|
"server_endpoint": "Server Endpoint",
|
||||||
"server_info_box_app_version": "نسخة التطبيق",
|
"server_info_box_app_version": "نسخة التطبيق",
|
||||||
"server_info_box_server_url": "عنوان URL الخادم",
|
"server_info_box_server_url": "عنوان URL الخادم",
|
||||||
"server_offline": "الخادم غير متصل",
|
"server_offline": "الخادم غير متصل",
|
||||||
@@ -1420,20 +1582,28 @@
|
|||||||
"setting_image_viewer_preview_title": "تحميل صورة معاينة",
|
"setting_image_viewer_preview_title": "تحميل صورة معاينة",
|
||||||
"setting_image_viewer_title": "الصور",
|
"setting_image_viewer_title": "الصور",
|
||||||
"setting_languages_apply": "تغيير الإعدادات",
|
"setting_languages_apply": "تغيير الإعدادات",
|
||||||
|
"setting_languages_subtitle": "Change the app's language",
|
||||||
|
"setting_languages_title": "اللغات",
|
||||||
|
"setting_notifications_notify_failures_grace_period": "Notify background backup failures: {}",
|
||||||
|
"setting_notifications_notify_hours": "{} hours",
|
||||||
"setting_notifications_notify_immediately": "في الحال",
|
"setting_notifications_notify_immediately": "في الحال",
|
||||||
|
"setting_notifications_notify_minutes": "{} minutes",
|
||||||
"setting_notifications_notify_never": "أبداً",
|
"setting_notifications_notify_never": "أبداً",
|
||||||
|
"setting_notifications_notify_seconds": "{} seconds",
|
||||||
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
|
"setting_notifications_single_progress_subtitle": "معلومات التقدم التفصيلية تحميل لكل أصل",
|
||||||
"setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية",
|
"setting_notifications_single_progress_title": "إظهار تقدم التفاصيل الاحتياطية الخلفية",
|
||||||
"setting_notifications_subtitle": "اضبط تفضيلات الإخطار",
|
"setting_notifications_subtitle": "اضبط تفضيلات الإخطار",
|
||||||
"setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)",
|
"setting_notifications_total_progress_subtitle": "التقدم التحميل العام (تم القيام به/إجمالي الأصول)",
|
||||||
"setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز",
|
"setting_notifications_total_progress_title": "إظهار النسخ الاحتياطي الخلفية التقدم المحرز",
|
||||||
"setting_video_viewer_looping_title": "تكرار مقطع فيديو تلقائيًا",
|
"setting_video_viewer_looping_title": "تكرار مقطع فيديو تلقائيًا",
|
||||||
|
"setting_video_viewer_original_video_subtitle": "When streaming a video from the server, play the original even when a transcode is available. May lead to buffering. Videos available locally are played in original quality regardless of this setting.",
|
||||||
|
"setting_video_viewer_original_video_title": "Force original video",
|
||||||
"settings": "الإعدادات",
|
"settings": "الإعدادات",
|
||||||
"settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
|
"settings_require_restart": "يرجى إعادة تشغيل لتطبيق هذا الإعداد",
|
||||||
"settings_saved": "تم حفظ الإعدادات",
|
"settings_saved": "تم حفظ الإعدادات",
|
||||||
"setup_pin_code": "تحديد رقم سري",
|
|
||||||
"share": "مشاركة",
|
"share": "مشاركة",
|
||||||
"share_add_photos": "إضافة الصور",
|
"share_add_photos": "إضافة الصور",
|
||||||
|
"share_assets_selected": "{} selected",
|
||||||
"share_dialog_preparing": "تحضير...",
|
"share_dialog_preparing": "تحضير...",
|
||||||
"shared": "مُشتَرك",
|
"shared": "مُشتَرك",
|
||||||
"shared_album_activities_input_disable": "التعليق معطل",
|
"shared_album_activities_input_disable": "التعليق معطل",
|
||||||
@@ -1447,22 +1617,40 @@
|
|||||||
"shared_by_user": "تمت المشاركة بواسطة {user}",
|
"shared_by_user": "تمت المشاركة بواسطة {user}",
|
||||||
"shared_by_you": "تمت مشاركته من قِبلك",
|
"shared_by_you": "تمت مشاركته من قِبلك",
|
||||||
"shared_from_partner": "صور من {partner}",
|
"shared_from_partner": "صور من {partner}",
|
||||||
|
"shared_intent_upload_button_progress_text": "{} / {} Uploaded",
|
||||||
"shared_link_app_bar_title": "روابط مشتركة",
|
"shared_link_app_bar_title": "روابط مشتركة",
|
||||||
"shared_link_clipboard_copied_massage": "نسخ إلى الحافظة",
|
"shared_link_clipboard_copied_massage": "نسخ إلى الحافظة",
|
||||||
|
"shared_link_clipboard_text": "Link: {}\nPassword: {}",
|
||||||
"shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك",
|
"shared_link_create_error": "خطأ أثناء إنشاء رابط مشترك",
|
||||||
"shared_link_edit_description_hint": "أدخل وصف المشاركة",
|
"shared_link_edit_description_hint": "أدخل وصف المشاركة",
|
||||||
"shared_link_edit_expire_after_option_day": "يوم 1",
|
"shared_link_edit_expire_after_option_day": "يوم 1",
|
||||||
|
"shared_link_edit_expire_after_option_days": "{} days",
|
||||||
"shared_link_edit_expire_after_option_hour": "1 ساعة",
|
"shared_link_edit_expire_after_option_hour": "1 ساعة",
|
||||||
|
"shared_link_edit_expire_after_option_hours": "{} hours",
|
||||||
"shared_link_edit_expire_after_option_minute": "1 دقيقة",
|
"shared_link_edit_expire_after_option_minute": "1 دقيقة",
|
||||||
|
"shared_link_edit_expire_after_option_minutes": "{} minutes",
|
||||||
|
"shared_link_edit_expire_after_option_months": "{} months",
|
||||||
|
"shared_link_edit_expire_after_option_year": "{} year",
|
||||||
"shared_link_edit_password_hint": "أدخل كلمة مرور المشاركة",
|
"shared_link_edit_password_hint": "أدخل كلمة مرور المشاركة",
|
||||||
"shared_link_edit_submit_button": "تحديث الرابط",
|
"shared_link_edit_submit_button": "تحديث الرابط",
|
||||||
"shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم",
|
"shared_link_error_server_url_fetch": "لا يمكن جلب عنوان الخادم",
|
||||||
|
"shared_link_expires_day": "Expires in {} day",
|
||||||
|
"shared_link_expires_days": "Expires in {} days",
|
||||||
|
"shared_link_expires_hour": "Expires in {} hour",
|
||||||
|
"shared_link_expires_hours": "Expires in {} hours",
|
||||||
|
"shared_link_expires_minute": "Expires in {} minute",
|
||||||
|
"shared_link_expires_minutes": "Expires in {} minutes",
|
||||||
"shared_link_expires_never": "تنتهي ∞",
|
"shared_link_expires_never": "تنتهي ∞",
|
||||||
|
"shared_link_expires_second": "Expires in {} second",
|
||||||
|
"shared_link_expires_seconds": "Expires in {} seconds",
|
||||||
|
"shared_link_individual_shared": "Individual shared",
|
||||||
|
"shared_link_info_chip_metadata": "EXIF",
|
||||||
"shared_link_manage_links": "إدارة الروابط المشتركة",
|
"shared_link_manage_links": "إدارة الروابط المشتركة",
|
||||||
"shared_link_options": "خيارات الرابط المشترك",
|
"shared_link_options": "خيارات الرابط المشترك",
|
||||||
"shared_links": "روابط مشتركة",
|
"shared_links": "روابط مشتركة",
|
||||||
"shared_links_description": "وصف الروابط المشتركة",
|
"shared_links_description": "وصف الروابط المشتركة",
|
||||||
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
|
"shared_photos_and_videos_count": "{assetCount, plural, other {# الصور ومقاطع الفيديو المُشارَكة.}}",
|
||||||
|
"shared_with_me": "Shared with me",
|
||||||
"shared_with_partner": "تمت المشاركة مع {partner}",
|
"shared_with_partner": "تمت المشاركة مع {partner}",
|
||||||
"sharing": "مشاركة",
|
"sharing": "مشاركة",
|
||||||
"sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.",
|
"sharing_enter_password": "الرجاء إدخال كلمة المرور لعرض هذه الصفحة.",
|
||||||
@@ -1538,6 +1726,9 @@
|
|||||||
"support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
|
"support_third_party_description": "تم حزم تثبيت immich الخاص بك بواسطة جهة خارجية. قد تكون المشكلات التي تواجهها ناجمة عن هذه الحزمة، لذا يرجى طرح المشكلات معهم في المقام الأول باستخدام الروابط أدناه.",
|
||||||
"swap_merge_direction": "تبديل اتجاه الدمج",
|
"swap_merge_direction": "تبديل اتجاه الدمج",
|
||||||
"sync": "مزامنة",
|
"sync": "مزامنة",
|
||||||
|
"sync_albums": "Sync albums",
|
||||||
|
"sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums",
|
||||||
|
"sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich",
|
||||||
"tag": "العلامة",
|
"tag": "العلامة",
|
||||||
"tag_assets": "أصول العلامة",
|
"tag_assets": "أصول العلامة",
|
||||||
"tag_created": "تم إنشاء العلامة: {tag}",
|
"tag_created": "تم إنشاء العلامة: {tag}",
|
||||||
@@ -1552,8 +1743,14 @@
|
|||||||
"theme_selection": "اختيار السمة",
|
"theme_selection": "اختيار السمة",
|
||||||
"theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك",
|
"theme_selection_description": "قم بتعيين السمة تلقائيًا على اللون الفاتح أو الداكن بناءً على تفضيلات نظام المتصفح الخاص بك",
|
||||||
"theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول",
|
"theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول",
|
||||||
|
"theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})",
|
||||||
|
"theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.",
|
||||||
|
"theme_setting_colorful_interface_title": "Colorful interface",
|
||||||
"theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية",
|
"theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية",
|
||||||
"theme_setting_image_viewer_quality_title": "جودة عارض الصورة",
|
"theme_setting_image_viewer_quality_title": "جودة عارض الصورة",
|
||||||
|
"theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.",
|
||||||
|
"theme_setting_primary_color_title": "Primary color",
|
||||||
|
"theme_setting_system_primary_color_title": "Use system color",
|
||||||
"theme_setting_system_theme_switch": "تلقائي (اتبع إعداد النظام)",
|
"theme_setting_system_theme_switch": "تلقائي (اتبع إعداد النظام)",
|
||||||
"theme_setting_theme_subtitle": "اختر إعدادات مظهر التطبيق",
|
"theme_setting_theme_subtitle": "اختر إعدادات مظهر التطبيق",
|
||||||
"theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
|
"theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير",
|
||||||
@@ -1570,22 +1767,24 @@
|
|||||||
"to_parent": "انتقل إلى الوالد",
|
"to_parent": "انتقل إلى الوالد",
|
||||||
"to_trash": "حذف",
|
"to_trash": "حذف",
|
||||||
"toggle_settings": "الإعدادات",
|
"toggle_settings": "الإعدادات",
|
||||||
|
"toggle_theme": "تبديل المظهر الداكن",
|
||||||
"total": "الإجمالي",
|
"total": "الإجمالي",
|
||||||
"total_usage": "الاستخدام الإجمالي",
|
"total_usage": "الاستخدام الإجمالي",
|
||||||
"trash": "المهملات",
|
"trash": "المهملات",
|
||||||
"trash_all": "نقل الكل إلى سلة المهملات",
|
"trash_all": "نقل الكل إلى سلة المهملات",
|
||||||
"trash_count": "سلة المحملات {count, number}",
|
"trash_count": "سلة المحملات {count, number}",
|
||||||
"trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات",
|
"trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات",
|
||||||
|
"trash_emptied": "Emptied trash",
|
||||||
"trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.",
|
"trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.",
|
||||||
"trash_page_delete_all": "حذف الكل",
|
"trash_page_delete_all": "حذف الكل",
|
||||||
"trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق",
|
"trash_page_empty_trash_dialog_content": "هل تريد تفريغ أصولك المهملة؟ ستتم إزالة هذه العناصر نهائيًا من التطبيق",
|
||||||
|
"trash_page_info": "Trashed items will be permanently deleted after {} days",
|
||||||
"trash_page_no_assets": "لا توجد اصول في سله المهملات",
|
"trash_page_no_assets": "لا توجد اصول في سله المهملات",
|
||||||
"trash_page_restore_all": "استعادة الكل",
|
"trash_page_restore_all": "استعادة الكل",
|
||||||
"trash_page_select_assets_btn": "اختر الأصول ",
|
"trash_page_select_assets_btn": "اختر الأصول ",
|
||||||
|
"trash_page_title": "Trash ({})",
|
||||||
"trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
|
"trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.",
|
||||||
"type": "النوع",
|
"type": "النوع",
|
||||||
"unable_to_change_pin_code": "تفيير الرقم السري غير ممكن",
|
|
||||||
"unable_to_setup_pin_code": "انشاء الرقم السري غير ممكن",
|
|
||||||
"unarchive": "أخرج من الأرشيف",
|
"unarchive": "أخرج من الأرشيف",
|
||||||
"unarchived_count": "{count, plural, other {غير مؤرشفة #}}",
|
"unarchived_count": "{count, plural, other {غير مؤرشفة #}}",
|
||||||
"unfavorite": "أزل التفضيل",
|
"unfavorite": "أزل التفضيل",
|
||||||
@@ -1606,6 +1805,8 @@
|
|||||||
"unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة",
|
"unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة",
|
||||||
"unstack": "فك الكومه",
|
"unstack": "فك الكومه",
|
||||||
"unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس",
|
"unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس",
|
||||||
|
"untracked_files": "الملفات التي لم يتم تعقبها",
|
||||||
|
"untracked_files_decription": "لا يتم تعقب هذه الملفات بواسطة التطبيق. يمكن أن تكون نتيجةً لعمليات نقل فاشلة، أو عمليات رفع متقطعة، أو يتم تركها في الخلف بسبب خللاً ما",
|
||||||
"up_next": "التالي",
|
"up_next": "التالي",
|
||||||
"updated_password": "تم تحديث كلمة المرور",
|
"updated_password": "تم تحديث كلمة المرور",
|
||||||
"upload": "رفع",
|
"upload": "رفع",
|
||||||
@@ -1619,14 +1820,15 @@
|
|||||||
"upload_status_errors": "الأخطاء",
|
"upload_status_errors": "الأخطاء",
|
||||||
"upload_status_uploaded": "تم الرفع",
|
"upload_status_uploaded": "تم الرفع",
|
||||||
"upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.",
|
"upload_success": "تم الرفع بنجاح، قم بتحديث الصفحة لرؤية المحتويات المرفوعة الجديدة.",
|
||||||
|
"upload_to_immich": "Upload to Immich ({})",
|
||||||
|
"uploading": "Uploading",
|
||||||
"url": "عنوان URL",
|
"url": "عنوان URL",
|
||||||
"usage": "الاستخدام",
|
"usage": "الاستخدام",
|
||||||
|
"use_current_connection": "use current connection",
|
||||||
"use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
|
"use_custom_date_range": "استخدم النطاق الزمني المخصص بدلاً من ذلك",
|
||||||
"user": "مستخدم",
|
"user": "مستخدم",
|
||||||
"user_id": "معرف المستخدم",
|
"user_id": "معرف المستخدم",
|
||||||
"user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}",
|
"user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}",
|
||||||
"user_pin_code_settings": "الرقم السري",
|
|
||||||
"user_pin_code_settings_description": "تغير الرقم السري",
|
|
||||||
"user_purchase_settings": "الشراء",
|
"user_purchase_settings": "الشراء",
|
||||||
"user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك",
|
"user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك",
|
||||||
"user_role_set": "قم بتعيين {user} كـ {role}",
|
"user_role_set": "قم بتعيين {user} كـ {role}",
|
||||||
@@ -1637,10 +1839,16 @@
|
|||||||
"users": "المستخدمين",
|
"users": "المستخدمين",
|
||||||
"utilities": "أدوات",
|
"utilities": "أدوات",
|
||||||
"validate": "تحقْق",
|
"validate": "تحقْق",
|
||||||
|
"validate_endpoint_error": "Please enter a valid URL",
|
||||||
"variables": "المتغيرات",
|
"variables": "المتغيرات",
|
||||||
"version": "الإصدار",
|
"version": "الإصدار",
|
||||||
"version_announcement_closing": "صديقك، أليكس",
|
"version_announcement_closing": "صديقك، أليكس",
|
||||||
"version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.",
|
"version_announcement_message": "مرحبًا! يتوفر إصدار جديد من Immich. يُرجى تخصيص بعض الوقت لقراءة <link>ملاحظات الإصدار</link> للتأكد من تحديث إعداداتك لمنع أي أخطاء في التكوين، خاصة إذا كنت تستخدم WatchTower أو أي آلية تتولى تحديث مثيل Immich الخاص بك تلقائيًا.",
|
||||||
|
"version_announcement_overlay_release_notes": "ملاحظات الإصدار",
|
||||||
|
"version_announcement_overlay_text_1": "مرحبًا يا صديقي ، هناك إصدار جديد",
|
||||||
|
"version_announcement_overlay_text_2": "من فضلك خذ وقتك لزيارة",
|
||||||
|
"version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.",
|
||||||
|
"version_announcement_overlay_title": "نسخه جديده متاحه للخادم ",
|
||||||
"version_history": "تاريخ الإصدار",
|
"version_history": "تاريخ الإصدار",
|
||||||
"version_history_item": "تم تثبيت {version} في {date}",
|
"version_history_item": "تم تثبيت {version} في {date}",
|
||||||
"video": "فيديو",
|
"video": "فيديو",
|
||||||
|
|||||||
@@ -34,15 +34,18 @@
|
|||||||
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
|
"backup_database_enable_description": "Verilənlər bazasının ehtiyat nüsxələrini aktiv et",
|
||||||
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
|
"backup_settings": "Ehtiyat Nüsxə Parametrləri",
|
||||||
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
|
"backup_settings_description": "Verilənlər bazasının ehtiyat nüsxə parametrlərini idarə et",
|
||||||
|
"check_all": "Hamısını yoxla",
|
||||||
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
|
"config_set_by_file": "Konfiqurasiya hal-hazırda konfiqurasiya faylı ilə təyin olunub",
|
||||||
"confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?",
|
"confirm_delete_library": "{library} kitabxanasını silmək istədiyinizdən əminmisiniz?",
|
||||||
"confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın",
|
"confirm_email_below": "Təsdiqləmək üçün aşağıya {email} yazın",
|
||||||
"confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?",
|
"confirm_user_password_reset": "{user} adlı istifadəçinin şifrəsini sıfırlamaq istədiyinizdən əminmisiniz?",
|
||||||
"disable_login": "Giriş etməni söndür",
|
"disable_login": "Giriş etməni söndür",
|
||||||
"duplicate_detection_job_description": "Bənzər şəkilləri tapmaq üçün maşın öyrənməsini işə salın. Bu prosses Smart Search funksiyasına əsaslanır",
|
"duplicate_detection_job_description": "Bənzər şəkilləri tapmaq üçün maşın öyrənməsini işə salın. Bu prosses Smart Search funksiyasına əsaslanır",
|
||||||
|
"external_library_created_at": "Xarici kitabxana ({date} (tarixində yaradıldı)",
|
||||||
"external_library_management": "Xarici kitabxana idarəetməsi",
|
"external_library_management": "Xarici kitabxana idarəetməsi",
|
||||||
"face_detection": "Üz tanıma",
|
"face_detection": "Üz tanıma",
|
||||||
"force_delete_user_warning": "XƏBƏRDARLIQ: Bu əməliyyat istifadəçi və bütün məlumatları siləcəkdir. Bu prossesi və silinən faylları geri qaytarmaq olmaz.",
|
"force_delete_user_warning": "XƏBƏRDARLIQ: Bu əməliyyat istifadəçi və bütün məlumatları siləcəkdir. Bu prossesi və silinən faylları geri qaytarmaq olmaz.",
|
||||||
|
"forcing_refresh_library_files": "Bütün kitabxana fayllarını məcburi yeniləmə",
|
||||||
"image_format_description": "WebP, JPEG faylına görə daha kiçik həcmə sahibdir, lakin onu kodlaşdırmaq daha çox vaxt alır.",
|
"image_format_description": "WebP, JPEG faylına görə daha kiçik həcmə sahibdir, lakin onu kodlaşdırmaq daha çox vaxt alır.",
|
||||||
"image_preview_title": "Önizləmə parametrləri",
|
"image_preview_title": "Önizləmə parametrləri",
|
||||||
"image_quality": "Keyfiyyət",
|
"image_quality": "Keyfiyyət",
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
"library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
|
"library_watching_settings_description": "Dəyişdirilən faylları avtomatik olaraq yoxla",
|
||||||
"logging_enable_description": "Jurnalı aktivləşdir",
|
"logging_enable_description": "Jurnalı aktivləşdir",
|
||||||
"logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
|
"logging_level_description": "Aktiv edildikdə hansı jurnal səviyyəsi istifadə olunur.",
|
||||||
|
"logging_settings": "",
|
||||||
"machine_learning_clip_model": "CLIP modeli",
|
"machine_learning_clip_model": "CLIP modeli",
|
||||||
"machine_learning_clip_model_description": "<link>Burada</link>qeyd olunan CLIP modelinin adı. Modeli dəyişdirdikdən sonra bütün şəkillər üçün 'Ağıllı Axtarış' funksiyasını yenidən işə salmalısınız.",
|
"machine_learning_clip_model_description": "<link>Burada</link>qeyd olunan CLIP modelinin adı. Modeli dəyişdirdikdən sonra bütün şəkillər üçün 'Ağıllı Axtarış' funksiyasını yenidən işə salmalısınız.",
|
||||||
"machine_learning_duplicate_detection": "Dublikat Aşkarlama",
|
"machine_learning_duplicate_detection": "Dublikat Aşkarlama",
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user