diff --git a/.dockerignore b/.dockerignore
index a3096e7d40883..e182865ae0afd 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -22,6 +22,7 @@ open-api/typescript-sdk/node_modules/
server/coverage/
server/node_modules/
server/upload/
+server/src/queries
server/dist/
server/www/
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index 472954447fc03..c7519a4684f8f 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1 +1 @@
-custom: ["https://buy.immich.app"]
+custom: ['https://buy.immich.app']
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml
index 12ffc89ea2b5a..346c6e60f2e21 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.yaml
+++ b/.github/ISSUE_TEMPLATE/bug_report.yaml
@@ -83,7 +83,6 @@ body:
2.
3.
...
- render: bash
validations:
required: true
diff --git a/.github/labeler.yml b/.github/labeler.yml
index a0eec41346a68..2a9abc7840381 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -33,3 +33,8 @@ documentation:
- changed-files:
- any-glob-to-any-file:
- machine-learning/app/**
+
+changelog:translation:
+ - changed-files:
+ - any-glob-to-any-file:
+ - web/src/lib/i18n/*.json
diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index 948f67e3d4ebc..c12b6e607a69d 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -16,10 +16,28 @@ concurrency:
cancel-in-progress: true
jobs:
+ pre-job:
+ runs-on: ubuntu-latest
+ outputs:
+ should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - id: found_paths
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ mobile:
+ - 'mobile/**'
+ - name: Check if we should force jobs to run
+ id: should_force
+ run: echo "should_force=${{ github.event_name == 'workflow_call' || github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
+
build-sign-android:
name: Build and sign Android
+ needs: pre-job
# Skip when PR from a fork
- if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' }}
+ if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && needs.pre-job.outputs.should_run == 'true' }}
runs-on: macos-14
steps:
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 1ec17b381dbfd..5292075cce902 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -22,7 +22,7 @@ permissions:
jobs:
publish:
- name: Publish
+ name: CLI Publish
runs-on: ubuntu-latest
defaults:
run:
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 2da49a7310a2d..8a2ba9f841434 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -17,47 +17,109 @@ permissions:
packages: write
jobs:
- build_and_push:
- name: Build and Push
+ pre-job:
runs-on: ubuntu-latest
+ outputs:
+ should_run_server: ${{ steps.found_paths.outputs.server == '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:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - id: found_paths
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ server:
+ - 'server/**'
+ - 'openapi/**'
+ - 'web/**'
+ machine-learning:
+ - 'machine-learning/**'
+
+ - name: Check if we should force jobs to run
+ id: should_force
+ run: echo "should_force=${{ github.event_name == 'workflow_dispatch' || github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
+
+ retag_ml:
+ name: Re-Tag ML
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_ml == 'false' }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ suffix: ["", "-cuda", "-openvino", "-armnn"]
+ steps:
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ # Skip when PR from a fork
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Re-tag image
+ run: |
+ REGISTRY_NAME="ghcr.io"
+ REPOSITORY=${{ github.repository_owner }}/immich-machine-learning
+ TAG_OLD=main${{ matrix.suffix }}
+ TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
+ docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
+
+ retag_server:
+ name: Re-Tag Server
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_server == 'false' }}
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ suffix: [""]
+ steps:
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ # Skip when PR from a fork
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Re-tag image
+ run: |
+ REGISTRY_NAME="ghcr.io"
+ REPOSITORY=${{ github.repository_owner }}/immich-server
+ TAG_OLD=main${{ matrix.suffix }}
+ TAG_NEW=${{ github.event.number == 0 && github.ref_name || format('pr-{0}', github.event.number) }}${{ matrix.suffix }}
+ docker buildx imagetools create -t $REGISTRY_NAME/$REPOSITORY:$TAG_NEW $REGISTRY_NAME/$REPOSITORY:$TAG_OLD
+
+
+ build_and_push_ml:
+ name: Build and Push ML
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
+ runs-on: ubuntu-latest
+ env:
+ image: immich-machine-learning
+ context: machine-learning
+ file: machine-learning/Dockerfile
strategy:
# Prevent a failure in one image from stopping the other builds
fail-fast: false
matrix:
include:
- - image: immich-machine-learning
- context: machine-learning
- file: machine-learning/Dockerfile
- platforms: linux/amd64,linux/arm64
+ - platforms: linux/amd64,linux/arm64
device: cpu
- - image: immich-machine-learning
- context: machine-learning
- file: machine-learning/Dockerfile
- platforms: linux/amd64
+ - platforms: linux/amd64
device: cuda
suffix: -cuda
- - image: immich-machine-learning
- context: machine-learning
- file: machine-learning/Dockerfile
- platforms: linux/amd64
+ - platforms: linux/amd64
device: openvino
suffix: -openvino
- - image: immich-machine-learning
- context: machine-learning
- file: machine-learning/Dockerfile
- platforms: linux/arm64
+ - platforms: linux/arm64
device: armnn
suffix: -armnn
- - image: immich-server
- context: .
- file: server/Dockerfile
- platforms: linux/amd64,linux/arm64
- device: cpu
-
steps:
- name: Checkout
uses: actions/checkout@v4
@@ -93,8 +155,8 @@ jobs:
# Disable latest tag
latest=false
images: |
- name=ghcr.io/${{ github.repository_owner }}/${{matrix.image}}
- name=altran1502/${{matrix.image}},enable=${{ github.event_name == 'release' }}
+ name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
+ name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
tags: |
# Tag with branch name
type=ref,event=branch,suffix=${{ matrix.suffix }}
@@ -111,18 +173,18 @@ jobs:
# Essentially just ignore the cache output (PR can't write to registry cache)
echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
else
- echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ matrix.image }}" >> $GITHUB_OUTPUT
+ echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
fi
- name: Build and push image
uses: docker/build-push-action@v6.7.0
with:
- context: ${{ matrix.context }}
- file: ${{ matrix.file }}
+ context: ${{ env.context }}
+ file: ${{ env.file }}
platforms: ${{ matrix.platforms }}
# Skip pushing when PR from a fork
push: ${{ !github.event.pull_request.head.repo.fork }}
- cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{matrix.image}}
+ cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
cache-to: ${{ steps.cache-target.outputs.cache-to }}
tags: ${{ steps.metadata.outputs.tags }}
labels: ${{ steps.metadata.outputs.labels }}
@@ -132,3 +194,120 @@ jobs:
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
BUILD_SOURCE_REF=${{ github.ref_name }}
BUILD_SOURCE_COMMIT=${{ github.sha }}
+
+
+ build_and_push_server:
+ name: Build and Push Server
+ runs-on: ubuntu-latest
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
+ env:
+ image: immich-server
+ context: .
+ file: server/Dockerfile
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - platforms: linux/amd64,linux/arm64
+ device: cpu
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3.2.0
+
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3.6.1
+
+ - name: Login to Docker Hub
+ # Only push to Docker Hub when making a release
+ if: ${{ github.event_name == 'release' }}
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKERHUB_USERNAME }}
+ password: ${{ secrets.DOCKERHUB_TOKEN }}
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ # Skip when PR from a fork
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ with:
+ registry: ghcr.io
+ username: ${{ github.repository_owner }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Generate docker image tags
+ id: metadata
+ uses: docker/metadata-action@v5
+ with:
+ flavor: |
+ # Disable latest tag
+ latest=false
+ images: |
+ name=ghcr.io/${{ github.repository_owner }}/${{env.image}}
+ name=altran1502/${{env.image}},enable=${{ github.event_name == 'release' }}
+ tags: |
+ # Tag with branch name
+ type=ref,event=branch,suffix=${{ matrix.suffix }}
+ # Tag with pr-number
+ type=ref,event=pr,suffix=${{ matrix.suffix }}
+ # Tag with git tag on release
+ type=ref,event=tag,suffix=${{ matrix.suffix }}
+ type=raw,value=release,enable=${{ github.event_name == 'release' }},suffix=${{ matrix.suffix }}
+
+ - name: Determine build cache output
+ id: cache-target
+ run: |
+ if [[ "${{ github.event_name }}" == "pull_request" ]]; then
+ # Essentially just ignore the cache output (PR can't write to registry cache)
+ echo "cache-to=type=local,dest=/tmp/discard,ignore-error=true" >> $GITHUB_OUTPUT
+ else
+ echo "cache-to=type=registry,mode=max,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{ env.image }}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Build and push image
+ uses: docker/build-push-action@v6.7.0
+ with:
+ context: ${{ env.context }}
+ file: ${{ env.file }}
+ platforms: ${{ matrix.platforms }}
+ # Skip pushing when PR from a fork
+ push: ${{ !github.event.pull_request.head.repo.fork }}
+ cache-from: type=registry,ref=ghcr.io/${{ github.repository_owner }}/immich-build-cache:${{env.image}}
+ cache-to: ${{ steps.cache-target.outputs.cache-to }}
+ tags: ${{ steps.metadata.outputs.tags }}
+ labels: ${{ steps.metadata.outputs.labels }}
+ build-args: |
+ DEVICE=${{ matrix.device }}
+ BUILD_ID=${{ github.run_id }}
+ BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
+ BUILD_SOURCE_REF=${{ github.ref_name }}
+ BUILD_SOURCE_COMMIT=${{ github.sha }}
+
+ success-check-server:
+ name: Docker Build & Push Server Success
+ needs: [build_and_push_server, retag_server]
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Any jobs failed?
+ if: ${{ contains(needs.*.result, 'failure') }}
+ run: exit 1
+ - name: All jobs passed or skipped
+ if: ${{ !(contains(needs.*.result, 'failure')) }}
+ run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
+
+ success-check-ml:
+ name: Docker Build & Push ML Success
+ needs: [build_and_push_ml, retag_ml]
+ runs-on: ubuntu-latest
+ if: always()
+ steps:
+ - name: Any jobs failed?
+ if: ${{ contains(needs.*.result, 'failure') }}
+ run: exit 1
+ - name: All jobs passed or skipped
+ if: ${{ !(contains(needs.*.result, 'failure')) }}
+ run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml
index 32e9dc399a9f4..387d8e042496b 100644
--- a/.github/workflows/docs-build.yml
+++ b/.github/workflows/docs-build.yml
@@ -2,12 +2,8 @@ name: Docs build
on:
push:
branches: [main]
- paths:
- - "docs/**"
pull_request:
branches: [main]
- paths:
- - "docs/**"
release:
types: [published]
@@ -16,7 +12,27 @@ concurrency:
cancel-in-progress: true
jobs:
+ pre-job:
+ runs-on: ubuntu-latest
+ outputs:
+ should_run: ${{ steps.found_paths.outputs.docs == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - id: found_paths
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ docs:
+ - 'docs/**'
+ - name: Check if we should force jobs to run
+ id: should_force
+ run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
+
build:
+ name: Docs Build
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 62f213eb2aa4a..ab197fa459d94 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -7,13 +7,32 @@ on:
jobs:
checks:
+ name: Docs Deploy Checks
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
+ artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- - if: ${{ github.event.workflow_run.conclusion == 'failure' }}
- run: echo 'The triggering workflow failed' && exit 1
-
+ - if: ${{ github.event.workflow_run.conclusion != 'success' }}
+ run: echo 'The triggering workflow did not succeed' && exit 1
+ - name: Get artifact
+ id: get-artifact
+ uses: actions/github-script@v7
+ with:
+ script: |
+ let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ run_id: context.payload.workflow_run.id,
+ });
+ let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
+ return artifact.name == "docs-build-output"
+ })[0];
+ if (!matchArtifact) {
+ console.log("No artifact found with the name docs-build-output, build job was skipped")
+ return { found: false };
+ }
+ return { found: true, id: matchArtifact.id };
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@v7
@@ -73,9 +92,10 @@ jobs:
return parameters;
deploy:
+ name: Docs Deploy
runs-on: ubuntu-latest
needs: checks
- if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
+ if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -98,18 +118,11 @@ jobs:
uses: actions/github-script@v7
with:
script: |
- let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
- owner: context.repo.owner,
- repo: context.repo.repo,
- run_id: context.payload.workflow_run.id,
- });
- let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
- return artifact.name == "docs-build-output"
- })[0];
+ let artifact = ${{ needs.checks.outputs.artifact }};
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
- artifact_id: matchArtifact.id,
+ artifact_id: artifact.id,
archive_format: 'zip',
});
let fs = require('fs');
diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml
index 861a6319fe95d..80700569245d1 100644
--- a/.github/workflows/docs-destroy.yml
+++ b/.github/workflows/docs-destroy.yml
@@ -5,6 +5,7 @@ on:
jobs:
deploy:
+ name: Docs Destroy
runs-on: ubuntu-latest
steps:
- name: Checkout code
diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml
index 510995aa549ef..1557b3d15cfba 100644
--- a/.github/workflows/pr-label-validation.yml
+++ b/.github/workflows/pr-label-validation.yml
@@ -1,12 +1,15 @@
name: PR Label Validation
on:
- pull_request:
+ pull_request_target:
types: [opened, labeled, unlabeled, synchronize]
jobs:
validate-release-label:
runs-on: ubuntu-latest
+ permissions:
+ issues: write
+ pull-requests: read
steps:
- name: Require PR to have a changelog label
uses: mheap/github-action-required-labels@v5
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 9d50f6f8f9100..fc03b24d085b7 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -29,10 +29,17 @@ jobs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
steps:
+ - name: Generate a token
+ id: generate-token
+ uses: actions/create-github-app-token@v1
+ with:
+ app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+
- name: Checkout
uses: actions/checkout@v4
with:
- token: ${{ secrets.ORG_RELEASE_TOKEN }}
+ token: ${{ steps.generate-token.outputs.token }}
- name: Install Poetry
run: pipx install poetry
@@ -44,10 +51,8 @@ jobs:
id: push-tag
uses: EndBug/add-and-commit@v9
with:
- author_name: Alex The Bot
- author_email: alex.tran1502@gmail.com
- default_author: user_info
- message: 'Version ${{ env.IMMICH_VERSION }}'
+ default_author: github_actions
+ message: 'chore: version ${{ env.IMMICH_VERSION }}'
tag: ${{ env.IMMICH_VERSION }}
push: true
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 27392a12bd389..94567c1cd567e 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -10,8 +10,27 @@ concurrency:
cancel-in-progress: true
jobs:
+ pre-job:
+ runs-on: ubuntu-latest
+ outputs:
+ should_run: ${{ steps.found_paths.outputs.mobile == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - id: found_paths
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ mobile:
+ - 'mobile/**'
+ - name: Check if we should force jobs to run
+ id: should_force
+ run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
+
mobile-dart-analyze:
name: Run Dart Code Analysis
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run == 'true' }}
runs-on: ubuntu-latest
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 34a9d984a0215..24e3e086235f0 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -10,8 +10,47 @@ concurrency:
cancel-in-progress: true
jobs:
+ pre-job:
+ runs-on: ubuntu-latest
+ outputs:
+ 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_cli: ${{ steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ should_run_e2e: ${{ steps.found_paths.outputs.e2e == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ should_run_mobile: ${{ steps.found_paths.outputs.mobile == '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' }}
+ should_run_e2e_web: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.web == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ should_run_e2e_server_cli: ${{ steps.found_paths.outputs.e2e == 'true' || steps.found_paths.outputs.server == 'true' || steps.found_paths.outputs.cli == 'true' || steps.should_force.outputs.should_force == 'true' }}
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ - id: found_paths
+ uses: dorny/paths-filter@v3
+ with:
+ filters: |
+ web:
+ - 'web/**'
+ - 'open-api/typescript-sdk/**'
+ server:
+ - 'server/**'
+ cli:
+ - 'cli/**'
+ - 'open-api/typescript-sdk/**'
+ e2e:
+ - 'e2e/**'
+ mobile:
+ - 'mobile/**'
+ machine-learning:
+ - 'machine-learning/**'
+
+ - name: Check if we should force jobs to run
+ id: should_force
+ run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
+
server-unit-tests:
- name: Server
+ name: Test & Lint Server
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -46,7 +85,9 @@ jobs:
if: ${{ !cancelled() }}
cli-unit-tests:
- name: CLI
+ name: Unit Test CLI
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -85,7 +126,9 @@ jobs:
if: ${{ !cancelled() }}
cli-unit-tests-win:
- name: CLI (Windows)
+ name: Unit Test CLI (Windows)
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
runs-on: windows-latest
defaults:
run:
@@ -117,7 +160,9 @@ jobs:
if: ${{ !cancelled() }}
web-unit-tests:
- name: Web
+ name: Test & Lint Web
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
@@ -159,13 +204,54 @@ jobs:
run: npm run test:cov
if: ${{ !cancelled() }}
- e2e-tests:
- name: End-to-End Tests
+ e2e-tests-lint:
+ name: End-to-End Lint
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_e2e == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./e2e
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: './e2e/.nvmrc'
+
+ - name: Run setup typescript-sdk
+ run: npm ci && npm run build
+ working-directory: ./open-api/typescript-sdk
+ if: ${{ !cancelled() }}
+
+ - name: Install dependencies
+ run: npm ci
+ if: ${{ !cancelled() }}
+
+ - name: Run linter
+ run: npm run lint
+ if: ${{ !cancelled() }}
+
+ - name: Run formatter
+ run: npm run format
+ if: ${{ !cancelled() }}
+
+ - name: Run tsc
+ run: npm run check
+ if: ${{ !cancelled() }}
+
+ e2e-tests-server-cli:
+ name: End-to-End Tests (Server & CLI)
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_e2e_server_cli == 'true' }}
+ runs-on: mich
+ defaults:
+ run:
+ working-directory: ./e2e
+
steps:
- name: Checkout code
uses: actions/checkout@v4
@@ -191,16 +277,41 @@ jobs:
run: npm ci
if: ${{ !cancelled() }}
- - name: Run linter
- run: npm run lint
+ - name: Docker build
+ run: docker compose build
if: ${{ !cancelled() }}
- - name: Run formatter
- run: npm run format
+ - name: Run e2e tests (api & cli)
+ run: npm run test
if: ${{ !cancelled() }}
- - name: Run tsc
- run: npm run check
+ e2e-tests-web:
+ name: End-to-End Tests (Web)
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_e2e_web == 'true' }}
+ runs-on: mich
+ defaults:
+ run:
+ working-directory: ./e2e
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+ with:
+ submodules: 'recursive'
+
+ - name: Setup Node
+ uses: actions/setup-node@v4
+ with:
+ node-version-file: './e2e/.nvmrc'
+
+ - name: Run setup typescript-sdk
+ run: npm ci && npm run build
+ working-directory: ./open-api/typescript-sdk
+ if: ${{ !cancelled() }}
+
+ - name: Install dependencies
+ run: npm ci
if: ${{ !cancelled() }}
- name: Install Playwright Browsers
@@ -211,16 +322,14 @@ jobs:
run: docker compose build
if: ${{ !cancelled() }}
- - name: Run e2e tests (api & cli)
- run: npm run test
- if: ${{ !cancelled() }}
-
- name: Run e2e tests (web)
run: npx playwright test
if: ${{ !cancelled() }}
mobile-unit-tests:
- name: Mobile
+ name: Unit Test Mobile
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -234,7 +343,9 @@ jobs:
run: flutter test -j 1
ml-unit-tests:
- name: Machine Learning
+ name: Unit Test ML
+ needs: pre-job
+ if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
runs-on: ubuntu-latest
defaults:
run:
diff --git a/cli/.nvmrc b/cli/.nvmrc
index 8ce7030825b5e..3516580bbbc04 100644
--- a/cli/.nvmrc
+++ b/cli/.nvmrc
@@ -1 +1 @@
-20.16.0
+20.17.0
diff --git a/cli/Dockerfile b/cli/Dockerfile
index 2c4aaf87186c0..e3cce6d448249 100644
--- a/cli/Dockerfile
+++ b/cli/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 AS core
+FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
diff --git a/cli/README.md b/cli/README.md
index a570a55239af1..8fa2ace483251 100644
--- a/cli/README.md
+++ b/cli/README.md
@@ -4,8 +4,18 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma
# For developers
+Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder:
+
+ $ npm install
+ $ npm run build
+
+Then, to build the open-api client run the following in the open-api folder:
+
+ $ ./bin/generate-open-api.sh
+
To run the Immich CLI from source, run the following in the cli folder:
+ $ npm install
$ npm run build
$ ts-node .
@@ -17,3 +27,4 @@ You can also build and install the CLI using
$ npm run build
$ npm install -g .
+****
diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs
index 3f724506a3c8e..9115a1feb79e5 100644
--- a/cli/eslint.config.mjs
+++ b/cli/eslint.config.mjs
@@ -55,6 +55,7 @@ export default [
'unicorn/import-style': 'off',
curly: 2,
'prettier/prettier': 0,
+ 'object-shorthand': ['error', 'always'],
},
},
];
diff --git a/cli/package-lock.json b/cli/package-lock.json
index 6044069672878..f443c141b9e06 100644
--- a/cli/package-lock.json
+++ b/cli/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@immich/cli",
- "version": "2.2.15",
+ "version": "2.2.18",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@immich/cli",
- "version": "2.2.15",
+ "version": "2.2.18",
"license": "GNU Affero General Public License version 3",
"dependencies": {
"fast-glob": "^3.3.2",
@@ -24,7 +24,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -52,14 +52,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.112.1",
+ "version": "1.114.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"typescript": "^5.3.3"
}
},
@@ -727,9 +727,9 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.17.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz",
- "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==",
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+ "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -825,9 +825,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.8.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz",
- "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==",
+ "version": "9.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
+ "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1054,169 +1054,224 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
- "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz",
+ "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
- "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz",
+ "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
- "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz",
+ "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
- "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz",
+ "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
- "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz",
+ "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==",
"cpu": [
"arm"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz",
+ "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
- "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz",
+ "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
- "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz",
+ "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz",
+ "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
- "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz",
+ "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==",
"cpu": [
"riscv64"
],
"dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz",
+ "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
- "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz",
+ "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
- "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz",
+ "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
- "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz",
+ "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==",
"cpu": [
"arm64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
- "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz",
+ "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==",
"cpu": [
"ia32"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
- "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz",
+ "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==",
"cpu": [
"x64"
],
"dev": true,
+ "license": "MIT",
"optional": true,
"os": [
"win32"
@@ -1269,9 +1324,9 @@
}
},
"node_modules/@types/node": {
- "version": "20.16.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz",
- "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==",
+ "version": "20.16.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz",
+ "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1285,17 +1340,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz",
- "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
+ "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/type-utils": "8.0.1",
- "@typescript-eslint/utils": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/type-utils": "8.3.0",
+ "@typescript-eslint/utils": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1319,16 +1374,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz",
- "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
+ "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/typescript-estree": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1348,14 +1403,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz",
- "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
+ "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1"
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1366,14 +1421,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz",
- "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
+ "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.0.1",
- "@typescript-eslint/utils": "8.0.1",
+ "@typescript-eslint/typescript-estree": "8.3.0",
+ "@typescript-eslint/utils": "8.3.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1391,9 +1446,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz",
- "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
+ "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1405,16 +1460,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz",
- "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
+ "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4",
- "globby": "^11.1.0",
+ "fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
@@ -1434,16 +1489,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz",
- "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
+ "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/typescript-estree": "8.0.1"
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1457,13 +1512,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz",
- "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
+ "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
+ "@typescript-eslint/types": "8.3.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1650,16 +1705,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1979,19 +2024,6 @@
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true
},
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2080,17 +2112,17 @@
}
},
"node_modules/eslint": {
- "version": "9.8.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz",
- "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==",
+ "version": "9.9.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
+ "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
- "@eslint/config-array": "^0.17.1",
+ "@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.8.0",
+ "@eslint/js": "9.9.1",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
@@ -2129,6 +2161,14 @@
},
"funding": {
"url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
}
},
"node_modules/eslint-config-prettier": {
@@ -2603,27 +2643,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/globrex": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz",
@@ -3374,16 +3393,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -3709,10 +3718,11 @@
}
},
"node_modules/rollup": {
- "version": "4.13.0",
- "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
- "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
+ "version": "4.21.1",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz",
+ "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/estree": "1.0.5"
},
@@ -3724,19 +3734,22 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
- "@rollup/rollup-android-arm-eabi": "4.13.0",
- "@rollup/rollup-android-arm64": "4.13.0",
- "@rollup/rollup-darwin-arm64": "4.13.0",
- "@rollup/rollup-darwin-x64": "4.13.0",
- "@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
- "@rollup/rollup-linux-arm64-gnu": "4.13.0",
- "@rollup/rollup-linux-arm64-musl": "4.13.0",
- "@rollup/rollup-linux-riscv64-gnu": "4.13.0",
- "@rollup/rollup-linux-x64-gnu": "4.13.0",
- "@rollup/rollup-linux-x64-musl": "4.13.0",
- "@rollup/rollup-win32-arm64-msvc": "4.13.0",
- "@rollup/rollup-win32-ia32-msvc": "4.13.0",
- "@rollup/rollup-win32-x64-msvc": "4.13.0",
+ "@rollup/rollup-android-arm-eabi": "4.21.1",
+ "@rollup/rollup-android-arm64": "4.21.1",
+ "@rollup/rollup-darwin-arm64": "4.21.1",
+ "@rollup/rollup-darwin-x64": "4.21.1",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.21.1",
+ "@rollup/rollup-linux-arm-musleabihf": "4.21.1",
+ "@rollup/rollup-linux-arm64-gnu": "4.21.1",
+ "@rollup/rollup-linux-arm64-musl": "4.21.1",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1",
+ "@rollup/rollup-linux-riscv64-gnu": "4.21.1",
+ "@rollup/rollup-linux-s390x-gnu": "4.21.1",
+ "@rollup/rollup-linux-x64-gnu": "4.21.1",
+ "@rollup/rollup-linux-x64-musl": "4.21.1",
+ "@rollup/rollup-win32-arm64-msvc": "4.21.1",
+ "@rollup/rollup-win32-ia32-msvc": "4.21.1",
+ "@rollup/rollup-win32-x64-msvc": "4.21.1",
"fsevents": "~2.3.2"
}
},
@@ -3813,16 +3826,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/source-map-js": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
@@ -4207,15 +4210,15 @@
}
},
"node_modules/vite": {
- "version": "5.4.0",
- "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
- "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
+ "version": "5.4.2",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
+ "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
- "postcss": "^8.4.40",
- "rollup": "^4.13.0"
+ "postcss": "^8.4.41",
+ "rollup": "^4.20.0"
},
"bin": {
"vite": "bin/vite.js"
diff --git a/cli/package.json b/cli/package.json
index ddd67308873d3..0d560c8456585 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -1,6 +1,6 @@
{
"name": "@immich/cli",
- "version": "2.2.15",
+ "version": "2.2.18",
"description": "Command Line Interface (CLI) for Immich",
"type": "module",
"exports": "./dist/index.js",
@@ -20,7 +20,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -67,6 +67,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
- "node": "20.16.0"
+ "node": "20.17.0"
}
}
diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
index 4774e1cacfe40..096177bb05366 100644
--- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
- version = "4.38.0"
- constraints = "4.38.0"
+ version = "4.40.0"
+ constraints = "4.40.0"
hashes = [
- "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
- "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
- "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
- "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
- "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
- "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
- "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
- "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
- "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
- "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
- "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
- "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
- "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
- "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
- "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
- "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
- "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
- "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
- "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
- "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
- "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
+ "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=",
+ "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=",
+ "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=",
+ "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=",
+ "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=",
+ "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=",
+ "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=",
+ "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=",
+ "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=",
+ "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=",
+ "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=",
+ "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=",
+ "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=",
+ "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=",
+ "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd",
+ "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f",
+ "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396",
+ "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb",
+ "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c",
+ "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2",
+ "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce",
+ "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
- "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
- "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
- "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
- "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
- "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
- "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
- "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
+ "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f",
+ "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d",
+ "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62",
+ "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e",
+ "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6",
+ "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e",
]
}
diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf
index b7c70f1c21719..63c96fc49805b 100644
--- a/deployment/modules/cloudflare/docs-release/config.tf
+++ b/deployment/modules/cloudflare/docs-release/config.tf
@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
- version = "4.38.0"
+ version = "4.40.0"
}
}
}
diff --git a/deployment/modules/cloudflare/docs-release/domain.tf b/deployment/modules/cloudflare/docs-release/domain.tf
index a8e93b8dd5c6b..0602045f71d2b 100644
--- a/deployment/modules/cloudflare/docs-release/domain.tf
+++ b/deployment/modules/cloudflare/docs-release/domain.tf
@@ -9,6 +9,6 @@ resource "cloudflare_record" "immich_app_release_domain" {
proxied = true
ttl = 1
type = "CNAME"
- value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
+ content = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
index 4774e1cacfe40..096177bb05366 100644
--- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl
+++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl
@@ -2,37 +2,37 @@
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
- version = "4.38.0"
- constraints = "4.38.0"
+ version = "4.40.0"
+ constraints = "4.40.0"
hashes = [
- "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=",
- "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=",
- "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=",
- "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=",
- "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=",
- "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=",
- "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=",
- "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=",
- "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=",
- "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=",
- "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=",
- "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=",
- "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=",
- "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=",
- "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071",
- "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979",
- "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567",
- "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965",
- "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a",
- "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607",
- "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df",
+ "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=",
+ "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=",
+ "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=",
+ "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=",
+ "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=",
+ "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=",
+ "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=",
+ "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=",
+ "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=",
+ "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=",
+ "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=",
+ "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=",
+ "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=",
+ "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=",
+ "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd",
+ "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f",
+ "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396",
+ "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb",
+ "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c",
+ "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2",
+ "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce",
+ "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
- "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805",
- "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988",
- "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba",
- "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d",
- "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02",
- "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf",
- "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722",
+ "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f",
+ "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d",
+ "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62",
+ "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e",
+ "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6",
+ "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e",
]
}
diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf
index b7c70f1c21719..63c96fc49805b 100644
--- a/deployment/modules/cloudflare/docs/config.tf
+++ b/deployment/modules/cloudflare/docs/config.tf
@@ -5,7 +5,7 @@ terraform {
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
- version = "4.38.0"
+ version = "4.40.0"
}
}
}
diff --git a/deployment/modules/cloudflare/docs/domain.tf b/deployment/modules/cloudflare/docs/domain.tf
index 6d00f26b604c6..80997c2e87176 100644
--- a/deployment/modules/cloudflare/docs/domain.tf
+++ b/deployment/modules/cloudflare/docs/domain.tf
@@ -9,7 +9,7 @@ resource "cloudflare_record" "immich_app_branch_subdomain" {
proxied = true
ttl = 1
type = "CNAME"
- value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
+ content = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml
index 2fec915a42c1f..509674f328b35 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -79,7 +79,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
- image: prom/prometheus@sha256:cafe963e591c872d38f3ea41ff8eb22cee97917b7c97b5c0ccd43a419f11f613
+ image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -91,7 +91,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
- image: grafana/grafana:11.1.4-ubuntu@sha256:8e74fb7eed4d59fb5595acd0576c21411167f6b6401426ae29f2e8f9f71b68f6
+ image: grafana/grafana:11.2.0-ubuntu@sha256:8e2c13739563c3da9d45de96c6bcb63ba617cac8c571c060112c7fc8ad6914e9
volumes:
- grafana-data:/var/lib/grafana
diff --git a/docs/.nvmrc b/docs/.nvmrc
index 8ce7030825b5e..3516580bbbc04 100644
--- a/docs/.nvmrc
+++ b/docs/.nvmrc
@@ -1 +1 @@
-20.16.0
+20.17.0
diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx
index 117ca74c037ca..b1a24e1788a2f 100644
--- a/docs/docs/FAQ.mdx
+++ b/docs/docs/FAQ.mdx
@@ -52,14 +52,25 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca
- Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich.
- Use the Immich app more often.
+### Why are features not working with a self-signed cert or mTLS?
+
+Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background).
+We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/).
+
---
## Assets
### Does Immich change the file?
-No, Immich does not touch the original file under any circumstances,
-all edited metadata are saved in the companion sidecar file and the database.
+No, Immich does not modify the original files.
+All edited metadata is saved in companion `.xmp` sidecar files and the database.
+However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI.
+
+### Why do my file names appear as a random string in the file manager?
+
+When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job.
+It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation.
### Can I add my existing photo library?
@@ -157,6 +168,19 @@ We haven't implemented an official mechanism for creating albums from external l
Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries.
+### Why are my edits to files not being saved in read-only external libraries?
+
+Images in read-write external libraries (the default) can be edited as normal.
+In read-only libraries (`:ro` in the `docker-compose.yml`), Immich is unable to create the `.xmp` sidecar files to store edited file metadata.
+For this reason, the metadata (timestamp, location, description, star rating, etc.) cannot be edited for files in read-only external libraries.
+
+### How are deletions of files handled in external libraries?
+
+Immich will attempt to delete original files that have been trashed when the trash is emptied.
+In read-write external libraries (the default), Immich will delete the original file.
+In read-only libraries (`:ro` in the `docker-compose.yml`), files can still be trashed in the UI.
+However, when the trash is emptied, the files will re-appear in the main timeline since Immich is unable to delete the original file.
+
---
## Machine Learning
diff --git a/docs/docs/administration/img/admin-jobs.png b/docs/docs/administration/img/admin-jobs.png
deleted file mode 100644
index 096bce4354f0f..0000000000000
Binary files a/docs/docs/administration/img/admin-jobs.png and /dev/null differ
diff --git a/docs/docs/administration/img/admin-jobs.webp b/docs/docs/administration/img/admin-jobs.webp
new file mode 100644
index 0000000000000..15580c3c9cc8c
Binary files /dev/null and b/docs/docs/administration/img/admin-jobs.webp differ
diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md
index ff74ea4673f7c..fb5ca7c059165 100644
--- a/docs/docs/administration/jobs-workers.md
+++ b/docs/docs/administration/jobs-workers.md
@@ -52,4 +52,4 @@ Additionally, some jobs run on a schedule, which is every night at midnight. Thi
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
-
+
diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md
index ab317787bc09c..12cd7502a5857 100644
--- a/docs/docs/administration/oauth.md
+++ b/docs/docs/administration/oauth.md
@@ -3,7 +3,7 @@
This page contains details about using OAuth in Immich.
:::tip
-Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution.
+Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution.
:::
## Overview
@@ -30,7 +30,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured
The **Sign-in redirect URIs** should include:
- - `app.immich:/` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
+ - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx)
- `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client
- `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client
@@ -38,7 +38,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured
Mobile
- - `app.immich:/` (You **MUST** include this for iOS and Android mobile apps to work properly)
+ - `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly)
Localhost
@@ -96,16 +96,16 @@ When Auto Launch is enabled, the login page will automatically redirect the user
## Mobile Redirect URI
-The redirect URI for the mobile app is `app.immich:/`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following:
+The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following:
-1. Configure an http(s) endpoint to forwards requests to `app.immich:/`
+1. Configure an http(s) endpoint to forwards requests to `app.immich:///oauth-callback`
2. Whitelist the new endpoint as a valid redirect URI with your provider.
3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings.
With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI.
:::info
-Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:/`, and can be used for step 1.
+Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1.
:::
## Example Configuration
@@ -154,21 +154,21 @@ Configuration of Authorised redirect URIs (Google Console)
Configuration of OAuth in Immich System Settings
-| Setting | Value |
-| ---------------------------- | ------------------------------------------------------------------------------------------------------ |
-| Issuer URL | [https://accounts.google.com](https://accounts.google.com) |
-| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
-| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
-| Scope | openid email profile |
-| Signing Algorithm | RS256 |
-| Storage Label Claim | preferred_username |
-| Storage Quota Claim | immich_quota |
-| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
-| Button Text | Sign in with Google (optional) |
-| Auto Register | Enabled (optional) |
-| Auto Launch | Enabled |
-| Mobile Redirect URI Override | Enabled (required) |
-| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) |
+| Setting | Value |
+| ---------------------------- | ---------------------------------------------------------------------------- |
+| Issuer URL | `https://accounts.google.com` |
+| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com |
+| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO |
+| Scope | openid email profile |
+| Signing Algorithm | RS256 |
+| Storage Label Claim | preferred_username |
+| Storage Quota Claim | immich_quota |
+| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
+| Button Text | Sign in with Google (optional) |
+| Auto Register | Enabled (optional) |
+| Auto Launch | Enabled |
+| Mobile Redirect URI Override | Enabled (required) |
+| Mobile Redirect URI | `https://example.immich.app/api/oauth/mobile-redirect` |
diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md
index f92bab4938921..9f35ed1010e2f 100644
--- a/docs/docs/administration/system-settings.md
+++ b/docs/docs/administration/system-settings.md
@@ -104,7 +104,7 @@ You can choose to disable a certain type of machine learning, for example smart
### Smart Search
-The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the
+The [smart search](/docs/features/smart-search) settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the
Smart Search job on all images to fully apply the change.
:::info Internet connection
@@ -113,15 +113,23 @@ After downloading, there is no need for Immich to connect to the network
Unless version checking has been enabled in the settings.
:::
+### Duplicate Detection
+
+Use CLIP embeddings to find likely duplicates. The maximum detection distance can be configured in order to improve / reduce the level of accuracy.
+
+- **Maximum detection distance -** Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives.
+
### Facial Recognition
Under these settings, you can change the facial recognition settings
Editable settings:
-- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model.
-- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives.
-- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible.
-- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person.
+- **Facial Recognition Model**
+- **Min Detection Score**
+- **Max Recognition Distance**
+- **Min Recognized Faces**
+
+You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works)
:::info
When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces.
diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md
index 94cbff6ebe393..cdea1a11a51e1 100644
--- a/docs/docs/features/libraries.md
+++ b/docs/docs/features/libraries.md
@@ -104,8 +104,8 @@ The `immich-server` container will need access to the gallery. Modify your docke
immich-server:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
-+ - /mnt/nas/christmas-trip:/mnt/nas/christmas-trip:ro
-+ - /home/user/old-pics:/home/user/old-pics:ro
++ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
++ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - /mnt/media/videos2:/mnt/media/videos2 # the files in this folder can be deleted, as it does not end with :ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
diff --git a/docs/docs/features/shared-albums.md b/docs/docs/features/shared-albums.md
index 2684acfd9c5be..dcf884bc9bbe5 100644
--- a/docs/docs/features/shared-albums.md
+++ b/docs/docs/features/shared-albums.md
@@ -16,7 +16,7 @@ When sharing shared albums, whats shared is:
- Download all assets as zip file (Web only).
:::info Archive size limited.
- If the size of the album exceeds 4GB, the archive files will be divided into 4GB each.
+ If the size of the album exceeds 4GB, the archive files will by default be divided into 4GB each. This can be changed on the user settings page.
:::
- Add a description to the album (Web only).
- Slideshow view (Web only).
@@ -152,7 +152,7 @@ Some of the features are not available on mobile, to understand what the full fe
## Sharing Between Users
-#### Add or remove users from the album.
+#### Add or remove users from the album
:::info remove user(s)
When a user is removed from the album, the photos he uploaded will still appear in the album.
diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md
index 20b841f4027dc..2b4f27cfceaa5 100644
--- a/docs/docs/guides/database-queries.md
+++ b/docs/docs/guides/database-queries.md
@@ -23,7 +23,7 @@ SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files wi
```
```sql title="Find by path"
-SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_20230903_232542848.jpg';
+SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg';
SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%';
```
@@ -37,6 +37,12 @@ SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e
SELECT * FROM "assets" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation
```
+```sql title="Find duplicate assets with identical checksum (SHA-1) (excluding trashed files)"
+SELECT T1."checksum", array_agg(T2."id") ids FROM "assets" T1
+ INNER JOIN "assets" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL
+ WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum";
+```
+
```sql title="Live photos"
SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL;
```
@@ -79,8 +85,7 @@ SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type";
```sql title="Count by type (per user)"
SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets"
JOIN "users" ON "assets"."ownerId" = "users"."id"
- GROUP BY "assets"."type", "users"."email"
- ORDER BY "users"."email";
+ GROUP BY "assets"."type", "users"."email" ORDER BY "users"."email";
```
```sql title="Failed file movements"
diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md
index cd8bf66f14cc9..1ea068c3a0a79 100644
--- a/docs/docs/guides/remote-access.md
+++ b/docs/docs/guides/remote-access.md
@@ -11,13 +11,13 @@ Never forward port 2283 directly to the internet without additional configuratio
You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/)
-### Pros:
+### Pros
- Simple to set up and very secure.
- Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk.
- Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal.
-### Cons:
+### Cons
- If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider.
- VPN software needs to be installed and active on both server-side and client-side.
@@ -27,6 +27,10 @@ You may use a VPN service to open an encrypted connection to your Immich instanc
If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation).
+:::tip Video toturial
+You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created.
+:::
+
### Pros
- Minimal configuration needed on server and client sides.
@@ -44,7 +48,7 @@ A reverse proxy is a service that sits between web servers and clients. A revers
If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/docs/administration/reverse-proxy.md).
-You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
+You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser.
A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder.
diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md
index e4186e1697db3..4dbb72a408f16 100644
--- a/docs/docs/guides/remote-machine-learning.md
+++ b/docs/docs/guides/remote-machine-learning.md
@@ -11,6 +11,10 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server.
:::
+:::danger
+When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud.
+:::
+
```yaml
name: immich_remote_ml
diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md
index 9777d002627a7..03c1a7a02b333 100644
--- a/docs/docs/guides/template-backup-script.md
+++ b/docs/docs/guides/template-backup-script.md
@@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
cd /tmp/immich-mountpoint
```
-You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
+You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx
index 0b69bd8639838..9ef63523a05ec 100644
--- a/docs/docs/install/docker-compose.mdx
+++ b/docs/docs/install/docker-compose.mdx
@@ -109,7 +109,7 @@ Immich is currently under heavy development, which means you can expect [breakin
[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
[watchtower]: https://containrrr.dev/watchtower/
-[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created
+[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created
[container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry
[releases]: https://github.com/immich-app/immich/releases
[docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md
index 78cd16cf1b7c2..a0cf71e044724 100644
--- a/docs/docs/install/environment-variables.md
+++ b/docs/docs/install/environment-variables.md
@@ -125,7 +125,7 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW
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 info can be found in the upstream [ioredis][redis-api] documentation.
+More info 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.
:::
@@ -159,26 +159,29 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
-| Variable | Description | Default | Containers |
-| :----------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
-| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
-| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
-| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
-| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
-| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
-| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
-| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning |
-| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
-| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
-| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
-| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
-| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
-| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
+| Variable | Description | Default | Containers |
+| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- |
+| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
+| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
+| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning |
+| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning |
+| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning |
+| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning |
+| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning |
+| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning |
+| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning |
+| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning |
+| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning |
+| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning |
+| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning |
+| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning |
\*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones.
\*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around.
+\*3: For scenarios like HPA in K8S. https://github.com/immich-app/immich/discussions/12064
+
:::info
Other machine learning parameters can be tuned from the admin UI.
@@ -223,4 +226,4 @@ to use use a Docker secret for the password in the Redis container.
[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]: https://docs.docker.com/engine/swarm/secrets/
-[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
+[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis
diff --git a/docs/docs/partials/_storage-template.md b/docs/docs/partials/_storage-template.md
index f48419c1ef7a6..b6dcd5ad7759c 100644
--- a/docs/docs/partials/_storage-template.md
+++ b/docs/docs/partials/_storage-template.md
@@ -27,3 +27,9 @@ If an asset is in multiple albums, `{{album}}` will be set to the name of the al
:::
Immich also provides a mechanism to migrate between templates so that if the template you set now doesn't work in the future, you can always migrate all the existing files to the new template. The mechanism is run as a job on the Job page.
+
+If you want to store assets in album folders, but you also have assets that do not belong to any album, you can use `{{#if album}}`, `{{else}}` and `{{/if}}` to create a conditional statement. For example, the following template will store assets in album folders if they belong to an album, and in a folder named "Other/Month" if they do not belong to an album:
+
+```
+{{y}}/{{#if album}}{{album}}{{else}}Other/{{MM}}{{/if}}/{{filename}}
+```
diff --git a/docs/package-lock.json b/docs/package-lock.json
index e5fb9f8b2aae7..05417ce1275a7 100644
--- a/docs/package-lock.json
+++ b/docs/package-lock.json
@@ -2155,9 +2155,9 @@
}
},
"node_modules/@docusaurus/core": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz",
- "integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz",
+ "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==",
"license": "MIT",
"dependencies": {
"@babel/core": "^7.23.3",
@@ -2170,12 +2170,12 @@
"@babel/runtime": "^7.22.6",
"@babel/runtime-corejs3": "^7.22.6",
"@babel/traverse": "^7.22.8",
- "@docusaurus/cssnano-preset": "3.4.0",
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/cssnano-preset": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"autoprefixer": "^10.4.14",
"babel-loader": "^9.1.3",
"babel-plugin-dynamic-import-node": "^2.3.3",
@@ -2236,14 +2236,15 @@
"node": ">=18.0"
},
"peerDependencies": {
+ "@mdx-js/react": "^3.0.0",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/cssnano-preset": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz",
- "integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz",
+ "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==",
"license": "MIT",
"dependencies": {
"cssnano-preset-advanced": "^6.1.2",
@@ -2256,9 +2257,9 @@
}
},
"node_modules/@docusaurus/logger": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz",
- "integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz",
+ "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==",
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
@@ -2269,14 +2270,14 @@
}
},
"node_modules/@docusaurus/mdx-loader": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz",
- "integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz",
+ "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"@mdx-js/mdx": "^3.0.0",
"@slorber/remark-comment": "^1.0.0",
"escape-html": "^1.0.3",
@@ -2308,12 +2309,12 @@
}
},
"node_modules/@docusaurus/module-type-aliases": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz",
- "integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz",
+ "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==",
"license": "MIT",
"dependencies": {
- "@docusaurus/types": "3.4.0",
+ "@docusaurus/types": "3.5.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2326,52 +2327,21 @@
"react-dom": "*"
}
},
- "node_modules/@docusaurus/plugin-content-blog": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz",
- "integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==",
- "license": "MIT",
- "dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
- "cheerio": "^1.0.0-rc.12",
- "feed": "^4.2.2",
- "fs-extra": "^11.1.1",
- "lodash": "^4.17.21",
- "reading-time": "^1.5.0",
- "srcset": "^4.0.0",
- "tslib": "^2.6.0",
- "unist-util-visit": "^5.0.0",
- "utility-types": "^3.10.0",
- "webpack": "^5.88.1"
- },
- "engines": {
- "node": ">=18.0"
- },
- "peerDependencies": {
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- }
- },
"node_modules/@docusaurus/plugin-content-docs": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz",
- "integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz",
+ "integrity": "sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/module-type-aliases": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/module-type-aliases": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"@types/react-router-config": "^5.0.7",
"combine-promises": "^1.1.0",
"fs-extra": "^11.1.1",
@@ -2389,38 +2359,15 @@
"react-dom": "^18.0.0"
}
},
- "node_modules/@docusaurus/plugin-content-pages": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz",
- "integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==",
- "license": "MIT",
- "dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
- "fs-extra": "^11.1.1",
- "tslib": "^2.6.0",
- "webpack": "^5.88.1"
- },
- "engines": {
- "node": ">=18.0"
- },
- "peerDependencies": {
- "react": "^18.0.0",
- "react-dom": "^18.0.0"
- }
- },
"node_modules/@docusaurus/plugin-debug": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz",
- "integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz",
+ "integrity": "sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
"fs-extra": "^11.1.1",
"react-json-view-lite": "^1.2.0",
"tslib": "^2.6.0"
@@ -2434,14 +2381,14 @@
}
},
"node_modules/@docusaurus/plugin-google-analytics": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz",
- "integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz",
+ "integrity": "sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2453,14 +2400,14 @@
}
},
"node_modules/@docusaurus/plugin-google-gtag": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz",
- "integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz",
+ "integrity": "sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"@types/gtag.js": "^0.0.12",
"tslib": "^2.6.0"
},
@@ -2473,14 +2420,14 @@
}
},
"node_modules/@docusaurus/plugin-google-tag-manager": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz",
- "integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz",
+ "integrity": "sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"tslib": "^2.6.0"
},
"engines": {
@@ -2492,17 +2439,17 @@
}
},
"node_modules/@docusaurus/plugin-sitemap": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz",
- "integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz",
+ "integrity": "sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"fs-extra": "^11.1.1",
"sitemap": "^7.1.1",
"tslib": "^2.6.0"
@@ -2516,24 +2463,81 @@
}
},
"node_modules/@docusaurus/preset-classic": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz",
- "integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz",
+ "integrity": "sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/plugin-content-blog": "3.4.0",
- "@docusaurus/plugin-content-docs": "3.4.0",
- "@docusaurus/plugin-content-pages": "3.4.0",
- "@docusaurus/plugin-debug": "3.4.0",
- "@docusaurus/plugin-google-analytics": "3.4.0",
- "@docusaurus/plugin-google-gtag": "3.4.0",
- "@docusaurus/plugin-google-tag-manager": "3.4.0",
- "@docusaurus/plugin-sitemap": "3.4.0",
- "@docusaurus/theme-classic": "3.4.0",
- "@docusaurus/theme-common": "3.4.0",
- "@docusaurus/theme-search-algolia": "3.4.0",
- "@docusaurus/types": "3.4.0"
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/plugin-content-blog": "3.5.2",
+ "@docusaurus/plugin-content-docs": "3.5.2",
+ "@docusaurus/plugin-content-pages": "3.5.2",
+ "@docusaurus/plugin-debug": "3.5.2",
+ "@docusaurus/plugin-google-analytics": "3.5.2",
+ "@docusaurus/plugin-google-gtag": "3.5.2",
+ "@docusaurus/plugin-google-tag-manager": "3.5.2",
+ "@docusaurus/plugin-sitemap": "3.5.2",
+ "@docusaurus/theme-classic": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/theme-search-algolia": "3.5.2",
+ "@docusaurus/types": "3.5.2"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-blog": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz",
+ "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==",
+ "license": "MIT",
+ "dependencies": {
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
+ "cheerio": "1.0.0-rc.12",
+ "feed": "^4.2.2",
+ "fs-extra": "^11.1.1",
+ "lodash": "^4.17.21",
+ "reading-time": "^1.5.0",
+ "srcset": "^4.0.0",
+ "tslib": "^2.6.0",
+ "unist-util-visit": "^5.0.0",
+ "utility-types": "^3.10.0",
+ "webpack": "^5.88.1"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "@docusaurus/plugin-content-docs": "*",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-pages": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz",
+ "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
+ "fs-extra": "^11.1.1",
+ "tslib": "^2.6.0",
+ "webpack": "^5.88.1"
},
"engines": {
"node": ">=18.0"
@@ -2544,27 +2548,27 @@
}
},
"node_modules/@docusaurus/theme-classic": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz",
- "integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz",
+ "integrity": "sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg==",
"license": "MIT",
"dependencies": {
- "@docusaurus/core": "3.4.0",
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/module-type-aliases": "3.4.0",
- "@docusaurus/plugin-content-blog": "3.4.0",
- "@docusaurus/plugin-content-docs": "3.4.0",
- "@docusaurus/plugin-content-pages": "3.4.0",
- "@docusaurus/theme-common": "3.4.0",
- "@docusaurus/theme-translations": "3.4.0",
- "@docusaurus/types": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/module-type-aliases": "3.5.2",
+ "@docusaurus/plugin-content-blog": "3.5.2",
+ "@docusaurus/plugin-content-docs": "3.5.2",
+ "@docusaurus/plugin-content-pages": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/theme-translations": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"@mdx-js/react": "^3.0.0",
"clsx": "^2.0.0",
"copy-text-to-clipboard": "^3.2.0",
- "infima": "0.2.0-alpha.43",
+ "infima": "0.2.0-alpha.44",
"lodash": "^4.17.21",
"nprogress": "^0.2.0",
"postcss": "^8.4.26",
@@ -2583,19 +2587,73 @@
"react-dom": "^18.0.0"
}
},
- "node_modules/@docusaurus/theme-common": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz",
- "integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==",
+ "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-blog": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz",
+ "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==",
"license": "MIT",
"dependencies": {
- "@docusaurus/mdx-loader": "3.4.0",
- "@docusaurus/module-type-aliases": "3.4.0",
- "@docusaurus/plugin-content-blog": "3.4.0",
- "@docusaurus/plugin-content-docs": "3.4.0",
- "@docusaurus/plugin-content-pages": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
+ "cheerio": "1.0.0-rc.12",
+ "feed": "^4.2.2",
+ "fs-extra": "^11.1.1",
+ "lodash": "^4.17.21",
+ "reading-time": "^1.5.0",
+ "srcset": "^4.0.0",
+ "tslib": "^2.6.0",
+ "unist-util-visit": "^5.0.0",
+ "utility-types": "^3.10.0",
+ "webpack": "^5.88.1"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "@docusaurus/plugin-content-docs": "*",
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-pages": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz",
+ "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==",
+ "license": "MIT",
+ "dependencies": {
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/types": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
+ "fs-extra": "^11.1.1",
+ "tslib": "^2.6.0",
+ "webpack": "^5.88.1"
+ },
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "react": "^18.0.0",
+ "react-dom": "^18.0.0"
+ }
+ },
+ "node_modules/@docusaurus/theme-common": {
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz",
+ "integrity": "sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew==",
+ "license": "MIT",
+ "dependencies": {
+ "@docusaurus/mdx-loader": "3.5.2",
+ "@docusaurus/module-type-aliases": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
"@types/history": "^4.7.11",
"@types/react": "*",
"@types/react-router-config": "*",
@@ -2609,24 +2667,25 @@
"node": ">=18.0"
},
"peerDependencies": {
+ "@docusaurus/plugin-content-docs": "*",
"react": "^18.0.0",
"react-dom": "^18.0.0"
}
},
"node_modules/@docusaurus/theme-search-algolia": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz",
- "integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz",
+ "integrity": "sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA==",
"license": "MIT",
"dependencies": {
"@docsearch/react": "^3.5.2",
- "@docusaurus/core": "3.4.0",
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/plugin-content-docs": "3.4.0",
- "@docusaurus/theme-common": "3.4.0",
- "@docusaurus/theme-translations": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-validation": "3.4.0",
+ "@docusaurus/core": "3.5.2",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/plugin-content-docs": "3.5.2",
+ "@docusaurus/theme-common": "3.5.2",
+ "@docusaurus/theme-translations": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-validation": "3.5.2",
"algoliasearch": "^4.18.0",
"algoliasearch-helper": "^3.13.3",
"clsx": "^2.0.0",
@@ -2645,9 +2704,9 @@
}
},
"node_modules/@docusaurus/theme-translations": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz",
- "integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz",
+ "integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==",
"license": "MIT",
"dependencies": {
"fs-extra": "^11.1.1",
@@ -2658,9 +2717,9 @@
}
},
"node_modules/@docusaurus/types": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz",
- "integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz",
+ "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==",
"license": "MIT",
"dependencies": {
"@mdx-js/mdx": "^3.0.0",
@@ -2679,13 +2738,13 @@
}
},
"node_modules/@docusaurus/utils": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz",
- "integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz",
+ "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
"@svgr/webpack": "^8.1.0",
"escape-string-regexp": "^4.0.0",
"file-loader": "^6.2.0",
@@ -2718,9 +2777,9 @@
}
},
"node_modules/@docusaurus/utils-common": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz",
- "integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz",
+ "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.6.0"
@@ -2738,14 +2797,14 @@
}
},
"node_modules/@docusaurus/utils-validation": {
- "version": "3.4.0",
- "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz",
- "integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==",
+ "version": "3.5.2",
+ "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz",
+ "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==",
"license": "MIT",
"dependencies": {
- "@docusaurus/logger": "3.4.0",
- "@docusaurus/utils": "3.4.0",
- "@docusaurus/utils-common": "3.4.0",
+ "@docusaurus/logger": "3.5.2",
+ "@docusaurus/utils": "3.5.2",
+ "@docusaurus/utils-common": "3.5.2",
"fs-extra": "^11.2.0",
"joi": "^17.9.2",
"js-yaml": "^4.1.0",
@@ -8767,9 +8826,10 @@
}
},
"node_modules/infima": {
- "version": "0.2.0-alpha.43",
- "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz",
- "integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==",
+ "version": "0.2.0-alpha.44",
+ "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.44.tgz",
+ "integrity": "sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ==",
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -13638,9 +13698,10 @@
}
},
"node_modules/prism-react-renderer": {
- "version": "2.3.1",
- "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz",
- "integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==",
+ "version": "2.4.0",
+ "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz",
+ "integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==",
+ "license": "MIT",
"dependencies": {
"@types/prismjs": "^1.26.0",
"clsx": "^2.0.0"
@@ -16020,9 +16081,9 @@
}
},
"node_modules/tailwindcss": {
- "version": "3.4.9",
- "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
- "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
+ "version": "3.4.10",
+ "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
+ "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
"license": "MIT",
"dependencies": {
"@alloc/quick-lru": "^5.2.0",
diff --git a/docs/package.json b/docs/package.json
index e32fe094996a6..cdcdf53446884 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
- "node": "20.16.0"
+ "node": "20.17.0"
}
}
diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx
index 1c1ad7cabdaa8..6982853fade77 100644
--- a/docs/src/components/community-guides.tsx
+++ b/docs/src/components/community-guides.tsx
@@ -43,6 +43,11 @@ const guides: CommunityGuidesProps[] = [
description: 'Access your local Immich installation over the internet using your own domain',
url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md',
},
+ {
+ title: 'Nginx caching map server',
+ description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server',
+ url: 'https://github.com/pcouy/pcouy.github.io/blob/main/_posts/2024-08-30-proxying-a-map-tile-server-for-increased-privacy.md',
+ },
];
function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element {
diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx
index 0fd4cc25c1f01..d8273c67c2179 100644
--- a/docs/src/components/community-projects.tsx
+++ b/docs/src/components/community-projects.tsx
@@ -28,11 +28,6 @@ const projects: CommunityProjectProps[] = [
description: 'A simple way to remove orphaned offline assets from the Immich database',
url: 'https://github.com/Thoroslives/immich_remove_offline_files',
},
- {
- title: 'Create albums from folders',
- description: 'A Python script to create albums based on the folder structure of an external library.',
- url: 'https://github.com/Salvoxia/immich-folder-album-creator',
- },
{
title: 'Immich-Tools',
description: 'Provides scripts for handling problems on the repair page.',
@@ -43,6 +38,11 @@ const projects: CommunityProjectProps[] = [
description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.',
url: 'https://github.com/midzelis/mi.Immich.Publisher',
},
+ {
+ title: 'Lightroom Immich Plugin: lrc-immich-plugin',
+ description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.',
+ url: 'https://github.com/bmachek/lrc-immich-plugin',
+ },
{
title: 'Immich Duplicate Finder',
description: 'Webapp that uses machine learning to identify near-duplicate images.',
@@ -58,6 +58,11 @@ const projects: CommunityProjectProps[] = [
description: 'Unofficial Immich Android TV app.',
url: 'https://github.com/giejay/Immich-Android-TV',
},
+ {
+ title: 'Create albums from folders',
+ description: 'A Python script to create albums based on the folder structure of an external library.',
+ url: 'https://github.com/Salvoxia/immich-folder-album-creator',
+ },
{
title: 'Powershell Module PSImmich',
description: 'Powershell Module for the Immich API',
@@ -68,6 +73,16 @@ const projects: CommunityProjectProps[] = [
description: 'Snap package for easy install and zero-care auto updates of Immich. Self-hosted photo management.',
url: 'https://immich-distribution.nsg.cc',
},
+ {
+ title: 'Immich Kiosk',
+ description: 'Lightweight slideshow to run on kiosk devices and browsers.',
+ url: 'https://github.com/damongolding/immich-kiosk',
+ },
+ {
+ title: 'Immich Power Tools',
+ description: 'Power tools for organizing your immich library.',
+ url: 'https://github.com/varun-raj/immich-power-tools',
+ },
];
function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element {
diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx
index dae822f4f7303..b89a65c6e4ae9 100644
--- a/docs/src/components/version-switcher.tsx
+++ b/docs/src/components/version-switcher.tsx
@@ -1,4 +1,3 @@
-import '@docusaurus/theme-classic/lib/theme/Unlisted/index';
import { useWindowSize } from '@docusaurus/theme-common';
import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem';
import React, { useEffect, useState } from 'react';
diff --git a/docs/src/pages/roadmap.tsx b/docs/src/pages/roadmap.tsx
index d8e5032e90e0b..b7c3c8af20b6c 100644
--- a/docs/src/pages/roadmap.tsx
+++ b/docs/src/pages/roadmap.tsx
@@ -15,6 +15,7 @@ import {
mdiCloudUploadOutline,
mdiCollage,
mdiContentDuplicate,
+ mdiCrop,
mdiDevices,
mdiEmailOutline,
mdiExpansionCard,
@@ -26,6 +27,7 @@ import {
mdiFileSearch,
mdiFlash,
mdiFolder,
+ mdiFolderMultiple,
mdiForum,
mdiHandshakeOutline,
mdiHeart,
@@ -36,6 +38,7 @@ import {
mdiImageMultipleOutline,
mdiImageSearch,
mdiKeyboardSettingsOutline,
+ mdiLicense,
mdiLockOutline,
mdiMagnify,
mdiMagnifyScan,
@@ -55,25 +58,29 @@ import {
mdiScaleBalance,
mdiSecurity,
mdiServer,
+ mdiShare,
mdiShareAll,
mdiShareCircle,
mdiStar,
+ mdiStarOutline,
mdiTableKey,
mdiTag,
+ mdiTagMultiple,
mdiText,
mdiThemeLightDark,
mdiTrashCanOutline,
mdiVectorCombine,
mdiVideo,
mdiWeb,
- mdiLicense,
} from '@mdi/js';
import Layout from '@theme/Layout';
import React from 'react';
import { Item, Timeline } from '../components/timeline';
const releases = {
- // TODO
+ 'v1.113.0': new Date(2024, 7, 30),
+ 'v1.112.0': new Date(2024, 7, 14),
+ 'v1.111.0': new Date(2024, 6, 26),
'v1.110.0': new Date(2024, 5, 11),
'v1.109.0': new Date(2024, 6, 18),
'v1.106.1': new Date(2024, 5, 11),
@@ -224,6 +231,47 @@ const roadmap: Item[] = [
];
const milestones: Item[] = [
+ withRelease({
+ icon: mdiTagMultiple,
+ iconColor: 'orange',
+ title: 'Tags',
+ description: 'Tag your photos and videos',
+ release: 'v1.113.0',
+ }),
+ withRelease({
+ icon: mdiFolderMultiple,
+ iconColor: 'brown',
+ title: 'Folders',
+ description: 'View your photos and videos in folders',
+ release: 'v1.113.0',
+ }),
+ withRelease({
+ icon: mdiPalette,
+ title: 'Theming (mobile)',
+ description: 'Pick a primary color for the mobile app',
+ release: 'v1.112.0',
+ }),
+ withRelease({
+ icon: mdiStarOutline,
+ iconColor: 'gold',
+ title: 'Star rating',
+ description: 'Rate your photos and videos',
+ release: 'v1.112.0',
+ }),
+ withRelease({
+ icon: mdiCrop,
+ iconColor: 'royalblue',
+ title: 'Editor (mobile)',
+ description: 'Crop and rotate on mobile',
+ release: 'v1.111.0',
+ }),
+ withRelease({
+ icon: mdiMap,
+ iconColor: 'green',
+ title: 'Deploy tiles.immich.cloud',
+ description: 'Dedicated tile server for Immich',
+ release: 'v1.111.0',
+ }),
{
icon: mdiStar,
iconColor: 'gold',
@@ -231,6 +279,12 @@ const milestones: Item[] = [
description: 'Reached 40K Stars on GitHub!',
getDateLabel: withLanguage(new Date(2024, 6, 21)),
},
+ withRelease({
+ icon: mdiShare,
+ title: 'Deploy my.immich.app',
+ description: 'Url router for immich links',
+ release: 'v1.109.0',
+ }),
withRelease({
icon: mdiLicense,
iconColor: 'gold',
diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json
index c2bce22893622..c16413f4c5f49 100644
--- a/docs/static/archived-versions.json
+++ b/docs/static/archived-versions.json
@@ -1,4 +1,16 @@
[
+ {
+ "label": "v1.114.0",
+ "url": "https://v1.114.0.archive.immich.app"
+ },
+ {
+ "label": "v1.113.1",
+ "url": "https://v1.113.1.archive.immich.app"
+ },
+ {
+ "label": "v1.113.0",
+ "url": "https://v1.113.0.archive.immich.app"
+ },
{
"label": "v1.112.1",
"url": "https://v1.112.1.archive.immich.app"
diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js
index d3ed1f3cda916..1ef26facbb621 100644
--- a/docs/tailwind.config.js
+++ b/docs/tailwind.config.js
@@ -4,7 +4,7 @@ module.exports = {
corePlugins: {
preflight: false, // disable Tailwind's reset
},
- content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src
+ content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src
darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns
theme: {
extend: {
diff --git a/e2e/.nvmrc b/e2e/.nvmrc
index 8ce7030825b5e..3516580bbbc04 100644
--- a/e2e/.nvmrc
+++ b/e2e/.nvmrc
@@ -1 +1 @@
-20.16.0
+20.17.0
diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml
index b45ea4137f25a..cbeca0deca296 100644
--- a/e2e/docker-compose.yml
+++ b/e2e/docker-compose.yml
@@ -30,7 +30,7 @@ services:
- redis
- database
ports:
- - 2283:3001
+ - 2285:3001
redis:
image: redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e
@@ -43,7 +43,7 @@ services:
POSTGRES_USER: postgres
POSTGRES_DB: immich
ports:
- - 5433:5432
+ - 5435:5432
volumes:
model-cache:
diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs
index 9a1bb9959851a..fd1e8a0af693d 100644
--- a/e2e/eslint.config.mjs
+++ b/e2e/eslint.config.mjs
@@ -59,6 +59,7 @@ export default [
'unicorn/prefer-top-level-await': 'off',
'unicorn/prefer-event-target': 'off',
'unicorn/no-thenable': 'off',
+ 'object-shorthand': ['error', 'always'],
},
},
];
diff --git a/e2e/package-lock.json b/e2e/package-lock.json
index bc08cb0f9218d..97e396c09f1b0 100644
--- a/e2e/package-lock.json
+++ b/e2e/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "immich-e2e",
- "version": "1.112.1",
+ "version": "1.114.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "immich-e2e",
- "version": "1.112.1",
+ "version": "1.114.0",
"license": "GNU Affero General Public License version 3",
"devDependencies": {
"@eslint/eslintrc": "^3.1.0",
@@ -15,7 +15,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -45,7 +45,7 @@
},
"../cli": {
"name": "@immich/cli",
- "version": "2.2.15",
+ "version": "2.2.18",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
@@ -64,7 +64,7 @@
"@types/cli-progress": "^3.11.0",
"@types/lodash-es": "^4.17.12",
"@types/mock-fs": "^4.13.1",
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"@vitest/coverage-v8": "^2.0.5",
@@ -92,14 +92,14 @@
},
"../open-api/typescript-sdk": {
"name": "@immich/sdk",
- "version": "1.112.1",
+ "version": "1.114.0",
"dev": true,
"license": "GNU Affero General Public License version 3",
"dependencies": {
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"typescript": "^5.3.3"
}
},
@@ -747,9 +747,9 @@
}
},
"node_modules/@eslint/config-array": {
- "version": "0.17.1",
- "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz",
- "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==",
+ "version": "0.18.0",
+ "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.18.0.tgz",
+ "integrity": "sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
@@ -799,9 +799,9 @@
}
},
"node_modules/@eslint/js": {
- "version": "9.8.0",
- "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz",
- "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==",
+ "version": "9.9.1",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.1.tgz",
+ "integrity": "sha512-xIDQRsfg5hNBqHz04H1R3scSVwmI+KUbqjsQKHKQ1DAUSaUjYPReZZmS/5PNiKu1fUvzDd6H7DEDKACSEhu+TQ==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1113,13 +1113,13 @@
}
},
"node_modules/@playwright/test": {
- "version": "1.46.0",
- "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz",
- "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==",
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz",
+ "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright": "1.46.0"
+ "playwright": "1.46.1"
},
"bin": {
"playwright": "cli.js"
@@ -1516,9 +1516,9 @@
"dev": true
},
"node_modules/@types/node": {
- "version": "20.16.1",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz",
- "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==",
+ "version": "20.16.3",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.3.tgz",
+ "integrity": "sha512-/wdGiWRkMOm53gAsSyFMXFZHbVg7C6CbkrzHNpaHoYfsUWPg7m6ZRKtvQjgvQ9i8WT540a3ydRlRQbxjY30XxQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1532,20 +1532,22 @@
"dev": true
},
"node_modules/@types/oidc-provider": {
- "version": "8.5.1",
- "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.1.tgz",
- "integrity": "sha512-NS8tBPOj9GG6SxyrUHWBzglOtAYNDX41J4cRE45oeK0iSqI6V6tDW70aPWg25pJFNSC1evccXFm9evfwjxm7HQ==",
+ "version": "8.5.2",
+ "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.2.tgz",
+ "integrity": "sha512-NiD3VG49+cRCAAe8+uZLM4onOcX8y9+cwaml8JG1qlgc98rWoCRgsnOB4Ypx+ysays5jiwzfUgT0nWyXPB/9uQ==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/koa": "*",
"@types/node": "*"
}
},
"node_modules/@types/pg": {
- "version": "8.11.6",
- "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.6.tgz",
- "integrity": "sha512-/2WmmBXHLsfRqzfHW7BNZ8SbYzE8OSk7i3WjFYvfgRHj7S1xj+16Je5fUKv3lVdVzk/zn9TXOqf+avFCFIE0yQ==",
+ "version": "8.11.8",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.11.8.tgz",
+ "integrity": "sha512-IqpCf8/569txXN/HoP5i1LjXfKZWL76Yr2R77xgeIICUbAYHeoaEZFhYHo2uDftecLWrTJUq63JvQu8q3lnDyA==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"@types/node": "*",
"pg-protocol": "*",
@@ -1557,6 +1559,7 @@
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-4.0.2.tgz",
"integrity": "sha512-cRL3JpS3lKMGsKaWndugWQoLOCoP+Cic8oseVcbr0qhPzYD5DWXK+RZ9LY9wxRf7RQia4SCwQlXk0q6FCPrVng==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"pg-numeric": "1.0.2",
@@ -1575,6 +1578,7 @@
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-3.0.2.tgz",
"integrity": "sha512-6faShkdFugNQCLwucjPcY5ARoW1SlbnrZjmGl0IrrqewpvxvhSLHimCVzqeuULCbG0fQv7Dtk1yDbG3xv7Veog==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -1584,6 +1588,7 @@
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-3.0.0.tgz",
"integrity": "sha512-CNd4jim9RFPkObHSjVHlVrxoVQXz7quwNFpz7RY1okNNme49+sVyiTvTRobiLV548Hx/hb1BG+iE7h9493WzFw==",
"dev": true,
+ "license": "MIT",
"dependencies": {
"obuf": "~1.1.2"
},
@@ -1596,6 +1601,7 @@
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-2.1.0.tgz",
"integrity": "sha512-K7Juri8gtgXVcDfZttFKVmhglp7epKb1K4pgrkLxehjqkrgPhfG6OO8LHLkfaqkbpjNRnra018XwAr1yQFWGcA==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -1605,6 +1611,7 @@
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-3.0.0.tgz",
"integrity": "sha512-BSNDnbyZCXSxgA+1f5UU2GmwhoI0aU5yMxRGO8CdFEcY2BQF9xm/7MqKnYoM1nJDk8nONNWDk9WeSmePFhQdlw==",
"dev": true,
+ "license": "MIT",
"engines": {
"node": ">=12"
}
@@ -1673,17 +1680,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz",
- "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz",
+ "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/type-utils": "8.0.1",
- "@typescript-eslint/utils": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/type-utils": "8.3.0",
+ "@typescript-eslint/utils": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1707,16 +1714,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz",
- "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz",
+ "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/typescript-estree": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1736,14 +1743,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz",
- "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz",
+ "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1"
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1754,14 +1761,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz",
- "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz",
+ "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/typescript-estree": "8.0.1",
- "@typescript-eslint/utils": "8.0.1",
+ "@typescript-eslint/typescript-estree": "8.3.0",
+ "@typescript-eslint/utils": "8.3.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1779,9 +1786,9 @@
}
},
"node_modules/@typescript-eslint/types": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz",
- "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz",
+ "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1793,16 +1800,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz",
- "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz",
+ "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/visitor-keys": "8.0.1",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/visitor-keys": "8.3.0",
"debug": "^4.3.4",
- "globby": "^11.1.0",
+ "fast-glob": "^3.3.2",
"is-glob": "^4.0.3",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
@@ -1848,16 +1855,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz",
- "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz",
+ "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
- "@typescript-eslint/scope-manager": "8.0.1",
- "@typescript-eslint/types": "8.0.1",
- "@typescript-eslint/typescript-estree": "8.0.1"
+ "@typescript-eslint/scope-manager": "8.3.0",
+ "@typescript-eslint/types": "8.3.0",
+ "@typescript-eslint/typescript-estree": "8.3.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1871,13 +1878,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz",
- "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==",
+ "version": "8.3.0",
+ "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz",
+ "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@typescript-eslint/types": "8.0.1",
+ "@typescript-eslint/types": "8.3.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -2111,16 +2118,6 @@
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
- "node_modules/array-union": {
- "version": "2.1.0",
- "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
- "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/asap": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
@@ -2724,19 +2721,6 @@
"wrappy": "1"
}
},
- "node_modules/dir-glob": {
- "version": "3.0.1",
- "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
- "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "path-type": "^4.0.0"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/eastasianwidth": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
@@ -2888,17 +2872,17 @@
}
},
"node_modules/eslint": {
- "version": "9.8.0",
- "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz",
- "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==",
+ "version": "9.9.1",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.1.tgz",
+ "integrity": "sha512-dHvhrbfr4xFQ9/dq+jcVneZMyRYLjggWjk6RVsIiHsP8Rz6yZ8LvZ//iU4TrZF+SXWG+JkNF2OyiZRvzgRDqMg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.11.0",
- "@eslint/config-array": "^0.17.1",
+ "@eslint/config-array": "^0.18.0",
"@eslint/eslintrc": "^3.1.0",
- "@eslint/js": "9.8.0",
+ "@eslint/js": "9.9.1",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.3.0",
"@nodelib/fs.walk": "^1.2.8",
@@ -2937,6 +2921,14 @@
},
"funding": {
"url": "https://eslint.org/donate"
+ },
+ "peerDependencies": {
+ "jiti": "*"
+ },
+ "peerDependenciesMeta": {
+ "jiti": {
+ "optional": true
+ }
}
},
"node_modules/eslint-config-prettier": {
@@ -3573,27 +3565,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
- "node_modules/globby": {
- "version": "11.1.0",
- "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
- "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "array-union": "^2.1.0",
- "dir-glob": "^3.0.1",
- "fast-glob": "^3.2.9",
- "ignore": "^5.2.0",
- "merge2": "^1.4.1",
- "slash": "^3.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@@ -4125,10 +4096,11 @@
}
},
"node_modules/jose": {
- "version": "5.6.3",
- "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz",
- "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==",
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/jose/-/jose-5.8.0.tgz",
+ "integrity": "sha512-E7CqYpL/t7MMnfGnK/eg416OsFCVUrU/Y3Vwe7QjKhu/BkS1Ms455+2xsqZQVN57/U2MHMBvEb5SrmAZWAIntA==",
"dev": true,
+ "license": "MIT",
"funding": {
"url": "https://github.com/sponsors/panva"
}
@@ -4436,9 +4408,9 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.7",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
- "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -5034,16 +5006,6 @@
"integrity": "sha512-GQX3SSMokngb36+whdpRXE+3f9V8UzyAorlYvOGx87ufGHehNTn5lCxrKtLyZ4Yl/wEKnNnr98ZzOwwDZV5ogw==",
"dev": true
},
- "node_modules/path-type": {
- "version": "4.0.0",
- "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
- "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/pathe": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz",
@@ -5178,13 +5140,13 @@
}
},
"node_modules/playwright": {
- "version": "1.46.0",
- "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz",
- "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==",
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz",
+ "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
- "playwright-core": "1.46.0"
+ "playwright-core": "1.46.1"
},
"bin": {
"playwright": "cli.js"
@@ -5197,9 +5159,9 @@
}
},
"node_modules/playwright-core": {
- "version": "1.46.0",
- "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz",
- "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==",
+ "version": "1.46.1",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz",
+ "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==",
"dev": true,
"license": "Apache-2.0",
"bin": {
@@ -5829,16 +5791,6 @@
"url": "https://github.com/sponsors/isaacs"
}
},
- "node_modules/slash": {
- "version": "3.0.0",
- "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
- "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
- "dev": true,
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/socket.io-client": {
"version": "4.7.5",
"resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz",
diff --git a/e2e/package.json b/e2e/package.json
index 1c19526e83dc2..3577bc4510a9e 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -1,6 +1,6 @@
{
"name": "immich-e2e",
- "version": "1.112.1",
+ "version": "1.114.0",
"description": "",
"main": "index.js",
"type": "module",
@@ -25,7 +25,7 @@
"@immich/sdk": "file:../open-api/typescript-sdk",
"@playwright/test": "^1.44.1",
"@types/luxon": "^3.4.2",
- "@types/node": "^20.14.15",
+ "@types/node": "^20.16.2",
"@types/oidc-provider": "^8.5.1",
"@types/pg": "^8.11.0",
"@types/pngjs": "^6.0.4",
@@ -53,6 +53,6 @@
"vitest": "^2.0.5"
},
"volta": {
- "node": "20.16.0"
+ "node": "20.17.0"
}
}
diff --git a/e2e/playwright.config.ts b/e2e/playwright.config.ts
index 65a9c78823f85..55032bd364bee 100644
--- a/e2e/playwright.config.ts
+++ b/e2e/playwright.config.ts
@@ -8,7 +8,7 @@ export default defineConfig({
workers: 1,
reporter: 'html',
use: {
- baseURL: 'http://127.0.0.1:2283',
+ baseURL: 'http://127.0.0.1:2285',
trace: 'on-first-retry',
},
@@ -54,7 +54,7 @@ export default defineConfig({
/* Run your local dev server before starting the tests */
webServer: {
command: 'docker compose up --build -V --remove-orphans',
- url: 'http://127.0.0.1:2283',
+ url: 'http://127.0.0.1:2285',
reuseExistingServer: true,
},
});
diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts
index 99b33dfed8e93..7d3c3c6e59ad2 100644
--- a/e2e/src/api/specs/asset.e2e-spec.ts
+++ b/e2e/src/api/specs/asset.e2e-spec.ts
@@ -6,7 +6,9 @@ import {
LoginResponseDto,
SharedLinkType,
getAssetInfo,
+ getConfig,
getMyUser,
+ updateConfig,
} from '@immich/sdk';
import { exiftool } from 'exiftool-vendored';
import { DateTime } from 'luxon';
@@ -43,6 +45,9 @@ const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
+const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
+
+const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
const readTags = async (bytes: Buffer, filename: string) => {
const filepath = join(tempDir, filename);
@@ -71,6 +76,7 @@ describe('/asset', () => {
let user2Assets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto;
let ratingAsset: AssetMediaResponseDto;
+ let facesAsset: AssetMediaResponseDto;
const setupTests = async () => {
await utils.resetDatabase();
@@ -224,6 +230,64 @@ describe('/asset', () => {
});
});
+ it('should get the asset faces', async () => {
+ const config = await getSystemConfig(admin.accessToken);
+ config.metadata.faces.import = true;
+ await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
+
+ // asset faces
+ facesAsset = await utils.createAsset(admin.accessToken, {
+ assetData: {
+ filename: 'portrait.jpg',
+ bytes: await readFile(facesAssetFilepath),
+ },
+ });
+
+ 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([
+ {
+ name: 'Marie Curie',
+ birthDate: null,
+ thumbnailPath: '',
+ isHidden: false,
+ faces: [
+ {
+ imageHeight: 700,
+ imageWidth: 840,
+ boundingBoxX1: 261,
+ boundingBoxX2: 356,
+ boundingBoxY1: 146,
+ boundingBoxY2: 284,
+ sourceType: 'exif',
+ },
+ ],
+ },
+ {
+ name: 'Pierre Curie',
+ birthDate: null,
+ thumbnailPath: '',
+ isHidden: false,
+ faces: [
+ {
+ imageHeight: 700,
+ imageWidth: 840,
+ boundingBoxX1: 536,
+ boundingBoxX2: 618,
+ boundingBoxY1: 83,
+ boundingBoxY2: 252,
+ sourceType: 'exif',
+ },
+ ],
+ },
+ ]);
+ });
+
it('should work with a shared link', async () => {
const sharedLink = await utils.createSharedLink(user1.accessToken, {
type: SharedLinkType.Individual,
@@ -843,7 +907,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.avif',
- resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
@@ -859,7 +922,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'el_torcal_rocks.jpg',
- resized: true,
exifInfo: {
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
exifImageWidth: 512,
@@ -883,7 +945,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '8bit-sRGB.jxl',
- resized: true,
exifInfo: {
description: '',
exifImageHeight: 1080,
@@ -899,7 +960,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'IMG_2682.heic',
- resized: true,
fileCreatedAt: '2019-03-21T16:04:22.348Z',
exifInfo: {
dateTimeOriginal: '2019-03-21T16:04:22.348Z',
@@ -924,7 +984,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'density_plot.png',
- resized: true,
exifInfo: {
exifImageWidth: 800,
exifImageHeight: 800,
@@ -939,7 +998,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'glarus.nef',
- resized: true,
fileCreatedAt: '2010-07-20T17:27:12.000Z',
exifInfo: {
make: 'NIKON CORPORATION',
@@ -961,7 +1019,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: 'philadelphia.nef',
- resized: true,
fileCreatedAt: '2016-09-22T22:10:29.060Z',
exifInfo: {
make: 'NIKON CORPORATION',
@@ -984,7 +1041,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '4_3.rw2',
- resized: true,
fileCreatedAt: '2018-05-10T08:42:37.842Z',
exifInfo: {
make: 'Panasonic',
@@ -1008,7 +1064,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '12bit-compressed-(3_2).arw',
- resized: true,
fileCreatedAt: '2016-09-27T10:51:44.000Z',
exifInfo: {
make: 'SONY',
@@ -1033,7 +1088,6 @@ describe('/asset', () => {
expected: {
type: AssetTypeEnum.Image,
originalFileName: '14bit-uncompressed-(3_2).arw',
- resized: true,
fileCreatedAt: '2016-01-08T14:08:01.000Z',
exifInfo: {
make: 'SONY',
diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts
index 59968f3b7942b..2f6274d1fca4d 100644
--- a/e2e/src/api/specs/library.e2e-spec.ts
+++ b/e2e/src/api/specs/library.e2e-spec.ts
@@ -353,7 +353,7 @@ describe('/libraries', () => {
expect(assets.count).toBe(2);
- utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
+ utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 });
@@ -361,11 +361,11 @@ describe('/libraries', () => {
const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
expect(newAssets.count).toBe(3);
- utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
+ utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
});
- it('should offline missing files', async () => {
- utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
+ it('should offline a file missing from disk', async () => {
+ utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
importPaths: [`${testAssetDirInternal}/temp`],
@@ -374,7 +374,40 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
- utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`);
+ const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+ expect(assets.count).toBe(3);
+
+ utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+ expect(newAssets.count).toBe(3);
+
+ expect(newAssets.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ isOffline: true,
+ originalFileName: 'assetC.png',
+ }),
+ ]),
+ );
+ });
+
+ it('should offline a file outside of import paths', async () => {
+ const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
+ importPaths: [`${testAssetDirInternal}/temp`],
+ });
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ await request(app)
+ .put(`/libraries/${library.id}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] });
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -383,6 +416,45 @@ describe('/libraries', () => {
expect(assets.items).toEqual(
expect.arrayContaining([
+ expect.objectContaining({
+ isOffline: false,
+ originalFileName: 'assetA.png',
+ }),
+ expect.objectContaining({
+ isOffline: true,
+ originalFileName: 'assetB.png',
+ }),
+ ]),
+ );
+ });
+
+ it('should offline a file covered by an exclusion pattern', async () => {
+ const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
+ importPaths: [`${testAssetDirInternal}/temp`],
+ });
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ await request(app)
+ .put(`/libraries/${library.id}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ exclusionPatterns: ['**/directoryB/**'] });
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+
+ expect(assets.count).toBe(2);
+
+ expect(assets.items).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ isOffline: false,
+ originalFileName: 'assetA.png',
+ }),
expect.objectContaining({
isOffline: true,
originalFileName: 'assetB.png',
@@ -434,6 +506,8 @@ describe('/libraries', () => {
await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 });
expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true);
+
+ utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`);
});
it('should scan new files', async () => {
@@ -445,14 +519,14 @@ describe('/libraries', () => {
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
- utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
+ utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
- utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`);
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+ expect(assets.count).toBe(3);
expect(assets.items).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -460,6 +534,8 @@ describe('/libraries', () => {
}),
]),
);
+
+ utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`);
});
describe('with refreshModifiedFiles=true', () => {
@@ -559,10 +635,11 @@ describe('/libraries', () => {
it('should remove offline files', async () => {
const library = await utils.createLibrary(admin.accessToken, {
ownerId: admin.userId,
- importPaths: [`${testAssetDirInternal}/temp/offline2`],
+ importPaths: [`${testAssetDirInternal}/temp/offline`],
});
- utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
+ utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
+ utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -570,9 +647,9 @@ describe('/libraries', () => {
const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
libraryId: library.id,
});
- expect(initialAssets.count).toBe(1);
+ expect(initialAssets.count).toBe(2);
- utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`);
+ utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
await scan(admin.accessToken, library.id);
await utils.waitForQueueFinish(admin.accessToken, 'library');
@@ -593,7 +670,54 @@ describe('/libraries', () => {
const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
- expect(assets.count).toBe(0);
+ expect(assets.count).toBe(1);
+
+ utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
+ });
+
+ it('should remove offline files from trash', async () => {
+ const library = await utils.createLibrary(admin.accessToken, {
+ ownerId: admin.userId,
+ importPaths: [`${testAssetDirInternal}/temp/offline`],
+ });
+
+ utils.createImageFile(`${testAssetDir}/temp/offline/online.png`);
+ utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, {
+ libraryId: library.id,
+ });
+
+ expect(initialAssets.count).toBe(2);
+ utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
+
+ await scan(admin.accessToken, library.id);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+
+ const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, {
+ libraryId: library.id,
+ isOffline: true,
+ });
+ expect(offlineAssets.count).toBe(1);
+
+ const { status } = await request(app)
+ .post(`/libraries/${library.id}/removeOffline`)
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send();
+ expect(status).toBe(204);
+ await utils.waitForQueueFinish(admin.accessToken, 'library');
+ await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask');
+
+ const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
+
+ expect(assets.count).toBe(1);
+ expect(assets.items[0].isOffline).toBe(false);
+ expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`);
+
+ utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`);
});
it('should not remove online files', async () => {
diff --git a/e2e/src/api/specs/oauth.e2e-spec.ts b/e2e/src/api/specs/oauth.e2e-spec.ts
index 8ca17eba813d5..a37a9528c9a7b 100644
--- a/e2e/src/api/specs/oauth.e2e-spec.ts
+++ b/e2e/src/api/specs/oauth.e2e-spec.ts
@@ -92,14 +92,14 @@ describe(`/oauth`, () => {
it('should return a redirect uri', async () => {
const { status, body } = await request(app)
.post('/oauth/authorize')
- .send({ redirectUri: 'http://127.0.0.1:2283/auth/login' });
+ .send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(201);
expect(body).toEqual({ url: expect.stringContaining(`${authServer.internal}/auth?`) });
const params = new URL(body.url).searchParams;
expect(params.get('client_id')).toBe('client-default');
expect(params.get('response_type')).toBe('code');
- expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2283/auth/login');
+ expect(params.get('redirect_uri')).toBe('http://127.0.0.1:2285/auth/login');
expect(params.get('state')).toBeDefined();
});
});
diff --git a/e2e/src/api/specs/server-info.e2e-spec.ts b/e2e/src/api/specs/server-info.e2e-spec.ts
index 092eab3ec5b67..571d98cda744e 100644
--- a/e2e/src/api/specs/server-info.e2e-spec.ts
+++ b/e2e/src/api/specs/server-info.e2e-spec.ts
@@ -102,6 +102,7 @@ describe('/server-info', () => {
configFile: false,
duplicateDetection: false,
facialRecognition: false,
+ importFaces: false,
map: true,
reverseGeocoding: true,
oauth: false,
diff --git a/e2e/src/api/specs/server.e2e-spec.ts b/e2e/src/api/specs/server.e2e-spec.ts
index d19744674fdda..b19e6d85c4ad0 100644
--- a/e2e/src/api/specs/server.e2e-spec.ts
+++ b/e2e/src/api/specs/server.e2e-spec.ts
@@ -110,6 +110,7 @@ describe('/server', () => {
facialRecognition: false,
map: true,
reverseGeocoding: true,
+ importFaces: false,
oauth: false,
oauthAutoLaunch: false,
passwordLogin: true,
diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/api/specs/tag.e2e-spec.ts
new file mode 100644
index 0000000000000..a4cbc99ed3bc7
--- /dev/null
+++ b/e2e/src/api/specs/tag.e2e-spec.ts
@@ -0,0 +1,603 @@
+import {
+ AssetMediaResponseDto,
+ LoginResponseDto,
+ Permission,
+ TagCreateDto,
+ TagResponseDto,
+ createTag,
+ getAllTags,
+ tagAssets,
+ upsertTags,
+} from '@immich/sdk';
+import { createUserDto, uuidDto } from 'src/fixtures';
+import { errorDto } from 'src/responses';
+import { app, asBearerAuth, utils } from 'src/utils';
+import request from 'supertest';
+import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
+
+const create = (accessToken: string, dto: TagCreateDto) =>
+ createTag({ tagCreateDto: dto }, { headers: asBearerAuth(accessToken) });
+
+const upsert = (accessToken: string, tags: string[]) =>
+ upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) });
+
+describe('/tags', () => {
+ let admin: LoginResponseDto;
+ let user: LoginResponseDto;
+ let userAsset: AssetMediaResponseDto;
+
+ beforeAll(async () => {
+ await utils.resetDatabase();
+
+ admin = await utils.adminSetup();
+ user = await utils.userSetup(admin.accessToken, createUserDto.user1);
+ userAsset = await utils.createAsset(user.accessToken);
+ });
+
+ beforeEach(async () => {
+ // tagging assets eventually triggers metadata extraction which can impact other tests
+ await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction');
+ await utils.resetDatabase(['tags']);
+ });
+
+ describe('POST /tags', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).post('/tags').send({ name: 'TagA' });
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.create'));
+ });
+
+ it('should work with tag.create', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]);
+ const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' });
+ expect(body).toEqual({
+ id: expect.any(String),
+ name: 'TagA',
+ value: 'TagA',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ expect(status).toBe(201);
+ });
+
+ it('should create a tag', async () => {
+ const { status, body } = await request(app)
+ .post('/tags')
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ name: 'TagA' });
+ expect(body).toEqual({
+ id: expect.any(String),
+ name: 'TagA',
+ value: 'TagA',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ expect(status).toBe(201);
+ });
+
+ it('should allow multiple users to create tags with the same value', async () => {
+ await create(admin.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .post('/tags')
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ name: 'TagA' });
+ expect(body).toEqual({
+ id: expect.any(String),
+ name: 'TagA',
+ value: 'TagA',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ expect(status).toBe(201);
+ });
+
+ it('should create a nested tag', async () => {
+ const parent = await create(admin.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .post('/tags')
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ name: 'TagB', parentId: parent.id });
+ expect(body).toEqual({
+ id: expect.any(String),
+ parentId: parent.id,
+ name: 'TagB',
+ value: 'TagA/TagB',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ expect(status).toBe(201);
+ });
+ });
+
+ describe('GET /tags', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).get('/tags');
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app).get('/tags').set('x-api-key', secret);
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.read'));
+ });
+
+ it('should start off empty', async () => {
+ const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(body).toEqual([]);
+ expect(status).toEqual(200);
+ });
+
+ it('should return a list of tags', async () => {
+ const [tagA, tagB, tagC] = await Promise.all([
+ create(admin.accessToken, { name: 'TagA' }),
+ create(admin.accessToken, { name: 'TagB' }),
+ create(admin.accessToken, { name: 'TagC' }),
+ ]);
+ const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(body).toHaveLength(3);
+ expect(body).toEqual([tagA, tagB, tagC]);
+ expect(status).toEqual(200);
+ });
+
+ it('should return a nested tags', async () => {
+ await upsert(admin.accessToken, ['TagA/TagB/TagC', 'TagD']);
+ const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`);
+
+ expect(body).toHaveLength(4);
+ expect(status).toEqual(200);
+
+ const tags = body as TagResponseDto[];
+ const tagA = tags.find((tag) => tag.value === 'TagA') as TagResponseDto;
+ const tagB = tags.find((tag) => tag.value === 'TagA/TagB') as TagResponseDto;
+ const tagC = tags.find((tag) => tag.value === 'TagA/TagB/TagC') as TagResponseDto;
+ const tagD = tags.find((tag) => tag.value === 'TagD') as TagResponseDto;
+
+ expect(tagA).toEqual(expect.objectContaining({ name: 'TagA', value: 'TagA' }));
+ expect(tagB).toEqual(expect.objectContaining({ name: 'TagB', value: 'TagA/TagB', parentId: tagA.id }));
+ expect(tagC).toEqual(expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC', parentId: tagB.id }));
+ expect(tagD).toEqual(expect.objectContaining({ name: 'TagD', value: 'TagD' }));
+ });
+ });
+
+ describe('PUT /tags', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' });
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.create'));
+ });
+
+ it('should upsert tags', async () => {
+ const { status, body } = await request(app)
+ .put(`/tags`)
+ .send({ tags: ['TagA/TagB/TagC/TagD'] })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual([expect.objectContaining({ name: 'TagD', value: 'TagA/TagB/TagC/TagD' })]);
+ });
+
+ it('should upsert tags in parallel without conflicts', async () => {
+ const [[tag1], [tag2], [tag3], [tag4]] = await Promise.all([
+ upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
+ upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
+ upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
+ upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']),
+ ]);
+
+ const { id, parentId, createdAt } = tag1;
+ for (const tag of [tag1, tag2, tag3, tag4]) {
+ expect(tag).toMatchObject({
+ id,
+ parentId,
+ createdAt,
+ name: 'TagD',
+ value: 'TagA/TagB/TagC/TagD',
+ });
+ }
+ });
+ });
+
+ describe('PUT /tags/assets', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] });
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app)
+ .put('/tags/assets')
+ .set('x-api-key', secret)
+ .send({ assetIds: [], tagIds: [] });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.asset'));
+ });
+
+ it('should skip assets that are not owned by the user', async () => {
+ const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
+ create(user.accessToken, { name: 'TagA' }),
+ create(user.accessToken, { name: 'TagB' }),
+ create(user.accessToken, { name: 'TagC' }),
+ utils.createAsset(user.accessToken),
+ utils.createAsset(admin.accessToken),
+ ]);
+ const { status, body } = await request(app)
+ .put(`/tags/assets`)
+ .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 3 });
+ });
+
+ it('should skip tags that are not owned by the user', async () => {
+ const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
+ create(user.accessToken, { name: 'TagA' }),
+ create(user.accessToken, { name: 'TagB' }),
+ create(admin.accessToken, { name: 'TagC' }),
+ utils.createAsset(user.accessToken),
+ utils.createAsset(user.accessToken),
+ ]);
+ const { status, body } = await request(app)
+ .put(`/tags/assets`)
+ .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 4 });
+ });
+
+ it('should bulk tag assets', async () => {
+ const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([
+ create(user.accessToken, { name: 'TagA' }),
+ create(user.accessToken, { name: 'TagB' }),
+ create(user.accessToken, { name: 'TagC' }),
+ utils.createAsset(user.accessToken),
+ utils.createAsset(user.accessToken),
+ ]);
+ const { status, body } = await request(app)
+ .put(`/tags/assets`)
+ .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({ count: 6 });
+ });
+ });
+
+ describe('GET /tags/:id', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`);
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .get(`/tags/${tag.id}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.noPermission);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app)
+ .get(`/tags/${uuidDto.notFound}`)
+ .set('x-api-key', secret)
+ .send({ assetIds: [], tagIds: [] });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.read'));
+ });
+
+ it('should require a valid uuid', async () => {
+ const { status, body } = await request(app)
+ .get(`/tags/${uuidDto.invalid}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
+ });
+
+ it('should get tag details', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .get(`/tags/${tag.id}`)
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({
+ id: expect.any(String),
+ name: 'TagA',
+ value: 'TagA',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ });
+
+ it('should get nested tag details', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
+ const tagC = await create(user.accessToken, { name: 'TagC', parentId: tagB.id });
+ const tagD = await create(user.accessToken, { name: 'TagD', parentId: tagC.id });
+
+ const { status, body } = await request(app)
+ .get(`/tags/${tagD.id}`)
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual({
+ id: expect.any(String),
+ parentId: tagC.id,
+ name: 'TagD',
+ value: 'TagA/TagB/TagC/TagD',
+ createdAt: expect.any(String),
+ updatedAt: expect.any(String),
+ });
+ });
+ });
+
+ describe('PUT /tags/:id', () => {
+ it('should require authentication', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' });
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization', async () => {
+ const tag = await create(admin.accessToken, { name: 'tagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}`)
+ .send({ color: '#000000' })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.noPermission);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}`)
+ .set('x-api-key', secret)
+ .send({ color: '#000000' });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.update'));
+ });
+
+ it('should update a tag', async () => {
+ const tag = await create(user.accessToken, { name: 'tagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}`)
+ .send({ color: '#000000' })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual(expect.objectContaining({ color: `#000000` }));
+ });
+
+ it('should update a tag color without a # prefix', async () => {
+ const tag = await create(user.accessToken, { name: 'tagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}`)
+ .send({ color: '000000' })
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toEqual(expect.objectContaining({ color: `#000000` }));
+ });
+ });
+
+ describe('DELETE /tags/:id', () => {
+ it('should require authentication', async () => {
+ const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`);
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .delete(`/tags/${tag.id}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.noPermission);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app).delete(`/tags/${tag.id}`).set('x-api-key', secret);
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.delete'));
+ });
+
+ it('should require a valid uuid', async () => {
+ const { status, body } = await request(app)
+ .delete(`/tags/${uuidDto.invalid}`)
+ .set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
+ });
+
+ it('should delete a tag', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status } = await request(app)
+ .delete(`/tags/${tag.id}`)
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(204);
+ });
+
+ it('should delete a nested tag (root)', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
+ const { status } = await request(app)
+ .delete(`/tags/${tagA.id}`)
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(204);
+ const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) });
+ expect(tags.length).toBe(0);
+ });
+
+ it('should delete a nested tag (leaf)', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id });
+ const { status } = await request(app)
+ .delete(`/tags/${tagB.id}`)
+ .set('Authorization', `Bearer ${user.accessToken}`);
+ expect(status).toBe(204);
+ const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) });
+ expect(tags.length).toBe(1);
+ expect(tags[0]).toEqual(tagA);
+ });
+ });
+
+ describe('PUT /tags/:id/assets', () => {
+ it('should require authentication', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tagA.id}/assets`)
+ .send({ ids: [userAsset.id] });
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}/assets`)
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ ids: [userAsset.id] });
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.noPermission);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app)
+ .put(`/tags/${tag.id}/assets`)
+ .set('x-api-key', secret)
+ .send({ ids: [userAsset.id] });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.asset'));
+ });
+
+ it('should be able to tag own asset', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ ids: [userAsset.id] });
+
+ expect(status).toBe(200);
+ expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
+ });
+
+ it("should not be able to add assets to another user's tag", async () => {
+ const tagA = await create(admin.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ ids: [userAsset.id] });
+
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.badRequest('Not found or no tag.asset access'));
+ });
+
+ it('should add duplicate assets only once', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .put(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ ids: [userAsset.id, userAsset.id] });
+
+ expect(status).toBe(200);
+ expect(body).toEqual([
+ expect.objectContaining({ id: userAsset.id, success: true }),
+ expect.objectContaining({ id: userAsset.id, success: false, error: 'duplicate' }),
+ ]);
+ });
+ });
+
+ describe('DELETE /tags/:id/assets', () => {
+ it('should require authentication', async () => {
+ const tagA = await create(admin.accessToken, { name: 'TagA' });
+ const { status, body } = await request(app)
+ .delete(`/tags/${tagA}/assets`)
+ .send({ ids: [userAsset.id] });
+
+ expect(status).toBe(401);
+ expect(body).toEqual(errorDto.unauthorized);
+ });
+
+ it('should require authorization', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ await tagAssets(
+ { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
+ { headers: asBearerAuth(user.accessToken) },
+ );
+ const { status, body } = await request(app)
+ .delete(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${admin.accessToken}`)
+ .send({ ids: [userAsset.id] });
+
+ expect(status).toBe(400);
+ expect(body).toEqual(errorDto.noPermission);
+ });
+
+ it('should require authorization (api key)', async () => {
+ const tag = await create(user.accessToken, { name: 'TagA' });
+ const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]);
+ const { status, body } = await request(app)
+ .delete(`/tags/${tag.id}/assets`)
+ .set('x-api-key', secret)
+ .send({ ids: [userAsset.id] });
+ expect(status).toBe(403);
+ expect(body).toEqual(errorDto.missingPermission('tag.asset'));
+ });
+
+ it('should be able to remove own asset from own tag', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ await tagAssets(
+ { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
+ { headers: asBearerAuth(user.accessToken) },
+ );
+ const { status, body } = await request(app)
+ .delete(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ ids: [userAsset.id] });
+
+ expect(status).toBe(200);
+ expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]);
+ });
+
+ it('should remove duplicate assets only once', async () => {
+ const tagA = await create(user.accessToken, { name: 'TagA' });
+ await tagAssets(
+ { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } },
+ { headers: asBearerAuth(user.accessToken) },
+ );
+ const { status, body } = await request(app)
+ .delete(`/tags/${tagA.id}/assets`)
+ .set('Authorization', `Bearer ${user.accessToken}`)
+ .send({ ids: [userAsset.id, userAsset.id] });
+
+ expect(status).toBe(200);
+ expect(body).toEqual([
+ expect.objectContaining({ id: userAsset.id, success: true }),
+ expect.objectContaining({ id: userAsset.id, success: false, error: 'not_found' }),
+ ]);
+ });
+ });
+});
diff --git a/e2e/src/api/specs/trash.e2e-spec.ts b/e2e/src/api/specs/trash.e2e-spec.ts
index 3049ff151165b..0c28a72825beb 100644
--- a/e2e/src/api/specs/trash.e2e-spec.ts
+++ b/e2e/src/api/specs/trash.e2e-spec.ts
@@ -42,6 +42,23 @@ describe('/trash', () => {
const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
expect(after.total).toBe(0);
});
+
+ it('should empty the trash with archived assets', async () => {
+ const { id: assetId } = await utils.createAsset(admin.accessToken);
+ await utils.archiveAssets(admin.accessToken, [assetId]);
+ await utils.deleteAssets(admin.accessToken, [assetId]);
+
+ const before = await getAssetInfo({ id: assetId }, { headers: asBearerAuth(admin.accessToken) });
+ expect(before).toStrictEqual(expect.objectContaining({ id: assetId, isTrashed: true, isArchived: true }));
+
+ const { status } = await request(app).post('/trash/empty').set('Authorization', `Bearer ${admin.accessToken}`);
+ expect(status).toBe(204);
+
+ await utils.waitForWebsocketEvent({ event: 'assetDelete', id: assetId });
+
+ const after = await getAssetStatistics({ isTrashed: true }, { headers: asBearerAuth(admin.accessToken) });
+ expect(after.total).toBe(0);
+ });
});
describe('POST /trash/restore', () => {
diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts
index fc3e8175957c0..3bc3ebc9c29b8 100644
--- a/e2e/src/cli/specs/login.e2e-spec.ts
+++ b/e2e/src/cli/specs/login.e2e-spec.ts
@@ -33,7 +33,7 @@ describe(`immich login`, () => {
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
expect(stdout.split('\n')).toEqual([
- 'Logging in to http://127.0.0.1:2283/api',
+ 'Logging in to http://127.0.0.1:2285/api',
'Logged in as admin@immich.cloud',
'Wrote auth info to /tmp/immich/auth.yml',
]);
@@ -50,8 +50,8 @@ describe(`immich login`, () => {
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
expect(stdout.split('\n')).toEqual([
- 'Logging in to http://127.0.0.1:2283',
- 'Discovered API at http://127.0.0.1:2283/api',
+ 'Logging in to http://127.0.0.1:2285',
+ 'Discovered API at http://127.0.0.1:2285/api',
'Logged in as admin@immich.cloud',
'Wrote auth info to /tmp/immich/auth.yml',
]);
diff --git a/e2e/src/cli/specs/server-info.e2e-spec.ts b/e2e/src/cli/specs/server-info.e2e-spec.ts
index 13eefd3df4676..96c45c8cc0111 100644
--- a/e2e/src/cli/specs/server-info.e2e-spec.ts
+++ b/e2e/src/cli/specs/server-info.e2e-spec.ts
@@ -12,7 +12,7 @@ describe(`immich server-info`, () => {
const { stderr, stdout, exitCode } = await immichCli(['server-info']);
expect(stdout.split('\n')).toEqual([
expect.stringContaining('Server Info (via admin@immich.cloud'),
- ' Url: http://127.0.0.1:2283/api',
+ ' Url: http://127.0.0.1:2285/api',
expect.stringContaining('Version:'),
' Formats:',
expect.stringContaining('Images:'),
diff --git a/e2e/src/setup/auth-server.ts b/e2e/src/setup/auth-server.ts
index a8c49050be129..3dd63fc403426 100644
--- a/e2e/src/setup/auth-server.ts
+++ b/e2e/src/setup/auth-server.ts
@@ -86,14 +86,14 @@ const setup = async () => {
{
client_id: OAuthClient.DEFAULT,
client_secret: OAuthClient.DEFAULT,
- redirect_uris: ['http://127.0.0.1:2283/auth/login'],
+ redirect_uris: ['http://127.0.0.1:2285/auth/login'],
grant_types: ['authorization_code'],
response_types: ['code'],
},
{
client_id: OAuthClient.RS256_TOKENS,
client_secret: OAuthClient.RS256_TOKENS,
- redirect_uris: ['http://127.0.0.1:2283/auth/login'],
+ redirect_uris: ['http://127.0.0.1:2285/auth/login'],
grant_types: ['authorization_code'],
id_token_signed_response_alg: 'RS256',
jwks: { keys: [await exportJWK(publicKey)] },
@@ -101,7 +101,7 @@ const setup = async () => {
{
client_id: OAuthClient.RS256_PROFILE,
client_secret: OAuthClient.RS256_PROFILE,
- redirect_uris: ['http://127.0.0.1:2283/auth/login'],
+ redirect_uris: ['http://127.0.0.1:2285/auth/login'],
grant_types: ['authorization_code'],
userinfo_signed_response_alg: 'RS256',
jwks: { keys: [await exportJWK(publicKey)] },
diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts
index 30e2497b514d1..c67e5696975a9 100644
--- a/e2e/src/utils.ts
+++ b/e2e/src/utils.ts
@@ -30,6 +30,7 @@ import {
signUpAdmin,
updateAdminOnboarding,
updateAlbumUser,
+ updateAssets,
updateConfig,
validate,
} from '@immich/sdk';
@@ -53,8 +54,8 @@ type WaitOptions = { event: EventType; id?: string; total?: number; timeout?: nu
type AdminSetupOptions = { onboarding?: boolean };
type FileData = { bytes?: Buffer; filename: string };
-const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5433/immich';
-export const baseUrl = 'http://127.0.0.1:2283';
+const dbUrl = 'postgres://postgres:postgres@127.0.0.1:5435/immich';
+export const baseUrl = 'http://127.0.0.1:2285';
export const shareUrl = `${baseUrl}/share`;
export const app = `${baseUrl}/api`;
// TODO move test assets into e2e/assets
@@ -148,6 +149,7 @@ export const utils = {
'sessions',
'users',
'system_metadata',
+ 'tags',
];
const sql: string[] = [];
@@ -388,6 +390,9 @@ export const utils = {
return searchMetadata({ metadataSearchDto: dto }, { headers: asBearerAuth(accessToken) });
},
+ archiveAssets: (accessToken: string, ids: string[]) =>
+ updateAssets({ assetBulkUpdateDto: { ids, isArchived: true } }, { headers: asBearerAuth(accessToken) }),
+
deleteAssets: (accessToken: string, ids: string[]) =>
deleteAssets({ assetBulkDeleteDto: { ids } }, { headers: asBearerAuth(accessToken) }),
diff --git a/e2e/src/web/specs/album.e2e-spec.ts b/e2e/src/web/specs/album.e2e-spec.ts
new file mode 100644
index 0000000000000..953c7d00ae1cd
--- /dev/null
+++ b/e2e/src/web/specs/album.e2e-spec.ts
@@ -0,0 +1,25 @@
+import { LoginResponseDto } from '@immich/sdk';
+import { test } from '@playwright/test';
+import { utils } from 'src/utils';
+
+test.describe('Album', () => {
+ let admin: LoginResponseDto;
+
+ test.beforeAll(async () => {
+ utils.initSdk();
+ await utils.resetDatabase();
+ admin = await utils.adminSetup();
+ });
+
+ test(`doesn't delete album after canceling add assets`, async ({ context, page }) => {
+ await utils.setAuthCookies(context, admin.accessToken);
+
+ await page.goto('/albums');
+ await page.getByRole('button', { name: 'Create album' }).click();
+ await page.getByRole('button', { name: 'Select photos' }).click();
+ await page.getByRole('button', { name: 'Close' }).click();
+
+ await page.reload();
+ await page.getByRole('button', { name: 'Select photos' }).waitFor();
+ });
+});
diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts
index bc3f6843ca750..09340e98cbfb3 100644
--- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts
+++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts
@@ -33,7 +33,7 @@ test.describe('Photo Viewer', () => {
await page.waitForLoadState('load');
// this is the spinner
await page.waitForSelector('svg[role=status]');
- await expect(page.getByRole('status')).toBeVisible();
+ await expect(page.getByTestId('loading-spinner')).toBeVisible();
});
test('loads high resolution photo when zoomed', async ({ page }) => {
diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts
index fe7da0b2c0ead..2a02e429a5900 100644
--- a/e2e/src/web/specs/shared-link.e2e-spec.ts
+++ b/e2e/src/web/specs/shared-link.e2e-spec.ts
@@ -44,7 +44,7 @@ test.describe('Shared Links', () => {
test('download from a shared link', async ({ page }) => {
await page.goto(`/share/${sharedLink.key}`);
await page.getByRole('heading', { name: 'Test Album' }).waitFor();
- await page.locator('.group').first().hover();
+ await page.locator(`[data-asset-id="${asset.id}"]`).hover();
await page.waitForSelector('#asset-group-by-date svg');
await page.getByRole('checkbox').click();
await page.getByRole('button', { name: 'Download' }).click();
@@ -69,4 +69,15 @@ test.describe('Shared Links', () => {
await page.goto('/share/invalid');
await page.getByRole('heading', { name: 'Invalid share key' }).waitFor();
});
+
+ test('auth on navigation from shared link to timeline', async ({ context, page }) => {
+ await utils.setAuthCookies(context, admin.accessToken);
+
+ await page.goto(`/share/${sharedLink.key}`);
+ await page.getByRole('heading', { name: 'Test Album' }).waitFor();
+
+ await page.locator('a[href="/"]').click();
+ await page.waitForURL('/photos');
+ await page.locator(`[data-asset-id="${asset.id}"]`).waitFor();
+ });
});
diff --git a/e2e/src/web/specs/websocket.e2e-spec.ts b/e2e/src/web/specs/websocket.e2e-spec.ts
index 47f69ec4eaf34..a929c6467f3ad 100644
--- a/e2e/src/web/specs/websocket.e2e-spec.ts
+++ b/e2e/src/web/specs/websocket.e2e-spec.ts
@@ -13,13 +13,13 @@ test.describe('Websocket', () => {
test('connects using ipv4', async ({ page, context }) => {
await utils.setAuthCookies(context, admin.accessToken);
- await page.goto('http://127.0.0.1:2283/');
+ await page.goto('http://127.0.0.1:2285/');
await expect(page.locator('#sidebar')).toContainText('Server Online');
});
test('connects using ipv6', async ({ page, context }) => {
await utils.setAuthCookies(context, admin.accessToken, '[::1]');
- await page.goto('http://[::1]:2283/');
+ await page.goto('http://[::1]:2285/');
await expect(page.locator('#sidebar')).toContainText('Server Online');
});
});
diff --git a/e2e/test-assets b/e2e/test-assets
index 4e9731d3fc270..3e057d2f58750 160000
--- a/e2e/test-assets
+++ b/e2e/test-assets
@@ -1 +1 @@
-Subproject commit 4e9731d3fc270fe25901f72a6b6f57277cdb8a30
+Subproject commit 3e057d2f58750acdf7ff281a3938e34a86cfef4d
diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts
index 500b6d3e5900e..9c80f25ace140 100644
--- a/e2e/vitest.config.ts
+++ b/e2e/vitest.config.ts
@@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config';
// skip `docker compose up` if `make e2e` was already run
const globalSetup: string[] = ['src/setup/auth-server.ts'];
try {
- await fetch('http://127.0.0.1:2283/api/server-info/ping');
+ await fetch('http://127.0.0.1:2285/api/server-info/ping');
} catch {
globalSetup.push('src/setup/docker-compose.ts');
}
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index c06b4900e699d..f680aac826af3 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -1,6 +1,6 @@
ARG DEVICE=cpu
-FROM python:3.11-bookworm@sha256:add76c758e402c3acf53b8251da50d8ae67989a81ca96ff4331e296773df853d AS builder-cpu
+FROM python:3.11-bookworm@sha256:20c1819af5af3acba0b2b66074a2615e398ceee6842adf03cd7ad5f8d0ee3daf AS builder-cpu
FROM builder-cpu AS builder-openvino
@@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
COPY poetry.lock pyproject.toml ./
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
-FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f20388a0eeb4af4c6f8579988ac AS prod-cpu
+FROM python:3.11-slim-bookworm@sha256:ed4e985674f478c90ce879e9aa224fbb772c84e39b4aed5155b9e2280f131039 AS prod-cpu
FROM prod-cpu AS prod-openvino
diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile
index 94082ae9573d8..eaa35d14be0dd 100644
--- a/machine-learning/export/Dockerfile
+++ b/machine-learning/export/Dockerfile
@@ -1,4 +1,4 @@
-FROM mambaorg/micromamba:bookworm-slim@sha256:e37ec9f3f7dea01ef9958d3d924d46077911f7e29c4faed40cd6b37a9ac239fc AS builder
+FROM mambaorg/micromamba:bookworm-slim@sha256:29174348bd09352e5f1b1f6756cf1d00021487b8340fae040e91e4f98e954ce5 AS builder
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \
diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock
index 9d19b671d1a04..bd09bd8469e67 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -680,23 +680,23 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi-slim"
-version = "0.112.0"
+version = "0.112.2"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
files = [
- {file = "fastapi_slim-0.112.0-py3-none-any.whl", hash = "sha256:7663edfbb5036d641aa45b4f5dad341cf78d98885216e78743a8cdd39a38883e"},
- {file = "fastapi_slim-0.112.0.tar.gz", hash = "sha256:2420f700b7dc2d1a6d02c7230f7aa2ae9fa0320d8d481094062ff717659c0843"},
+ {file = "fastapi_slim-0.112.2-py3-none-any.whl", hash = "sha256:c023f74768f187af142c2fe5ff9e4ca3c4c1940bbde7df008cb283532422a23f"},
+ {file = "fastapi_slim-0.112.2.tar.gz", hash = "sha256:75b8eb0c6ee05a20270da7a527ac7ad53b83414602f42b68f7027484dab3aedb"},
]
[package.dependencies]
pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0"
-starlette = ">=0.37.2,<0.38.0"
+starlette = ">=0.37.2,<0.39.0"
typing-extensions = ">=4.8.0"
[package.extras]
-all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
-standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
+all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
+standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
[[package]]
name = "filelock"
@@ -1111,13 +1111,13 @@ test = ["objgraph", "psutil"]
[[package]]
name = "gunicorn"
-version = "22.0.0"
+version = "23.0.0"
description = "WSGI HTTP Server for UNIX"
optional = false
python-versions = ">=3.7"
files = [
- {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"},
- {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"},
+ {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"},
+ {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"},
]
[package.dependencies]
@@ -1212,13 +1212,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
[[package]]
name = "httpx"
-version = "0.27.0"
+version = "0.27.2"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
files = [
- {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
- {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
+ {file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
+ {file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
]
[package.dependencies]
@@ -1233,16 +1233,17 @@ brotli = ["brotli", "brotlicffi"]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
+zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "huggingface-hub"
-version = "0.24.5"
+version = "0.24.6"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
- {file = "huggingface_hub-0.24.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"},
- {file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"},
+ {file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"},
+ {file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"},
]
[package.dependencies]
@@ -1530,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
-version = "2.31.2"
+version = "2.31.5"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
- {file = "locust-2.31.2-py3-none-any.whl", hash = "sha256:9bcb8b777d9844ac9498d6eebe17a0afa21712419c42da27b1d1cac5895cd182"},
- {file = "locust-2.31.2.tar.gz", hash = "sha256:a31f8e1d24535494eb809bd8dfd545ada9514df4581b69bdc2ecf3e109b7a1dd"},
+ {file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
+ {file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
]
[package.dependencies]
@@ -1794,38 +1795,38 @@ files = [
[[package]]
name = "mypy"
-version = "1.11.1"
+version = "1.11.2"
description = "Optional static typing for Python"
optional = false
python-versions = ">=3.8"
files = [
- {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
- {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
- {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
- {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
- {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
- {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
- {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
- {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
- {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
- {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
- {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
- {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
- {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
- {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
- {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
- {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
- {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
- {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
- {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
- {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
- {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
- {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
- {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
- {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
- {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
- {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
- {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
+ {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
+ {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
+ {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
+ {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
+ {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
+ {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
+ {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
+ {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
+ {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
+ {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
+ {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
+ {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
+ {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
+ {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
+ {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
+ {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
+ {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
+ {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
+ {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
+ {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
+ {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
+ {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
+ {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
+ {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
+ {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
+ {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
+ {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
]
[package.dependencies]
@@ -1962,42 +1963,42 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
-version = "1.18.1"
+version = "1.19.0"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
- {file = "onnxruntime-1.18.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ef7683312393d4ba04252f1b287d964bd67d5e6048b94d2da3643986c74d80"},
- {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc706eb1df06ddf55776e15a30519fb15dda7697f987a2bbda4962845e3cec05"},
- {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7de69f5ced2a263531923fa68bbec52a56e793b802fcd81a03487b5e292bc3a"},
- {file = "onnxruntime-1.18.1-cp310-cp310-win32.whl", hash = "sha256:221e5b16173926e6c7de2cd437764492aa12b6811f45abd37024e7cf2ae5d7e3"},
- {file = "onnxruntime-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:75211b619275199c861ee94d317243b8a0fcde6032e5a80e1aa9ded8ab4c6060"},
- {file = "onnxruntime-1.18.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f26582882f2dc581b809cfa41a125ba71ad9e715738ec6402418df356969774a"},
- {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36f3a8b768506d02be349ac303fd95d92813ba3ba70304d40c3cd5c25d6a4c"},
- {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:170e711393e0618efa8ed27b59b9de0ee2383bd2a1f93622a97006a5ad48e434"},
- {file = "onnxruntime-1.18.1-cp311-cp311-win32.whl", hash = "sha256:9b6a33419b6949ea34e0dc009bc4470e550155b6da644571ecace4b198b0d88f"},
- {file = "onnxruntime-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c1380a9f1b7788da742c759b6a02ba771fe1ce620519b2b07309decbd1a2fe1"},
- {file = "onnxruntime-1.18.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:31bd57a55e3f983b598675dfc7e5d6f0877b70ec9864b3cc3c3e1923d0a01919"},
- {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9e03c4ba9f734500691a4d7d5b381cd71ee2f3ce80a1154ac8f7aed99d1ecaa"},
- {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:781aa9873640f5df24524f96f6070b8c550c66cb6af35710fd9f92a20b4bfbf6"},
- {file = "onnxruntime-1.18.1-cp312-cp312-win32.whl", hash = "sha256:3a2d9ab6254ca62adbb448222e630dc6883210f718065063518c8f93a32432be"},
- {file = "onnxruntime-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:ad93c560b1c38c27c0275ffd15cd7f45b3ad3fc96653c09ce2931179982ff204"},
- {file = "onnxruntime-1.18.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3b55dc9d3c67626388958a3eb7ad87eb7c70f75cb0f7ff4908d27b8b42f2475c"},
- {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80dbcfb6763cc0177a31168b29b4bd7662545b99a19e211de8c734b657e0669"},
- {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1ff2c61a16d6c8631796c54139bafea41ee7736077a0fc64ee8ae59432f5c58"},
- {file = "onnxruntime-1.18.1-cp38-cp38-win32.whl", hash = "sha256:219855bd272fe0c667b850bf1a1a5a02499269a70d59c48e6f27f9c8bcb25d02"},
- {file = "onnxruntime-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:afdf16aa607eb9a2c60d5ca2d5abf9f448e90c345b6b94c3ed14f4fb7e6a2d07"},
- {file = "onnxruntime-1.18.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:128df253ade673e60cea0955ec9d0e89617443a6d9ce47c2d79eb3f72a3be3de"},
- {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9839491e77e5c5a175cab3621e184d5a88925ee297ff4c311b68897197f4cde9"},
- {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3187c1faff3ac15f7f0e7373ef4788c582cafa655a80fdbb33eaec88976c66"},
- {file = "onnxruntime-1.18.1-cp39-cp39-win32.whl", hash = "sha256:34657c78aa4e0b5145f9188b550ded3af626651b15017bf43d280d7e23dbf195"},
- {file = "onnxruntime-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c14fd97c3ddfa97da5feef595e2c73f14c2d0ec1d4ecbea99c8d96603c89589"},
+ {file = "onnxruntime-1.19.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6ce22a98dfec7b646ae305f52d0ce14a189a758b02ea501860ca719f4b0ae04b"},
+ {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19019c72873f26927aa322c54cf2bf7312b23451b27451f39b88f57016c94f8b"},
+ {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8eaa16df99171dc636e30108d15597aed8c4c2dd9dbfdd07cc464d57d73fb275"},
+ {file = "onnxruntime-1.19.0-cp310-cp310-win32.whl", hash = "sha256:0eb0f8dbe596fd0f4737fe511fdbb17603853a7d204c5b2ca38d3c7808fc556b"},
+ {file = "onnxruntime-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:616092d54ba8023b7bc0a5f6d900a07a37cc1cfcc631873c15f8c1d6e9e184d4"},
+ {file = "onnxruntime-1.19.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a2b53b3c287cd933e5eb597273926e899082d8c84ab96e1b34035764a1627e17"},
+ {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e94984663963e74fbb468bde9ec6f19dcf890b594b35e249c4dc8789d08993c5"},
+ {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f379d1f050cfb55ce015d53727b78ee362febc065c38eed81512b22b757da73"},
+ {file = "onnxruntime-1.19.0-cp311-cp311-win32.whl", hash = "sha256:4ccb48faea02503275ae7e79e351434fc43c294c4cb5c4d8bcb7479061396614"},
+ {file = "onnxruntime-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:9cdc8d311289a84e77722de68bd22b8adfb94eea26f4be6f9e017350faac8b18"},
+ {file = "onnxruntime-1.19.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1b59eaec1be9a8613c5fdeaafe67f73a062edce3ac03bbbdc9e2d98b58a30617"},
+ {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4144d014a4b25184e63ce7a463a2e7796e2f3df931fccc6a6aefa6f1365dc5"},
+ {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d7e7d4ca7021ce7f29a66dbc6071addf2de5839135339bd855c6d9c2bba371"},
+ {file = "onnxruntime-1.19.0-cp312-cp312-win32.whl", hash = "sha256:87f2c58b577a1fb31dc5d92b647ecc588fd5f1ea0c3ad4526f5f80a113357c8d"},
+ {file = "onnxruntime-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a1f50d49676d7b69566536ff039d9e4e95fc482a55673719f46528218ecbb94"},
+ {file = "onnxruntime-1.19.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:71423c8c4b2d7a58956271534302ec72721c62a41efd0c4896343249b8399ab0"},
+ {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d63630d45e9498f96e75bbeb7fd4a56acb10155de0de4d0e18d1b6cbb0b358a"},
+ {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bfd15db1e8794d379a86c1a9116889f47f2cca40cc82208fc4f7e8c38e8522"},
+ {file = "onnxruntime-1.19.0-cp38-cp38-win32.whl", hash = "sha256:3b098003b6b4cb37cc84942e5f1fe27f945dd857cbd2829c824c26b0ba4a247e"},
+ {file = "onnxruntime-1.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:cea067a6541d6787d903ee6843401c5b1332a266585160d9700f9f0939443886"},
+ {file = "onnxruntime-1.19.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c4fcff12dc5ca963c5f76b9822bb404578fa4a98c281e8c666b429192799a099"},
+ {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6dcad8a4db908fbe70b98c79cea1c8b6ac3316adf4ce93453136e33a524ac59"},
+ {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bc449907c6e8d99eee5ae5cc9c8fdef273d801dcd195393d3f9ab8ad3f49522"},
+ {file = "onnxruntime-1.19.0-cp39-cp39-win32.whl", hash = "sha256:947febd48405afcf526e45ccff97ff23b15e530434705f734870d22ae7fcf236"},
+ {file = "onnxruntime-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:f60be47eff5ee77fd28a466b0fd41d7debc42a32179d1ddb21e05d6067d7b48b"},
]
[package.dependencies]
coloredlogs = "*"
flatbuffers = "*"
-numpy = ">=1.21.6,<2.0"
+numpy = ">=1.21.6"
packaging = "*"
protobuf = "*"
sympy = "*"
@@ -2082,64 +2083,68 @@ numpy = [
[[package]]
name = "orjson"
-version = "3.10.6"
+version = "3.10.7"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.8"
files = [
- {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"},
- {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"},
- {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"},
- {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"},
- {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"},
- {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"},
- {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"},
- {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"},
- {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"},
- {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"},
- {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"},
- {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"},
- {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"},
- {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"},
- {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"},
- {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"},
- {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"},
- {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"},
- {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"},
- {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"},
- {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"},
- {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"},
- {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"},
- {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"},
- {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"},
- {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"},
- {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"},
- {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"},
- {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"},
- {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"},
- {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"},
- {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"},
- {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"},
+ {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"},
+ {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"},
+ {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"},
+ {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"},
+ {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"},
+ {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"},
+ {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"},
+ {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"},
+ {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"},
+ {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"},
+ {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"},
+ {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"},
+ {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"},
+ {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"},
+ {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"},
+ {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"},
+ {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"},
+ {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"},
+ {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"},
+ {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"},
+ {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"},
+ {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"},
+ {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"},
+ {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"},
+ {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"},
+ {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"},
+ {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"},
+ {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"},
+ {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"},
+ {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"},
+ {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"},
+ {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"},
+ {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"},
+ {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"},
+ {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"},
+ {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"},
+ {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"},
+ {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"},
+ {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"},
+ {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"},
+ {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"},
+ {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"},
+ {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"},
+ {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"},
+ {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"},
+ {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"},
+ {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"},
+ {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"},
+ {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"},
+ {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"},
+ {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"},
+ {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"},
+ {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"},
+ {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"},
+ {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"},
+ {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"},
+ {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"},
]
[[package]]
@@ -2369,54 +2374,54 @@ files = [
[[package]]
name = "pydantic"
-version = "1.10.17"
+version = "1.10.18"
description = "Data validation and settings management using python type hints"
optional = false
python-versions = ">=3.7"
files = [
- {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"},
- {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"},
- {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"},
- {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"},
- {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"},
- {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"},
- {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"},
- {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"},
- {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"},
- {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"},
- {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"},
- {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"},
- {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"},
- {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"},
- {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"},
- {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"},
- {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"},
- {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"},
- {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"},
- {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"},
- {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"},
- {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"},
- {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"},
- {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"},
- {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"},
- {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"},
- {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"},
- {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"},
- {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"},
- {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"},
- {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"},
- {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"},
- {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"},
- {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"},
- {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"},
- {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"},
- {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"},
- {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"},
- {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"},
- {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"},
- {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"},
- {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"},
- {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"},
+ {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"},
+ {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"},
+ {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"},
+ {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"},
+ {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"},
+ {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"},
+ {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"},
+ {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"},
+ {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"},
+ {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"},
+ {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"},
+ {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"},
+ {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"},
+ {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"},
+ {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"},
+ {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"},
+ {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"},
+ {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"},
+ {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"},
+ {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"},
+ {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"},
+ {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"},
+ {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"},
+ {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"},
+ {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"},
+ {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"},
+ {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"},
+ {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"},
+ {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"},
+ {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"},
+ {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"},
+ {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"},
+ {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"},
+ {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"},
+ {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"},
+ {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"},
+ {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"},
+ {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"},
+ {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"},
+ {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"},
+ {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"},
+ {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"},
+ {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"},
]
[package.dependencies]
@@ -2490,17 +2495,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
-version = "0.23.8"
+version = "0.24.0"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
files = [
- {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"},
- {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"},
+ {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"},
+ {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"},
]
[package.dependencies]
-pytest = ">=7.0.0,<9"
+pytest = ">=8.2,<9"
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"]
@@ -2508,13 +2513,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
[[package]]
name = "pytest-cov"
-version = "4.1.0"
+version = "5.0.0"
description = "Pytest plugin for measuring coverage."
optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
files = [
- {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
- {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
+ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"},
+ {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"},
]
[package.dependencies]
@@ -2522,7 +2527,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
-testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
+testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"]
[[package]]
name = "pytest-mock"
@@ -2811,13 +2816,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
[[package]]
name = "rich"
-version = "13.7.1"
+version = "13.8.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.7.0"
files = [
- {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
- {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
+ {file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
+ {file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
]
[package.dependencies]
@@ -2829,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
-version = "0.5.7"
+version = "0.6.3"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
- {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"},
- {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"},
- {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"},
- {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"},
- {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"},
- {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"},
- {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"},
- {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"},
- {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"},
- {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"},
- {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"},
- {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"},
+ {file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"},
+ {file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"},
+ {file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"},
+ {file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"},
+ {file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"},
+ {file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"},
+ {file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"},
+ {file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"},
+ {file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"},
+ {file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"},
+ {file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"},
+ {file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"},
]
[[package]]
@@ -3264,13 +3269,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
-version = "0.30.5"
+version = "0.30.6"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
- {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"},
- {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"},
+ {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"},
+ {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"},
]
[package.dependencies]
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index 05ac4618cdef2..a69fb33a8d50e 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -1,6 +1,6 @@
[tool.poetry]
name = "machine-learning"
-version = "1.112.1"
+version = "1.114.0"
description = ""
authors = ["Hau Tran "]
readme = "README.md"
diff --git a/machine-learning/start.sh b/machine-learning/start.sh
index 6b8e55a23657d..c3fda523df832 100755
--- a/machine-learning/start.sh
+++ b/machine-learning/start.sh
@@ -13,6 +13,7 @@ fi
: "${IMMICH_HOST:=[::]}"
: "${IMMICH_PORT:=3003}"
: "${MACHINE_LEARNING_WORKERS:=1}"
+: "${MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S:=2}"
gunicorn app.main:app \
-k app.config.CustomUvicornWorker \
@@ -20,4 +21,5 @@ gunicorn app.main:app \
-w "$MACHINE_LEARNING_WORKERS" \
-t "$MACHINE_LEARNING_WORKER_TIMEOUT" \
--log-config-json log_conf.json \
+ --keep-alive "$MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S" \
--graceful-timeout 0
diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml
index edb41510f0156..17c2830b48e26 100644
--- a/mobile/android/app/src/main/AndroidManifest.xml
+++ b/mobile/android/app/src/main/AndroidManifest.xml
@@ -24,7 +24,7 @@
+ android:largeHeap="true" android:enableOnBackInvokedCallback="false">
+ android:value="false" />
-
+
@@ -14,13 +16,14 @@
-
+
-
+
+
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index c7a5991212991..138b0e426d251 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -58,11 +58,11 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.112.1
+ 1.113.1
CFBundleSignature
????
CFBundleVersion
- 169
+ 172
FLTEnableImpeller
ITSAppUsesNonExemptEncryption
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index c7d078ceeafb4..c1740771d98c4 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -19,7 +19,7 @@ platform :ios do
desc "iOS Release"
lane :release do
increment_version_number(
- version_number: "1.112.1"
+ version_number: "1.114.0"
)
increment_build_number(
build_number: latest_testflight_build_number + 1,
diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart
index a84f9800019c3..1dda2b9a12a03 100644
--- a/mobile/lib/entities/store.entity.dart
+++ b/mobile/lib/entities/store.entity.dart
@@ -234,6 +234,8 @@ enum StoreKey {
primaryColor(128, type: String),
dynamicTheme(129, type: bool),
colorfulInterface(130, type: bool),
+
+ syncAlbums(131, type: bool),
;
const StoreKey(
diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart
new file mode 100644
index 0000000000000..5ef15167455df
--- /dev/null
+++ b/mobile/lib/models/backup/backup_candidate.model.dart
@@ -0,0 +1,19 @@
+import 'package:photo_manager/photo_manager.dart';
+
+class BackupCandidate {
+ BackupCandidate({required this.asset, required this.albumNames});
+
+ AssetEntity asset;
+ List albumNames;
+
+ @override
+ int get hashCode => asset.hashCode;
+
+ @override
+ bool operator ==(Object other) {
+ if (other is! BackupCandidate) {
+ return false;
+ }
+ return asset == other.asset;
+ }
+}
diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart
index bb693a5b75f7a..d829f411fc355 100644
--- a/mobile/lib/models/backup/backup_state.model.dart
+++ b/mobile/lib/models/backup/backup_state.model.dart
@@ -2,7 +2,7 @@
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
-import 'package:photo_manager/photo_manager.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -41,7 +41,7 @@ class BackUpState {
final Set excludedBackupAlbums;
/// Assets that are not overlapping in selected backup albums and excluded backup albums
- final Set allUniqueAssets;
+ final Set allUniqueAssets;
/// All assets from the selected albums that have been backup
final Set selectedAlbumsBackupAssetsIds;
@@ -94,7 +94,7 @@ class BackUpState {
List? availableAlbums,
Set? selectedBackupAlbums,
Set? excludedBackupAlbums,
- Set? allUniqueAssets,
+ Set? allUniqueAssets,
Set? selectedAlbumsBackupAssetsIds,
CurrentUploadAsset? currentUploadAsset,
}) {
diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart
new file mode 100644
index 0000000000000..045715e8cbbda
--- /dev/null
+++ b/mobile/lib/models/backup/success_upload_asset.model.dart
@@ -0,0 +1,42 @@
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
+
+class SuccessUploadAsset {
+ final BackupCandidate candidate;
+ final String remoteAssetId;
+ final bool isDuplicate;
+
+ SuccessUploadAsset({
+ required this.candidate,
+ required this.remoteAssetId,
+ required this.isDuplicate,
+ });
+
+ SuccessUploadAsset copyWith({
+ BackupCandidate? candidate,
+ String? remoteAssetId,
+ bool? isDuplicate,
+ }) {
+ return SuccessUploadAsset(
+ candidate: candidate ?? this.candidate,
+ remoteAssetId: remoteAssetId ?? this.remoteAssetId,
+ isDuplicate: isDuplicate ?? this.isDuplicate,
+ );
+ }
+
+ @override
+ String toString() =>
+ 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)';
+
+ @override
+ bool operator ==(covariant SuccessUploadAsset other) {
+ if (identical(this, other)) return true;
+
+ return other.candidate == candidate &&
+ other.remoteAssetId == remoteAssetId &&
+ other.isDuplicate == isDuplicate;
+ }
+
+ @override
+ int get hashCode =>
+ candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode;
+}
diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart
index 9f3e387755e85..8dccece325d8f 100644
--- a/mobile/lib/pages/backup/backup_album_selection.page.dart
+++ b/mobile/lib/pages/backup/backup_album_selection.page.dart
@@ -4,19 +4,24 @@ import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
+import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart';
import 'package:immich_mobile/widgets/backup/album_info_card.dart';
import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart';
import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart';
+import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart';
@RoutePage()
class BackupAlbumSelectionPage extends HookConsumerWidget {
const BackupAlbumSelectionPage({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
- // final availableAlbums = ref.watch(backupProvider).availableAlbums;
final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums;
final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums;
+ final enableSyncUploadAlbum =
+ useAppSettingsState(AppSettingsEnum.syncAlbums);
final isDarkTheme = context.isDarkTheme;
final albums = ref.watch(backupProvider).availableAlbums;
@@ -144,47 +149,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
}).toSet();
}
- // buildSearchBar() {
- // return Padding(
- // padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0),
- // child: TextFormField(
- // onChanged: (searchValue) {
- // // if (searchValue.isEmpty) {
- // // albums = availableAlbums;
- // // } else {
- // // albums.value = availableAlbums
- // // .where(
- // // (album) => album.name
- // // .toLowerCase()
- // // .contains(searchValue.toLowerCase()),
- // // )
- // // .toList();
- // // }
- // },
- // decoration: InputDecoration(
- // contentPadding: const EdgeInsets.symmetric(
- // horizontal: 8.0,
- // vertical: 8.0,
- // ),
- // hintText: "Search",
- // hintStyle: TextStyle(
- // color: isDarkTheme ? Colors.white : Colors.grey,
- // fontSize: 14.0,
- // ),
- // prefixIcon: const Icon(
- // Icons.search,
- // color: Colors.grey,
- // ),
- // border: OutlineInputBorder(
- // borderRadius: BorderRadius.circular(10),
- // borderSide: BorderSide.none,
- // ),
- // filled: true,
- // fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200],
- // ),
- // ),
- // );
- // }
+ handleSyncAlbumToggle(bool isEnable) async {
+ if (isEnable) {
+ await ref.read(albumProvider.notifier).getAllAlbums();
+ for (final album in selectedBackupAlbums) {
+ await ref.read(albumProvider.notifier).createSyncAlbum(album.name);
+ }
+ }
+ }
return Scaffold(
appBar: AppBar(
@@ -226,6 +198,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget {
),
),
+ SettingsSwitchListTile(
+ valueNotifier: enableSyncUploadAlbum,
+ title: "sync_albums".tr(),
+ subtitle: "sync_upload_album_setting_subtitle".tr(),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16),
+ titleStyle: context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.bold,
+ ),
+ subtitleStyle: context.textTheme.labelLarge?.copyWith(
+ color: context.colorScheme.primary,
+ ),
+ onChanged: handleSyncAlbumToggle,
+ ),
+
ListTile(
title: Text(
"backup_album_selection_page_albums_device".tr(
diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart
index 51282d8dd6ad4..1fd860520d5c7 100644
--- a/mobile/lib/pages/common/create_album.page.dart
+++ b/mobile/lib/pages/common/create_album.page.dart
@@ -52,6 +52,7 @@ class CreateAlbumPage extends HookConsumerWidget {
if (albumTitleController.text.isEmpty) {
albumTitleController.text = 'create_album_page_untitled'.tr();
+ isAlbumTitleEmpty.value = false;
ref
.watch(albumTitleProvider.notifier)
.setAlbumTitle('create_album_page_untitled'.tr());
@@ -191,6 +192,7 @@ class CreateAlbumPage extends HookConsumerWidget {
}
createNonSharedAlbum() async {
+ onBackgroundTapped();
var newAlbum = await ref.watch(albumProvider.notifier).createAlbum(
ref.watch(albumTitleProvider),
selectedAssets.value,
@@ -238,15 +240,16 @@ class CreateAlbumPage extends HookConsumerWidget {
),
if (!isSharedAlbum)
TextButton(
- onPressed: albumTitleController.text.isNotEmpty &&
- selectedAssets.value.isNotEmpty
+ onPressed: albumTitleController.text.isNotEmpty
? createNonSharedAlbum
: null,
child: Text(
'create_shared_album_page_create'.tr(),
style: TextStyle(
fontWeight: FontWeight.bold,
- color: context.primaryColor,
+ color: albumTitleController.text.isNotEmpty
+ ? context.primaryColor
+ : context.themeData.disabledColor,
),
),
),
diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart
index 8a21cdf76908f..729b59ded5911 100644
--- a/mobile/lib/pages/editing/crop.page.dart
+++ b/mobile/lib/pages/editing/crop.page.dart
@@ -1,9 +1,12 @@
import 'package:flutter/material.dart';
import 'package:crop_image/crop_image.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart';
+import 'package:immich_mobile/entities/asset.entity.dart';
import 'edit.page.dart';
+import 'package:easy_localization/easy_localization.dart';
import 'package:auto_route/auto_route.dart';
/// A widget for cropping an image.
@@ -14,7 +17,8 @@ import 'package:auto_route/auto_route.dart';
@RoutePage()
class CropImagePage extends HookWidget {
final Image image;
- const CropImagePage({super.key, required this.image});
+ final Asset asset;
+ const CropImagePage({super.key, required this.image, required this.asset});
@override
Widget build(BuildContext context) {
@@ -23,29 +27,37 @@ class CropImagePage extends HookWidget {
return Scaffold(
appBar: AppBar(
- backgroundColor: Theme.of(context).bottomAppBarTheme.color,
- leading: CloseButton(color: Theme.of(context).iconTheme.color),
+ backgroundColor: context.scaffoldBackgroundColor,
+ title: Text("crop".tr()),
+ leading: CloseButton(color: context.primaryColor),
actions: [
IconButton(
icon: Icon(
Icons.done_rounded,
- color: Theme.of(context).iconTheme.color,
+ color: context.primaryColor,
size: 24,
),
onPressed: () async {
final croppedImage = await cropController.croppedImage();
- context.pushRoute(EditImageRoute(image: croppedImage));
+ context.pushRoute(
+ EditImageRoute(
+ asset: asset,
+ image: croppedImage,
+ isEdited: true,
+ ),
+ );
},
),
],
),
+ backgroundColor: context.scaffoldBackgroundColor,
body: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
return Column(
children: [
Container(
padding: const EdgeInsets.only(top: 20),
- width: double.infinity,
+ width: constraints.maxWidth * 0.9,
height: constraints.maxHeight * 0.6,
child: CropImage(
controller: cropController,
@@ -57,7 +69,7 @@ class CropImagePage extends HookWidget {
child: Container(
width: double.infinity,
decoration: BoxDecoration(
- color: Theme.of(context).bottomAppBarTheme.color,
+ color: context.scaffoldBackgroundColor,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(20),
topRight: Radius.circular(20),
@@ -188,7 +200,7 @@ class _AspectRatioButton extends StatelessWidget {
icon: Icon(
iconData,
color: aspectRatio.value == ratio
- ? Colors.indigo
+ ? context.primaryColor
: Theme.of(context).iconTheme.color,
),
onPressed: () {
@@ -197,7 +209,7 @@ class _AspectRatioButton extends StatelessWidget {
cropController.aspectRatio = ratio;
},
),
- Text(label, style: Theme.of(context).textTheme.bodyMedium),
+ Text(label, style: context.textTheme.displayMedium),
],
);
}
diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart
index 22fb345e0f706..c81e84877b208 100644
--- a/mobile/lib/pages/editing/edit.page.dart
+++ b/mobile/lib/pages/editing/edit.page.dart
@@ -7,12 +7,15 @@ import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
+import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:auto_route/auto_route.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:easy_localization/easy_localization.dart';
+import 'package:path/path.dart' as p;
/// A stateless widget that provides functionality for editing an image.
///
@@ -24,18 +27,16 @@ import 'package:immich_mobile/providers/album/album.provider.dart';
@immutable
@RoutePage()
class EditImagePage extends ConsumerWidget {
- final Asset? asset;
- final Image? image;
+ final Asset asset;
+ final Image image;
+ final bool isEdited;
const EditImagePage({
super.key,
- this.image,
- this.asset,
- }) : assert(
- (image != null && asset == null) || (image == null && asset != null),
- 'Must supply one of asset or image',
- );
-
+ required this.asset,
+ required this.image,
+ required this.isEdited,
+ });
Future _imageToUint8List(Image image) async {
final Completer completer = Completer();
image.image.resolve(const ImageConfiguration()).addListener(
@@ -58,83 +59,102 @@ class EditImagePage extends ConsumerWidget {
return completer.future;
}
+ Future _saveEditedImage(
+ BuildContext context,
+ Asset asset,
+ Image image,
+ WidgetRef ref,
+ ) async {
+ try {
+ final Uint8List imageData = await _imageToUint8List(image);
+ await PhotoManager.editor.saveImage(
+ imageData,
+ title: "${p.withoutExtension(asset.fileName)}_edited.jpg",
+ );
+ await ref.read(albumProvider.notifier).getDeviceAlbums();
+ Navigator.of(context).popUntil((route) => route.isFirst);
+ ImmichToast.show(
+ durationInSecond: 3,
+ context: context,
+ msg: 'Image Saved!',
+ gravity: ToastGravity.CENTER,
+ );
+ } catch (e) {
+ ImmichToast.show(
+ durationInSecond: 6,
+ context: context,
+ msg: "error_saving_image".tr(args: [e.toString()]),
+ gravity: ToastGravity.CENTER,
+ );
+ }
+ }
+
@override
Widget build(BuildContext context, WidgetRef ref) {
- final ImageProvider provider = (asset != null)
- ? ImmichImage.imageProvider(asset: asset!)
- : (image != null)
- ? image!.image
- : throw Exception('Invalid image source type');
-
- final Image imageWidget = (asset != null)
- ? Image(image: ImmichImage.imageProvider(asset: asset!))
- : (image != null)
- ? image!
- : throw Exception('Invalid image source type');
+ final Image imageWidget =
+ Image(image: ImmichImage.imageProvider(asset: asset));
return Scaffold(
appBar: AppBar(
- backgroundColor: Theme.of(context).appBarTheme.backgroundColor,
+ title: Text("edit_image_title".tr()),
+ backgroundColor: context.scaffoldBackgroundColor,
leading: IconButton(
icon: Icon(
Icons.close_rounded,
- color: Theme.of(context).iconTheme.color,
+ color: context.primaryColor,
size: 24,
),
onPressed: () =>
Navigator.of(context).popUntil((route) => route.isFirst),
),
actions: [
- if (image != null)
- TextButton(
- onPressed: () async {
- try {
- final Uint8List imageData = await _imageToUint8List(image!);
- ImmichToast.show(
- durationInSecond: 3,
- context: context,
- msg: 'Image Saved!',
- gravity: ToastGravity.CENTER,
- );
-
- await PhotoManager.editor.saveImage(
- imageData,
- title: '${asset!.fileName}_edited.jpg',
- );
- await ref.read(albumProvider.notifier).getDeviceAlbums();
- Navigator.of(context).popUntil((route) => route.isFirst);
- } catch (e) {
- ImmichToast.show(
- durationInSecond: 6,
- context: context,
- msg: 'Error: ${e.toString()}',
- gravity: ToastGravity.BOTTOM,
- );
- }
- },
- child: Text(
- 'Save to gallery',
- style: Theme.of(context).textTheme.displayMedium,
+ TextButton(
+ onPressed: isEdited
+ ? () => _saveEditedImage(context, asset, image, ref)
+ : null,
+ child: Text(
+ "save_to_gallery".tr(),
+ style: TextStyle(
+ color: isEdited ? context.primaryColor : Colors.grey,
),
),
+ ),
],
),
- body: Column(
- children: [
- Expanded(
- child: Image(image: provider),
+ backgroundColor: context.scaffoldBackgroundColor,
+ body: Center(
+ child: ConstrainedBox(
+ constraints: BoxConstraints(
+ maxHeight: MediaQuery.of(context).size.height * 0.7,
+ maxWidth: MediaQuery.of(context).size.width * 0.9,
),
- Container(
- height: 80,
- color: Theme.of(context).bottomAppBarTheme.color,
+ child: Container(
+ decoration: BoxDecoration(
+ borderRadius: BorderRadius.circular(7),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.2),
+ spreadRadius: 2,
+ blurRadius: 10,
+ offset: const Offset(0, 3),
+ ),
+ ],
+ ),
+ child: ClipRRect(
+ borderRadius: BorderRadius.circular(7),
+ child: Image(
+ image: image.image,
+ fit: BoxFit.contain,
+ ),
+ ),
),
- ],
+ ),
),
bottomNavigationBar: Container(
- height: 80,
- margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10),
+ height: 70,
+ margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10),
decoration: BoxDecoration(
- color: Theme.of(context).bottomAppBarTheme.color,
+ color: context.scaffoldBackgroundColor,
borderRadius: BorderRadius.circular(30),
),
child: Column(
@@ -146,12 +166,15 @@ class EditImagePage extends ConsumerWidget {
? Icons.crop_rotate_rounded
: Icons.crop_rotate_rounded,
color: Theme.of(context).iconTheme.color,
+ size: 25,
),
onPressed: () {
- context.pushRoute(CropImageRoute(image: imageWidget));
+ context.pushRoute(
+ CropImageRoute(asset: asset, image: imageWidget),
+ );
},
),
- Text('Crop', style: Theme.of(context).textTheme.displayMedium),
+ Text("crop".tr(), style: context.textTheme.displayMedium),
],
),
),
diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart
index b305b5fc534d6..8045ae649fc9a 100644
--- a/mobile/lib/pages/login/login.page.dart
+++ b/mobile/lib/pages/login/login.page.dart
@@ -29,7 +29,7 @@ class LoginPage extends HookConsumerWidget {
);
return Scaffold(
- body: const LoginForm(),
+ body: LoginForm(),
bottomNavigationBar: SafeArea(
child: Padding(
padding: const EdgeInsets.only(bottom: 16.0),
diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart
index 8251d5e66bf33..ed9dc07f5e5c0 100644
--- a/mobile/lib/providers/album/album.provider.dart
+++ b/mobile/lib/providers/album/album.provider.dart
@@ -23,6 +23,7 @@ class AlbumNotifier extends StateNotifier> {
});
_streamSub = query.watch().listen((data) => state = data);
}
+
final AlbumService _albumService;
late final StreamSubscription> _streamSub;
@@ -41,6 +42,23 @@ class AlbumNotifier extends StateNotifier> {
) =>
_albumService.createAlbum(albumTitle, assets, []);
+ Future getAlbumByName(String albumName, {bool remoteOnly = false}) =>
+ _albumService.getAlbumByName(albumName, remoteOnly);
+
+ /// Create an album on the server with the same name as the selected album for backup
+ /// First this will check if the album already exists on the server with name
+ /// If it does not exist, it will create the album on the server
+ Future createSyncAlbum(
+ String albumName,
+ ) async {
+ final album = await getAlbumByName(albumName, remoteOnly: true);
+ if (album != null) {
+ return;
+ }
+
+ await createAlbum(albumName, {});
+ }
+
@override
void dispose() {
_streamSub.cancel();
diff --git a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
index ee45e6bc5e56b..631011f200bbd 100644
--- a/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
+++ b/mobile/lib/providers/asset_viewer/image_viewer_page_state.provider.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fluttertoast/fluttertoast.dart';
@@ -31,19 +33,21 @@ class ImageViewerStateNotifier extends StateNotifier {
ImmichToast.show(
context: context,
- msg: 'image_viewer_page_state_provider_download_started'.tr(),
+ msg: 'download_started'.tr(),
toastType: ToastType.info,
gravity: ToastGravity.BOTTOM,
);
- bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
+ bool isSuccess = await _imageViewerService.downloadAsset(asset);
if (isSuccess) {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
ImmichToast.show(
context: context,
- msg: 'image_viewer_page_state_provider_download_success'.tr(),
+ msg: Platform.isAndroid
+ ? 'download_sucess_android'.tr()
+ : 'download_sucess'.tr(),
toastType: ToastType.success,
gravity: ToastGravity.BOTTOM,
);
@@ -52,7 +56,7 @@ class ImageViewerStateNotifier extends StateNotifier {
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
ImmichToast.show(
context: context,
- msg: 'image_viewer_page_state_provider_download_error'.tr(),
+ msg: 'download_error'.tr(),
toastType: ToastType.error,
gravity: ToastGravity.BOTTOM,
);
diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart
index 5846bb78cc3e8..b56e71b11b3f6 100644
--- a/mobile/lib/providers/authentication.provider.dart
+++ b/mobile/lib/providers/authentication.provider.dart
@@ -170,8 +170,10 @@ class AuthenticationNotifier extends StateNotifier {
UserPreferencesResponseDto? userPreferences;
try {
final responses = await Future.wait([
- _apiService.usersApi.getMyUser(),
- _apiService.usersApi.getMyPreferences(),
+ _apiService.usersApi.getMyUser().timeout(const Duration(seconds: 7)),
+ _apiService.usersApi
+ .getMyPreferences()
+ .timeout(const Duration(seconds: 7)),
]);
userResponse = responses[0] as UserAdminResponseDto;
userPreferences = responses[1] as UserPreferencesResponseDto;
@@ -190,6 +192,9 @@ class AuthenticationNotifier extends StateNotifier {
error,
stackTrace,
);
+ debugPrint(
+ "Error getting user information from the server [CATCH ALL] $error $stackTrace",
+ );
}
// If the user information is successfully retrieved, update the store
diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart
index 58027e3b941e0..02f1f07904f97 100644
--- a/mobile/lib/providers/backup/backup.provider.dart
+++ b/mobile/lib/providers/backup/backup.provider.dart
@@ -2,13 +2,16 @@ import 'dart:io';
import 'package:cancellation_token_http/http.dart';
import 'package:collection/collection.dart';
+import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
+import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/services/backup.service.dart';
@@ -290,8 +293,8 @@ class BackupNotifier extends StateNotifier {
///
Future _updateBackupAssetCount() async {
final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds();
- final Set assetsFromSelectedAlbums = {};
- final Set assetsFromExcludedAlbums = {};
+ final Set assetsFromSelectedAlbums = {};
+ final Set assetsFromExcludedAlbums = {};
for (final album in state.selectedBackupAlbums) {
final assetCount = await album.albumEntity.assetCountAsync;
@@ -304,7 +307,27 @@ class BackupNotifier extends StateNotifier {
start: 0,
end: assetCount,
);
- assetsFromSelectedAlbums.addAll(assets);
+
+ // Add album's name to the asset info
+ for (final asset in assets) {
+ List albumNames = [album.name];
+
+ final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull(
+ (a) => a.asset.id == asset.id,
+ );
+
+ if (existingAsset != null) {
+ albumNames.addAll(existingAsset.albumNames);
+ assetsFromSelectedAlbums.remove(existingAsset);
+ }
+
+ assetsFromSelectedAlbums.add(
+ BackupCandidate(
+ asset: asset,
+ albumNames: albumNames,
+ ),
+ );
+ }
}
for (final album in state.excludedBackupAlbums) {
@@ -318,11 +341,17 @@ class BackupNotifier extends StateNotifier {
start: 0,
end: assetCount,
);
- assetsFromExcludedAlbums.addAll(assets);
+
+ for (final asset in assets) {
+ assetsFromExcludedAlbums.add(
+ BackupCandidate(asset: asset, albumNames: [album.name]),
+ );
+ }
}
- final Set allUniqueAssets =
+ final Set allUniqueAssets =
assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums);
+
final allAssetsInDatabase = await _backupService.getDeviceBackupAsset();
if (allAssetsInDatabase == null) {
@@ -331,14 +360,14 @@ class BackupNotifier extends StateNotifier {
// Find asset that were backup from selected albums
final Set selectedAlbumsBackupAssets =
- Set.from(allUniqueAssets.map((e) => e.id));
+ Set.from(allUniqueAssets.map((e) => e.asset.id));
selectedAlbumsBackupAssets
.removeWhere((assetId) => !allAssetsInDatabase.contains(assetId));
// Remove duplicated asset from all unique assets
allUniqueAssets.removeWhere(
- (asset) => duplicatedAssetIds.contains(asset.id),
+ (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
);
if (allUniqueAssets.isEmpty) {
@@ -433,10 +462,10 @@ class BackupNotifier extends StateNotifier {
return;
}
- Set assetsWillBeBackup = Set.from(state.allUniqueAssets);
+ Set assetsWillBeBackup = Set.from(state.allUniqueAssets);
// Remove item that has already been backed up
for (final assetId in state.allAssetsInDatabase) {
- assetsWillBeBackup.removeWhere((e) => e.id == assetId);
+ assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId);
}
if (assetsWillBeBackup.isEmpty) {
@@ -456,11 +485,11 @@ class BackupNotifier extends StateNotifier {
await _backupService.backupAsset(
assetsWillBeBackup,
state.cancelToken,
- pmProgressHandler,
- _onAssetUploaded,
- _onUploadProgress,
- _onSetCurrentBackupAsset,
- _onBackupError,
+ pmProgressHandler: pmProgressHandler,
+ onSuccess: _onAssetUploaded,
+ onProgress: _onUploadProgress,
+ onCurrentAsset: _onSetCurrentBackupAsset,
+ onError: _onBackupError,
);
await notifyBackgroundServiceCanRun();
} else {
@@ -497,34 +526,36 @@ class BackupNotifier extends StateNotifier {
);
}
- void _onAssetUploaded(
- String deviceAssetId,
- String deviceId,
- bool isDuplicated,
- ) {
- if (isDuplicated) {
+ void _onAssetUploaded(SuccessUploadAsset result) async {
+ if (result.isDuplicate) {
state = state.copyWith(
allUniqueAssets: state.allUniqueAssets
- .where((asset) => asset.id != deviceAssetId)
+ .where(
+ (candidate) => candidate.asset.id != result.candidate.asset.id,
+ )
.toSet(),
);
} else {
state = state.copyWith(
selectedAlbumsBackupAssetsIds: {
...state.selectedAlbumsBackupAssetsIds,
- deviceAssetId,
+ result.candidate.asset.id,
},
- allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId],
+ allAssetsInDatabase: [
+ ...state.allAssetsInDatabase,
+ result.candidate.asset.id,
+ ],
);
}
if (state.allUniqueAssets.length -
state.selectedAlbumsBackupAssetsIds.length ==
0) {
- final latestAssetBackup =
- state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce(
- (v, e) => e.isAfter(v) ? e : v,
- );
+ final latestAssetBackup = state.allUniqueAssets
+ .map((candidate) => candidate.asset.modifiedDateTime)
+ .reduce(
+ (v, e) => e.isAfter(v) ? e : v,
+ );
state = state.copyWith(
selectedBackupAlbums: state.selectedBackupAlbums
.map((e) => e.copyWith(lastBackup: latestAssetBackup))
diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart
index b446711226324..a76b56fea7f8a 100644
--- a/mobile/lib/providers/backup/manual_upload.provider.dart
+++ b/mobile/lib/providers/backup/manual_upload.provider.dart
@@ -6,6 +6,8 @@ import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/widgets.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
+import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/services/background.service.dart';
import 'package:immich_mobile/models/backup/backup_state.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -22,6 +24,7 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart';
import 'package:immich_mobile/services/local_notification.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
+import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart';
@@ -31,6 +34,7 @@ final manualUploadProvider =
return ManualUploadNotifier(
ref.watch(localNotificationService),
ref.watch(backupProvider.notifier),
+ ref.watch(backupServiceProvider),
ref,
);
});
@@ -39,11 +43,13 @@ class ManualUploadNotifier extends StateNotifier {
final Logger _log = Logger("ManualUploadNotifier");
final LocalNotificationService _localNotificationService;
final BackupNotifier _backupProvider;
+ final BackupService _backupService;
final Ref ref;
ManualUploadNotifier(
this._localNotificationService,
this._backupProvider,
+ this._backupService,
this.ref,
) : super(
ManualUploadState(
@@ -115,11 +121,7 @@ class ManualUploadNotifier extends StateNotifier {
}
}
- void _onAssetUploaded(
- String deviceAssetId,
- String deviceId,
- bool isDuplicated,
- ) {
+ void _onAssetUploaded(SuccessUploadAsset result) {
state = state.copyWith(successfulUploads: state.successfulUploads + 1);
_backupProvider.updateDiskInfo();
}
@@ -209,9 +211,23 @@ class ManualUploadNotifier extends StateNotifier {
);
}
- Set allUploadAssets = allAssetsFromDevice.nonNulls.toSet();
+ final selectedBackupAlbums =
+ _backupService.selectedAlbumsQuery().findAllSync();
+ final excludedBackupAlbums =
+ _backupService.excludedAlbumsQuery().findAllSync();
- if (allUploadAssets.isEmpty) {
+ // Get candidates from selected albums and excluded albums
+ Set candidates =
+ await _backupService.buildUploadCandidates(
+ selectedBackupAlbums,
+ excludedBackupAlbums,
+ );
+
+ // Extrack candidate from allAssetsFromDevice.nonNulls
+ final uploadAssets = candidates
+ .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset));
+
+ if (uploadAssets.isEmpty) {
debugPrint("[_startUpload] No Assets to upload - Abort Process");
_backupProvider.updateBackupProgress(BackUpProgressEnum.idle);
return false;
@@ -221,7 +237,7 @@ class ManualUploadNotifier extends StateNotifier {
progressInPercentage: 0,
progressInFileSize: "0 B / 0 B",
progressInFileSpeed: 0,
- totalAssetsToUpload: allUploadAssets.length,
+ totalAssetsToUpload: uploadAssets.length,
successfulUploads: 0,
currentAssetIndex: 0,
currentUploadAsset: CurrentUploadAsset(
@@ -250,13 +266,13 @@ class ManualUploadNotifier extends StateNotifier {
final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null;
final bool ok = await ref.read(backupServiceProvider).backupAsset(
- allUploadAssets,
+ uploadAssets,
state.cancelToken,
- pmProgressHandler,
- _onAssetUploaded,
- _onProgress,
- _onSetCurrentBackupAsset,
- _onAssetUploadError,
+ pmProgressHandler: pmProgressHandler,
+ onSuccess: _onAssetUploaded,
+ onProgress: _onProgress,
+ onCurrentAsset: _onSetCurrentBackupAsset,
+ onError: _onAssetUploadError,
);
// Close detailed notification
diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart
index a4259676c7a6d..90fc4cb0fe96c 100644
--- a/mobile/lib/routing/router.gr.dart
+++ b/mobile/lib/routing/router.gr.dart
@@ -613,12 +613,14 @@ class CropImageRoute extends PageRouteInfo {
CropImageRoute({
Key? key,
required Image image,
+ required Asset asset,
List? children,
}) : super(
CropImageRoute.name,
args: CropImageRouteArgs(
key: key,
image: image,
+ asset: asset,
),
initialChildren: children,
);
@@ -632,6 +634,7 @@ class CropImageRoute extends PageRouteInfo {
return CropImagePage(
key: args.key,
image: args.image,
+ asset: args.asset,
);
},
);
@@ -641,15 +644,18 @@ class CropImageRouteArgs {
const CropImageRouteArgs({
this.key,
required this.image,
+ required this.asset,
});
final Key? key;
final Image image;
+ final Asset asset;
+
@override
String toString() {
- return 'CropImageRouteArgs{key: $key, image: $image}';
+ return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}';
}
}
@@ -658,15 +664,17 @@ class CropImageRouteArgs {
class EditImageRoute extends PageRouteInfo {
EditImageRoute({
Key? key,
- Image? image,
- Asset? asset,
+ required Asset asset,
+ required Image image,
+ required bool isEdited,
List? children,
}) : super(
EditImageRoute.name,
args: EditImageRouteArgs(
key: key,
- image: image,
asset: asset,
+ image: image,
+ isEdited: isEdited,
),
initialChildren: children,
);
@@ -676,12 +684,12 @@ class EditImageRoute extends PageRouteInfo {
static PageInfo page = PageInfo(
name,
builder: (data) {
- final args = data.argsAs(
- orElse: () => const EditImageRouteArgs());
+ final args = data.argsAs();
return EditImagePage(
key: args.key,
- image: args.image,
asset: args.asset,
+ image: args.image,
+ isEdited: args.isEdited,
);
},
);
@@ -690,19 +698,22 @@ class EditImageRoute extends PageRouteInfo {
class EditImageRouteArgs {
const EditImageRouteArgs({
this.key,
- this.image,
- this.asset,
+ required this.asset,
+ required this.image,
+ required this.isEdited,
});
final Key? key;
- final Image? image;
+ final Asset asset;
- final Asset? asset;
+ final Image image;
+
+ final bool isEdited;
@override
String toString() {
- return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}';
+ return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}';
}
}
diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart
index c2494680c7da5..ef56f9bf6c12a 100644
--- a/mobile/lib/services/album.service.dart
+++ b/mobile/lib/services/album.service.dart
@@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
-import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/entities/album.entity.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@@ -28,7 +27,6 @@ final albumServiceProvider = Provider(
ref.watch(userServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(dbProvider),
- ref.watch(backupServiceProvider),
),
);
@@ -37,7 +35,6 @@ class AlbumService {
final UserService _userService;
final SyncService _syncService;
final Isar _db;
- final BackupService _backupService;
final Logger _log = Logger('AlbumService');
Completer _localCompleter = Completer()..complete(false);
Completer _remoteCompleter = Completer()..complete(false);
@@ -47,9 +44,15 @@ class AlbumService {
this._userService,
this._syncService,
this._db,
- this._backupService,
);
+ QueryBuilder
+ selectedAlbumsQuery() =>
+ _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select);
+ QueryBuilder
+ excludedAlbumsQuery() =>
+ _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
+
/// Checks all selected device albums for changes of albums and their assets
/// Updates the local database and returns `true` if there were any changes
Future refreshDeviceAlbums() async {
@@ -63,9 +66,9 @@ class AlbumService {
bool changes = false;
try {
final List excludedIds =
- await _backupService.excludedAlbumsQuery().idProperty().findAll();
+ await excludedAlbumsQuery().idProperty().findAll();
final List selectedIds =
- await _backupService.selectedAlbumsQuery().idProperty().findAll();
+ await selectedAlbumsQuery().idProperty().findAll();
if (selectedIds.isEmpty) {
final numLocal = await _db.albums.where().localIdIsNotNull().count();
if (numLocal > 0) {
@@ -441,4 +444,33 @@ class AlbumService {
return false;
}
}
+
+ Future getAlbumByName(String name, bool remoteOnly) async {
+ return _db.albums
+ .filter()
+ .optional(remoteOnly, (q) => q.localIdIsNull())
+ .nameEqualTo(name)
+ .sharedEqualTo(false)
+ .findFirst();
+ }
+
+ ///
+ /// Add the uploaded asset to the selected albums
+ ///
+ Future syncUploadAlbums(
+ List albumNames,
+ List assetIds,
+ ) async {
+ for (final albumName in albumNames) {
+ Album? album = await getAlbumByName(albumName, true);
+ album ??= await createAlbum(albumName, []);
+
+ if (album != null && album.remoteId != null) {
+ await _apiService.albumsApi.addAssetsToAlbum(
+ album.remoteId!,
+ BulkIdsDto(ids: assetIds),
+ );
+ }
+ }
+ }
}
diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart
index bd254032159c0..8f773e1bb33a9 100644
--- a/mobile/lib/services/app_settings.service.dart
+++ b/mobile/lib/services/app_settings.service.dart
@@ -76,6 +76,7 @@ enum AppSettingsEnum {
false,
),
enableHapticFeedback(StoreKey.enableHapticFeedback, null, true),
+ syncAlbums(StoreKey.syncAlbums, null, false),
;
const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue);
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index d37133a63b9c7..c4f258e259129 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -2,15 +2,19 @@
import 'dart:async';
+import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/entities/etag.entity.dart';
import 'package:immich_mobile/entities/exif_info.entity.dart';
import 'package:immich_mobile/entities/user.entity.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/sync.service.dart';
import 'package:immich_mobile/services/user.service.dart';
import 'package:isar/isar.dart';
@@ -23,6 +27,8 @@ final assetServiceProvider = Provider(
ref.watch(apiServiceProvider),
ref.watch(syncServiceProvider),
ref.watch(userServiceProvider),
+ ref.watch(backupServiceProvider),
+ ref.watch(albumServiceProvider),
ref.watch(dbProvider),
),
);
@@ -31,6 +37,8 @@ class AssetService {
final ApiService _apiService;
final SyncService _syncService;
final UserService _userService;
+ final BackupService _backupService;
+ final AlbumService _albumService;
final log = Logger('AssetService');
final Isar _db;
@@ -38,6 +46,8 @@ class AssetService {
this._apiService,
this._syncService,
this._userService,
+ this._backupService,
+ this._albumService,
this._db,
);
@@ -284,4 +294,52 @@ class AssetService {
return Future.value(null);
}
}
+
+ Future syncUploadedAssetToAlbums() async {
+ try {
+ final [selectedAlbums, excludedAlbums] = await Future.wait([
+ _backupService.selectedAlbumsQuery().findAll(),
+ _backupService.excludedAlbumsQuery().findAll(),
+ ]);
+
+ final candidates = await _backupService.buildUploadCandidates(
+ selectedAlbums,
+ excludedAlbums,
+ useTimeFilter: false,
+ );
+
+ await refreshRemoteAssets();
+ final remoteAssets = await _db.assets
+ .where()
+ .localIdIsNotNull()
+ .filter()
+ .remoteIdIsNotNull()
+ .findAll();
+
+ /// Map
+ Map> assetToAlbums = {};
+
+ for (BackupCandidate candidate in candidates) {
+ final asset = remoteAssets.firstWhereOrNull(
+ (a) => a.localId == candidate.asset.id,
+ );
+
+ if (asset != null) {
+ for (final albumName in candidate.albumNames) {
+ assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!);
+ }
+ }
+ }
+
+ // Upload assets to albums
+ for (final entry in assetToAlbums.entries) {
+ final albumName = entry.key;
+ final assetIds = entry.value;
+
+ await _albumService.syncUploadAlbums([albumName], assetIds);
+ }
+ } catch (error, stack) {
+ log.severe("Error while syncing uploaded asset to albums", error, stack);
+ }
+ }
}
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index ba8f5c01ed963..fc3feb174d582 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -10,6 +10,10 @@ import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/main.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
+import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
+import 'package:immich_mobile/services/album.service.dart';
+import 'package:immich_mobile/services/hash.service.dart';
import 'package:immich_mobile/services/localization.service.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
@@ -18,6 +22,9 @@ import 'package:immich_mobile/services/backup.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/services/api.service.dart';
+import 'package:immich_mobile/services/partner.service.dart';
+import 'package:immich_mobile/services/sync.service.dart';
+import 'package:immich_mobile/services/user.service.dart';
import 'package:immich_mobile/utils/backup_progress.dart';
import 'package:immich_mobile/utils/diff.dart';
import 'package:immich_mobile/utils/http_ssl_cert_override.dart';
@@ -342,11 +349,20 @@ class BackgroundService {
Future _onAssetsChanged() async {
final Isar db = await loadDb();
+ HttpOverrides.global = HttpSSLCertOverride();
ApiService apiService = ApiService();
apiService.setAccessToken(Store.get(StoreKey.accessToken));
AppSettingsService settingService = AppSettingsService();
- BackupService backupService = BackupService(apiService, db, settingService);
AppSettingsService settingsService = AppSettingsService();
+ PartnerService partnerService = PartnerService(apiService, db);
+ HashService hashService = HashService(db, this);
+ SyncService syncSerive = SyncService(db, hashService);
+ UserService userService =
+ UserService(apiService, db, syncSerive, partnerService);
+ AlbumService albumService =
+ AlbumService(apiService, userService, syncSerive, db);
+ BackupService backupService =
+ BackupService(apiService, db, settingService, albumService);
final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync();
final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync();
@@ -416,7 +432,7 @@ class BackgroundService {
return false;
}
- List toUpload = await backupService.buildUploadCandidates(
+ Set toUpload = await backupService.buildUploadCandidates(
selectedAlbums,
excludedAlbums,
);
@@ -460,29 +476,47 @@ class BackgroundService {
final bool ok = await backupService.backupAsset(
toUpload,
_cancellationToken!,
- pmProgressHandler,
- notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {},
- notifySingleProgress ? _onProgress : (sent, total) {},
- notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {},
- _onBackupError,
- sortAssets: true,
+ pmProgressHandler: pmProgressHandler,
+ onSuccess: (result) => _onAssetUploaded(
+ result: result,
+ shouldNotify: notifyTotalProgress,
+ ),
+ onProgress: (bytes, totalBytes) =>
+ _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress),
+ onCurrentAsset: (asset) =>
+ _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress),
+ onError: _onBackupError,
+ isBackground: true,
);
+
if (!ok && !_cancellationToken!.isCancelled) {
_showErrorNotification(
title: "backup_background_service_error_title".tr(),
content: "backup_background_service_backup_failed_message".tr(),
);
}
+
return ok;
}
- void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) {
+ void _onAssetUploaded({
+ required SuccessUploadAsset result,
+ bool shouldNotify = false,
+ }) async {
+ if (!shouldNotify) {
+ return;
+ }
+
_uploadedAssetsCount++;
_throttledNotifiy();
}
- void _onProgress(int sent, int total) {
- _throttledDetailNotify(progress: sent, total: total);
+ void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) {
+ if (!shouldNotify) {
+ return;
+ }
+
+ _throttledDetailNotify(progress: bytes, total: totalBytes);
}
void _updateDetailProgress(String? title, int progress, int total) {
@@ -522,7 +556,14 @@ class BackgroundService {
);
}
- void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) {
+ void _onSetCurrentBackupAsset(
+ CurrentUploadAsset currentUploadAsset, {
+ bool shouldNotify = false,
+ }) {
+ if (!shouldNotify) {
+ return;
+ }
+
_throttledDetailNotify.title =
"backup_background_service_current_upload_notification"
.tr(args: [currentUploadAsset.fileName]);
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 64d683dc2ae83..858499443ee1d 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -9,11 +9,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
+import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
+import 'package:immich_mobile/models/backup/success_upload_asset.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
+import 'package:immich_mobile/services/album.service.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:isar/isar.dart';
@@ -28,6 +31,7 @@ final backupServiceProvider = Provider(
ref.watch(apiServiceProvider),
ref.watch(dbProvider),
ref.watch(appSettingsServiceProvider),
+ ref.watch(albumServiceProvider),
),
);
@@ -37,8 +41,14 @@ class BackupService {
final Isar _db;
final Logger _log = Logger("BackupService");
final AppSettingsService _appSetting;
+ final AlbumService _albumService;
- BackupService(this._apiService, this._db, this._appSetting);
+ BackupService(
+ this._apiService,
+ this._db,
+ this._appSetting,
+ this._albumService,
+ );
Future?> getDeviceBackupAsset() async {
final String deviceId = Store.get(StoreKey.deviceId);
@@ -70,10 +80,12 @@ class BackupService {
_db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude);
/// Returns all assets newer than the last successful backup per album
- Future> buildUploadCandidates(
+ /// if `useTimeFilter` is set to true, all assets will be returned
+ Future> buildUploadCandidates(
List selectedBackupAlbums,
- List excludedBackupAlbums,
- ) async {
+ List excludedBackupAlbums, {
+ bool useTimeFilter = true,
+ }) async {
final filter = FilterOptionGroup(
containsPathModified: true,
orders: [const OrderOption(type: OrderOptionType.updateDate)],
@@ -82,105 +94,156 @@ class BackupService {
videoOption: const FilterOption(needTitle: true),
);
final now = DateTime.now();
+
final List selectedAlbums =
- await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now);
+ await _loadAlbumsWithTimeFilter(
+ selectedBackupAlbums,
+ filter,
+ now,
+ useTimeFilter: useTimeFilter,
+ );
+
if (selectedAlbums.every((e) => e == null)) {
- return [];
- }
- final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll);
- if (allIdx != -1) {
- final List excludedAlbums =
- await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now);
- final List toAdd = await _fetchAssetsAndUpdateLastBackup(
- selectedAlbums.slice(allIdx, allIdx + 1),
- selectedBackupAlbums.slice(allIdx, allIdx + 1),
- now,
- );
- final List toRemove = await _fetchAssetsAndUpdateLastBackup(
- excludedAlbums,
- excludedBackupAlbums,
- now,
- );
- return toAdd.toSet().difference(toRemove.toSet()).toList();
- } else {
- return await _fetchAssetsAndUpdateLastBackup(
- selectedAlbums,
- selectedBackupAlbums,
- now,
- );
+ return {};
}
+
+ final List excludedAlbums =
+ await _loadAlbumsWithTimeFilter(
+ excludedBackupAlbums,
+ filter,
+ now,
+ useTimeFilter: useTimeFilter,
+ );
+
+ final Set toAdd = await _fetchAssetsAndUpdateLastBackup(
+ selectedAlbums,
+ selectedBackupAlbums,
+ now,
+ useTimeFilter: useTimeFilter,
+ );
+
+ final Set toRemove = await _fetchAssetsAndUpdateLastBackup(
+ excludedAlbums,
+ excludedBackupAlbums,
+ now,
+ useTimeFilter: useTimeFilter,
+ );
+
+ return toAdd.difference(toRemove);
}
Future> _loadAlbumsWithTimeFilter(
List albums,
FilterOptionGroup filter,
- DateTime now,
- ) async {
+ DateTime now, {
+ bool useTimeFilter = true,
+ }) async {
List result = [];
- for (BackupAlbum a in albums) {
+ for (BackupAlbum backupAlbum in albums) {
try {
+ final optionGroup = useTimeFilter
+ ? filter.copyWith(
+ updateTimeCond: DateTimeCond(
+ // subtract 2 seconds to prevent missing assets due to rounding issues
+ min: backupAlbum.lastBackup
+ .subtract(const Duration(seconds: 2)),
+ max: now,
+ ),
+ )
+ : filter;
+
final AssetPathEntity album =
await AssetPathEntity.obtainPathFromProperties(
- id: a.id,
- optionGroup: filter.copyWith(
- updateTimeCond: DateTimeCond(
- // subtract 2 seconds to prevent missing assets due to rounding issues
- min: a.lastBackup.subtract(const Duration(seconds: 2)),
- max: now,
- ),
- ),
+ id: backupAlbum.id,
+ optionGroup: optionGroup,
maxDateTimeToNow: false,
);
+
result.add(album);
} on StateError {
// either there are no assets matching the filter criteria OR the album no longer exists
}
}
+
return result;
}
- Future> _fetchAssetsAndUpdateLastBackup(
- List albums,
+ Future> _fetchAssetsAndUpdateLastBackup(
+ List localAlbums,
List backupAlbums,
- DateTime now,
- ) async {
- List result = [];
- for (int i = 0; i < albums.length; i++) {
- final AssetPathEntity? a = albums[i];
- if (a != null &&
- a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) {
- result.addAll(
- await a.getAssetListRange(start: 0, end: await a.assetCountAsync),
- );
- backupAlbums[i].lastBackup = now;
+ DateTime now, {
+ bool useTimeFilter = true,
+ }) async {
+ Set candidate = {};
+
+ for (int i = 0; i < localAlbums.length; i++) {
+ final localAlbum = localAlbums[i];
+ if (localAlbum == null) {
+ continue;
}
+
+ if (useTimeFilter &&
+ localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) ==
+ true) {
+ continue;
+ }
+
+ final assets = await localAlbum.getAssetListRange(
+ start: 0,
+ end: await localAlbum.assetCountAsync,
+ );
+
+ // Add album's name to the asset info
+ for (final asset in assets) {
+ List albumNames = [localAlbum.name];
+
+ final existingAsset = candidate.firstWhereOrNull(
+ (a) => a.asset.id == asset.id,
+ );
+
+ if (existingAsset != null) {
+ albumNames.addAll(existingAsset.albumNames);
+ candidate.remove(existingAsset);
+ }
+
+ candidate.add(
+ BackupCandidate(
+ asset: asset,
+ albumNames: albumNames,
+ ),
+ );
+ }
+
+ backupAlbums[i].lastBackup = now;
}
- return result;
+
+ return candidate;
}
/// Returns a new list of assets not yet uploaded
- Future> removeAlreadyUploadedAssets(
- List candidates,
+ Future> removeAlreadyUploadedAssets(
+ Set candidates,
) async {
if (candidates.isEmpty) {
return candidates;
}
+
final Set duplicatedAssetIds = await getDuplicatedAssetIds();
- candidates = duplicatedAssetIds.isEmpty
- ? candidates
- : candidates
- .whereNot((asset) => duplicatedAssetIds.contains(asset.id))
- .toList();
+ candidates.removeWhere(
+ (candidate) => duplicatedAssetIds.contains(candidate.asset.id),
+ );
+
if (candidates.isEmpty) {
return candidates;
}
+
final Set existing = {};
try {
final String deviceId = Store.get(StoreKey.deviceId);
final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetsApi.checkExistingAssets(
CheckExistingAssetsDto(
- deviceAssetIds: candidates.map((e) => e.id).toList(),
+ deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
deviceId: deviceId,
),
);
@@ -194,55 +257,75 @@ class BackupService {
existing.addAll(allAssetsInDatabase);
}
}
- return existing.isEmpty
- ? candidates
- : candidates.whereNot((e) => existing.contains(e.id)).toList();
+
+ if (existing.isNotEmpty) {
+ candidates.removeWhere((c) => existing.contains(c.asset.id));
+ }
+
+ return candidates;
}
- Future backupAsset(
- Iterable assetList,
- http.CancellationToken cancelToken,
- PMProgressHandler? pmProgressHandler,
- Function(String, String, bool) uploadSuccessCb,
- Function(int, int) uploadProgressCb,
- Function(CurrentUploadAsset) setCurrentUploadAssetCb,
- Function(ErrorUploadAsset) errorCb, {
- bool sortAssets = false,
- }) async {
- final bool isIgnoreIcloudAssets =
- _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets);
-
+ Future _checkPermissions() async {
if (Platform.isAndroid &&
!(await pm.Permission.accessMediaLocation.status).isGranted) {
// double check that permission is granted here, to guard against
// uploading corrupt assets without EXIF information
_log.warning("Media location permission is not granted. "
"Cannot access original assets for backup.");
+
return false;
}
- final String deviceId = Store.get(StoreKey.deviceId);
- final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
- bool anyErrors = false;
- final List duplicatedAssetIds = [];
// DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS
if (Platform.isIOS) {
await PhotoManager.requestPermissionExtend();
}
- List assetsToUpload = sortAssets
- // Upload images before video assets
- // these are further sorted by using their creation date
- ? assetList.sorted(
- (a, b) {
- final cmp = a.typeInt - b.typeInt;
- if (cmp != 0) return cmp;
- return a.createDateTime.compareTo(b.createDateTime);
- },
- )
- : assetList.toList();
+ return true;
+ }
- for (var entity in assetsToUpload) {
+ /// Upload images before video assets for background tasks
+ /// these are further sorted by using their creation date
+ List _sortPhotosFirst(List candidates) {
+ return candidates.sorted(
+ (a, b) {
+ final cmp = a.asset.typeInt - b.asset.typeInt;
+ if (cmp != 0) return cmp;
+ return a.asset.createDateTime.compareTo(b.asset.createDateTime);
+ },
+ );
+ }
+
+ Future backupAsset(
+ Iterable assets,
+ http.CancellationToken cancelToken, {
+ bool isBackground = false,
+ PMProgressHandler? pmProgressHandler,
+ required void Function(SuccessUploadAsset result) onSuccess,
+ required void Function(int bytes, int totalBytes) onProgress,
+ required void Function(CurrentUploadAsset asset) onCurrentAsset,
+ required void Function(ErrorUploadAsset error) onError,
+ }) async {
+ final bool isIgnoreIcloudAssets =
+ _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets);
+ final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums);
+ final String deviceId = Store.get(StoreKey.deviceId);
+ final String savedEndpoint = Store.get(StoreKey.serverEndpoint);
+ final List duplicatedAssetIds = [];
+ bool anyErrors = false;
+
+ final hasPermission = await _checkPermissions();
+ if (!hasPermission) {
+ return false;
+ }
+
+ List candidates = assets.toList();
+ if (isBackground) {
+ candidates = _sortPhotosFirst(candidates);
+ }
+
+ for (final candidate in candidates) {
+ final AssetEntity entity = candidate.asset;
File? file;
File? livePhotoFile;
@@ -257,7 +340,7 @@ class BackupService {
continue;
}
- setCurrentUploadAssetCb(
+ onCurrentAsset(
CurrentUploadAsset(
id: entity.id,
fileCreatedAt: entity.createDateTime.year == 1970
@@ -299,23 +382,22 @@ class BackupService {
}
}
- var fileStream = file.openRead();
- var assetRawUploadData = http.MultipartFile(
+ final fileStream = file.openRead();
+ final assetRawUploadData = http.MultipartFile(
"assetData",
fileStream,
file.lengthSync(),
filename: originalFileName,
);
- var baseRequest = MultipartRequest(
+ final baseRequest = MultipartRequest(
'POST',
Uri.parse('$savedEndpoint/assets'),
- onProgress: ((bytes, totalBytes) =>
- uploadProgressCb(bytes, totalBytes)),
+ onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)),
);
+
baseRequest.headers.addAll(ApiService.getRequestHeaders());
baseRequest.headers["Transfer-Encoding"] = "chunked";
-
baseRequest.fields['deviceAssetId'] = entity.id;
baseRequest.fields['deviceId'] = deviceId;
baseRequest.fields['fileCreatedAt'] =
@@ -324,12 +406,9 @@ class BackupService {
entity.modifiedDateTime.toUtc().toIso8601String();
baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
baseRequest.fields['duration'] = entity.videoDuration.toString();
-
baseRequest.files.add(assetRawUploadData);
- var fileSize = file.lengthSync();
-
- setCurrentUploadAssetCb(
+ onCurrentAsset(
CurrentUploadAsset(
id: entity.id,
fileCreatedAt: entity.createDateTime.year == 1970
@@ -337,7 +416,7 @@ class BackupService {
: entity.createDateTime,
fileName: originalFileName,
fileType: _getAssetType(entity.type),
- fileSize: fileSize,
+ fileSize: file.lengthSync(),
iCloudAsset: false,
),
);
@@ -356,22 +435,23 @@ class BackupService {
baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId;
}
- var response = await httpClient.send(
+ final response = await httpClient.send(
baseRequest,
cancellationToken: cancelToken,
);
- var responseBody = jsonDecode(await response.stream.bytesToString());
+ final responseBody =
+ jsonDecode(await response.stream.bytesToString());
if (![200, 201].contains(response.statusCode)) {
- var error = responseBody;
- var errorMessage = error['message'] ?? error['error'];
+ final error = responseBody;
+ final errorMessage = error['message'] ?? error['error'];
debugPrint(
"Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}",
);
- errorCb(
+ onError(
ErrorUploadAsset(
asset: entity,
id: entity.id,
@@ -386,23 +466,37 @@ class BackupService {
anyErrors = true;
break;
}
+
continue;
}
- var isDuplicate = false;
+ bool isDuplicate = false;
if (response.statusCode == 200) {
isDuplicate = true;
duplicatedAssetIds.add(entity.id);
}
- uploadSuccessCb(entity.id, deviceId, isDuplicate);
+ onSuccess(
+ SuccessUploadAsset(
+ candidate: candidate,
+ remoteAssetId: responseBody['id'] as String,
+ isDuplicate: isDuplicate,
+ ),
+ );
+
+ if (shouldSyncAlbums) {
+ await _albumService.syncUploadAlbums(
+ candidate.albumNames,
+ [responseBody['id'] as String],
+ );
+ }
}
} on http.CancelledException {
debugPrint("Backup was cancelled by the user");
anyErrors = true;
break;
- } catch (e) {
- debugPrint("ERROR backupAsset: ${e.toString()}");
+ } catch (error, stackTrace) {
+ debugPrint("Error backup asset: ${error.toString()}: $stackTrace");
anyErrors = true;
continue;
} finally {
@@ -416,9 +510,11 @@ class BackupService {
}
}
}
+
if (duplicatedAssetIds.isNotEmpty) {
await _saveDuplicatedAssetIds(duplicatedAssetIds);
}
+
return !anyErrors;
}
diff --git a/mobile/lib/services/image_viewer.service.dart b/mobile/lib/services/image_viewer.service.dart
index e61573af379ae..9bcaba1d26b95 100644
--- a/mobile/lib/services/image_viewer.service.dart
+++ b/mobile/lib/services/image_viewer.service.dart
@@ -19,7 +19,7 @@ class ImageViewerService {
ImageViewerService(this._apiService);
- Future downloadAssetToDevice(Asset asset) async {
+ Future downloadAsset(Asset asset) async {
File? imageFile;
File? videoFile;
try {
@@ -82,18 +82,23 @@ class ImageViewerService {
}
final AssetEntity? entity;
+ final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
if (asset.isImage) {
entity = await PhotoManager.editor.saveImage(
res.bodyBytes,
title: asset.fileName,
+ relativePath: relativePath,
);
} else {
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
videoFile.writeAsBytesSync(res.bodyBytes);
- entity = await PhotoManager.editor
- .saveVideo(videoFile, title: asset.fileName);
+ entity = await PhotoManager.editor.saveVideo(
+ videoFile,
+ title: asset.fileName,
+ relativePath: relativePath,
+ );
}
return entity != null;
}
diff --git a/mobile/lib/services/oauth.service.dart b/mobile/lib/services/oauth.service.dart
index 807c88db8de50..30e6448d7f8a9 100644
--- a/mobile/lib/services/oauth.service.dart
+++ b/mobile/lib/services/oauth.service.dart
@@ -3,7 +3,7 @@ import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:flutter_web_auth/flutter_web_auth.dart';
-// Redirect URL = app.immich://
+// Redirect URL = app.immich:///oauth-callback
class OAuthService {
final ApiService _apiService;
@@ -16,28 +16,40 @@ class OAuthService {
) async {
// Resolve API server endpoint from user provided serverUrl
await _apiService.resolveAndSetEndpoint(serverUrl);
+ final redirectUri = '$callbackUrlScheme:///oauth-callback';
+ log.info(
+ "Starting OAuth flow with redirect URI: $redirectUri",
+ );
final dto = await _apiService.oAuthApi.startOAuth(
- OAuthConfigDto(redirectUri: '$callbackUrlScheme:/'),
+ OAuthConfigDto(redirectUri: redirectUri),
);
- return dto?.url;
+
+ final authUrl = dto?.url;
+ log.info('Received Authorization URL: $authUrl');
+
+ return authUrl;
}
Future oAuthLogin(String oauthUrl) async {
- try {
- var result = await FlutterWebAuth.authenticate(
- url: oauthUrl,
- callbackUrlScheme: callbackUrlScheme,
- );
+ String result = await FlutterWebAuth.authenticate(
+ url: oauthUrl,
+ callbackUrlScheme: callbackUrlScheme,
+ );
- return await _apiService.oAuthApi.finishOAuth(
- OAuthCallbackDto(
- url: result,
- ),
+ log.info('Received OAuth callback: $result');
+
+ if (result.startsWith('app.immich:/oauth-callback')) {
+ result = result.replaceAll(
+ 'app.immich:/oauth-callback',
+ 'app.immich:///oauth-callback',
);
- } catch (e, stack) {
- log.severe("OAuth login failed", e, stack);
- return null;
}
+
+ return await _apiService.oAuthApi.finishOAuth(
+ OAuthCallbackDto(
+ url: result,
+ ),
+ );
}
}
diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart
index b03d9ccdb0917..04bc9787548eb 100644
--- a/mobile/lib/utils/hooks/crop_controller_hook.dart
+++ b/mobile/lib/utils/hooks/crop_controller_hook.dart
@@ -6,7 +6,7 @@ import 'dart:ui'; // Import the dart:ui library for Rect
CropController useCropController() {
return useMemoized(
() => CropController(
- defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9),
+ defaultCrop: const Rect.fromLTRB(0, 0, 1, 1),
),
);
}
diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart
index 7b27f59aee8c3..349b2322afac1 100644
--- a/mobile/lib/utils/openapi_patching.dart
+++ b/mobile/lib/utils/openapi_patching.dart
@@ -4,9 +4,30 @@ dynamic upgradeDto(dynamic value, String targetType) {
switch (targetType) {
case 'UserPreferencesResponseDto':
if (value is Map) {
- if (value['rating'] == null) {
- value['rating'] = RatingResponse().toJson();
- }
+ addDefault(value, 'download.includeEmbeddedVideos', false);
+ addDefault(value, 'folders', FoldersResponse().toJson());
+ addDefault(value, 'memories', MemoriesResponse().toJson());
+ addDefault(value, 'ratings', RatingsResponse().toJson());
+ addDefault(value, 'people', PeopleResponse().toJson());
+ addDefault(value, 'tags', TagsResponse().toJson());
}
+ break;
+ }
+}
+
+addDefault(dynamic value, String keys, dynamic defaultValue) {
+ // Loop through the keys and assign the default value if the key is not present
+ List keyList = keys.split('.');
+ dynamic current = value;
+
+ for (int i = 0; i < keyList.length - 1; i++) {
+ if (current[keyList[i]] == null) {
+ current[keyList[i]] = {};
+ }
+ current = current[keyList[i]];
+ }
+
+ if (current[keyList.last] == null) {
+ current[keyList.last] = defaultValue;
}
}
diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
index 7d9e49bd29305..7e6136c256192 100644
--- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart
@@ -16,6 +16,7 @@ import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'
import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart';
import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart';
import 'package:immich_mobile/routing/router.dart';
+import 'package:immich_mobile/widgets/common/immich_image.dart';
import 'package:immich_mobile/entities/asset.entity.dart';
import 'package:immich_mobile/providers/asset.provider.dart';
import 'package:immich_mobile/providers/server_info.provider.dart';
@@ -184,6 +185,7 @@ class BottomGalleryBar extends ConsumerWidget {
}
void handleEdit() async {
+ final image = Image(image: ImmichImage.imageProvider(asset: asset));
if (asset.isOffline) {
ImmichToast.show(
durationInSecond: 1,
@@ -195,8 +197,11 @@ class BottomGalleryBar extends ConsumerWidget {
}
Navigator.of(context).push(
MaterialPageRoute(
- builder: (context) =>
- EditImagePage(asset: asset), // Send the Asset object
+ builder: (context) => EditImagePage(
+ asset: asset,
+ image: image,
+ isEdited: false,
+ ),
),
);
}
diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
index fde0d2e82d617..6de8f5da33944 100644
--- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
+++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart
@@ -93,6 +93,10 @@ class GalleryAppBar extends ConsumerWidget {
);
}
+ handleDownloadAsset() {
+ ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context);
+ }
+
return IgnorePointer(
ignoring: !ref.watch(showControlsProvider),
child: AnimatedOpacity(
@@ -109,13 +113,7 @@ class GalleryAppBar extends ConsumerWidget {
onFavorite: toggleFavorite,
onRestorePressed: () => handleRestore(asset),
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
- onDownloadPressed: asset.isLocal
- ? null
- : () =>
- ref.read(imageViewerStateProvider.notifier).downloadAsset(
- asset,
- context,
- ),
+ onDownloadPressed: asset.isLocal ? null : handleDownloadAsset,
onToggleMotionVideo: onToggleMotionVideo,
onAddToAlbumPressed: () => addToAlbum(asset),
onActivitiesPressed: handleActivities,
diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart
index e9349bd69eccf..0c9cd2d89d33c 100644
--- a/mobile/lib/widgets/backup/album_info_card.dart
+++ b/mobile/lib/widgets/backup/album_info_card.dart
@@ -5,15 +5,21 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoCard extends HookConsumerWidget {
final AvailableAlbum album;
- const AlbumInfoCard({super.key, required this.album});
+ const AlbumInfoCard({
+ super.key,
+ required this.album,
+ });
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -21,6 +27,9 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
+ final syncAlbum = ref
+ .watch(appSettingsServiceProvider)
+ .getSetting(AppSettingsEnum.syncAlbums);
final isDarkTheme = context.isDarkTheme;
@@ -85,6 +94,9 @@ class AlbumInfoCard extends HookConsumerWidget {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
+ if (syncAlbum) {
+ ref.read(albumProvider.notifier).createSyncAlbum(album.name);
+ }
}
},
onDoubleTap: () {
diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart
index 7cdc595c7fc53..d326bad3e0fc7 100644
--- a/mobile/lib/widgets/backup/album_info_list_tile.dart
+++ b/mobile/lib/widgets/backup/album_info_list_tile.dart
@@ -5,9 +5,12 @@ import 'package:fluttertoast/fluttertoast.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/models/backup/available_album.model.dart';
+import 'package:immich_mobile/providers/album/album.provider.dart';
+import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/providers/backup/backup.provider.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:immich_mobile/providers/haptic_feedback.provider.dart';
+import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
class AlbumInfoListTile extends HookConsumerWidget {
@@ -21,7 +24,10 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.watch(backupProvider).selectedBackupAlbums.contains(album);
final bool isExcluded =
ref.watch(backupProvider).excludedBackupAlbums.contains(album);
- var assetCount = useState(0);
+ final assetCount = useState(0);
+ final syncAlbum = ref
+ .watch(appSettingsServiceProvider)
+ .getSetting(AppSettingsEnum.syncAlbums);
useEffect(
() {
@@ -98,6 +104,9 @@ class AlbumInfoListTile extends HookConsumerWidget {
ref.read(backupProvider.notifier).removeAlbumForBackup(album);
} else {
ref.read(backupProvider.notifier).addAlbumForBackup(album);
+ if (syncAlbum) {
+ ref.read(albumProvider.notifier).createSyncAlbum(album.name);
+ }
}
},
leading: buildIcon(),
diff --git a/mobile/lib/widgets/forms/login/login_form.dart b/mobile/lib/widgets/forms/login/login_form.dart
index 4384879fce6a0..14a4e89dd6066 100644
--- a/mobile/lib/widgets/forms/login/login_form.dart
+++ b/mobile/lib/widgets/forms/login/login_form.dart
@@ -27,12 +27,15 @@ import 'package:immich_mobile/widgets/forms/login/login_button.dart';
import 'package:immich_mobile/widgets/forms/login/o_auth_login_button.dart';
import 'package:immich_mobile/widgets/forms/login/password_input.dart';
import 'package:immich_mobile/widgets/forms/login/server_endpoint_input.dart';
+import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:permission_handler/permission_handler.dart';
class LoginForm extends HookConsumerWidget {
- const LoginForm({super.key});
+ LoginForm({super.key});
+
+ final log = Logger('LoginForm');
@override
Widget build(BuildContext context, WidgetRef ref) {
@@ -229,7 +232,9 @@ class LoginForm extends HookConsumerWidget {
.getOAuthServerUrl(sanitizeUrl(serverEndpointController.text));
isLoading.value = true;
- } catch (e) {
+ } catch (error, stack) {
+ log.severe('Error getting OAuth server Url: $error', stack);
+
ImmichToast.show(
context: context,
msg: "login_form_failed_get_oauth_server_config".tr(),
@@ -241,10 +246,19 @@ class LoginForm extends HookConsumerWidget {
}
if (oAuthServerUrl != null) {
- var loginResponseDto = await oAuthService.oAuthLogin(oAuthServerUrl);
+ try {
+ final loginResponseDto =
+ await oAuthService.oAuthLogin(oAuthServerUrl);
- if (loginResponseDto != null) {
- var isSuccess = await ref
+ if (loginResponseDto == null) {
+ return;
+ }
+
+ log.info(
+ "Finished OAuth login with response: ${loginResponseDto.userEmail}",
+ );
+
+ final isSuccess = await ref
.watch(authenticationProvider.notifier)
.setSuccessLoginInfo(
accessToken: loginResponseDto.accessToken,
@@ -258,17 +272,19 @@ class LoginForm extends HookConsumerWidget {
ref.watch(backupProvider.notifier).resumeBackup();
}
context.replaceRoute(const TabControllerRoute());
- } else {
- ImmichToast.show(
- context: context,
- msg: "login_form_failed_login".tr(),
- toastType: ToastType.error,
- gravity: ToastGravity.TOP,
- );
}
- }
+ } catch (error, stack) {
+ log.severe('Error logging in with OAuth: $error', stack);
- isLoading.value = false;
+ ImmichToast.show(
+ context: context,
+ msg: error.toString(),
+ toastType: ToastType.error,
+ gravity: ToastGravity.TOP,
+ );
+ } finally {
+ isLoading.value = false;
+ }
} else {
ImmichToast.show(
context: context,
diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart
index 2e5618c9e03bf..e2110c9c295f1 100644
--- a/mobile/lib/widgets/search/search_filter/camera_picker.dart
+++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart
@@ -51,10 +51,14 @@ class CameraPicker extends HookConsumerWidget {
controller: makeTextController,
leadingIcon: const Icon(Icons.photo_camera_rounded),
onSelected: (value) {
+ if (value.toString() == selectedMake.value) {
+ return;
+ }
selectedMake.value = value.toString();
+ modelTextController.value = TextEditingValue.empty;
onSelect({
'make': selectedMake.value,
- 'model': selectedModel.value,
+ 'model': null,
});
},
);
diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart
index 230d7dd4daa5d..dd8785459f7fb 100644
--- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart
+++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart
@@ -29,6 +29,7 @@ class SearchDropdown extends StatelessWidget {
return LayoutBuilder(
builder: (context, constraints) {
return DropdownMenu(
+ controller: controller,
leadingIcon: leadingIcon,
width: constraints.maxWidth,
dropdownMenuEntries: dropdownMenuEntries,
diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
index 25bcf2d06e507..c093e8f1e3c98 100644
--- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
+++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart
@@ -1,9 +1,12 @@
import 'dart:io';
+import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
+import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/providers/backup/backup_verification.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
+import 'package:immich_mobile/services/asset.service.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart';
import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart';
import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart';
@@ -23,7 +26,21 @@ class BackupSettings extends HookConsumerWidget {
useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets);
final isAdvancedTroubleshooting =
useAppSettingsState(AppSettingsEnum.advancedTroubleshooting);
+ final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums);
final isCorruptCheckInProgress = ref.watch(backupVerificationProvider);
+ final isAlbumSyncInProgress = useState(false);
+
+ syncAlbums() async {
+ isAlbumSyncInProgress.value = true;
+ try {
+ await ref.read(assetServiceProvider).syncUploadedAssetToAlbums();
+ } catch (_) {
+ } finally {
+ Future.delayed(const Duration(seconds: 1), () {
+ isAlbumSyncInProgress.value = false;
+ });
+ }
+ }
final backupSettings = [
const ForegroundBackupSettings(),
@@ -58,6 +75,23 @@ class BackupSettings extends HookConsumerWidget {
.performBackupCheck(context)
: null,
),
+ if (albumSync.value)
+ SettingsButtonListTile(
+ icon: Icons.photo_album_outlined,
+ title: 'sync_albums'.tr(),
+ subtitle: Text(
+ "sync_albums_manual_subtitle".tr(),
+ ),
+ buttonText: 'sync_albums'.tr(),
+ child: isAlbumSyncInProgress.value
+ ? const CircularProgressIndicator.adaptive(
+ strokeWidth: 2,
+ )
+ : ElevatedButton(
+ onPressed: syncAlbums,
+ child: Text('sync'.tr()),
+ ),
+ ),
];
return SettingsSubPageScaffold(
diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart
index 196e3d170feaf..c8bd8e4b588c9 100644
--- a/mobile/lib/widgets/settings/settings_button_list_tile.dart
+++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart
@@ -9,6 +9,7 @@ class SettingsButtonListTile extends StatelessWidget {
final Widget? subtitle;
final String? subtileText;
final String buttonText;
+ final Widget? child;
final void Function()? onButtonTap;
const SettingsButtonListTile({
@@ -18,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget {
this.subtileText,
this.subtitle,
required this.buttonText,
+ this.child,
this.onButtonTap,
super.key,
});
@@ -48,7 +50,8 @@ class SettingsButtonListTile extends StatelessWidget {
),
if (subtitle != null) subtitle!,
const SizedBox(height: 6),
- ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)),
+ child ??
+ ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)),
],
),
);
diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart
index 78f1738266a31..8aa4ec0a60ec0 100644
--- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart
+++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart
@@ -9,6 +9,9 @@ class SettingsSwitchListTile extends StatelessWidget {
final String? subtitle;
final IconData? icon;
final Function(bool)? onChanged;
+ final EdgeInsets? contentPadding;
+ final TextStyle? titleStyle;
+ final TextStyle? subtitleStyle;
const SettingsSwitchListTile({
required this.valueNotifier,
@@ -17,6 +20,9 @@ class SettingsSwitchListTile extends StatelessWidget {
this.icon,
this.enabled = true,
this.onChanged,
+ this.contentPadding = const EdgeInsets.symmetric(horizontal: 20),
+ this.titleStyle,
+ this.subtitleStyle,
super.key,
});
@@ -30,7 +36,7 @@ class SettingsSwitchListTile extends StatelessWidget {
}
return SwitchListTile.adaptive(
- contentPadding: const EdgeInsets.symmetric(horizontal: 20),
+ contentPadding: contentPadding,
selectedTileColor: enabled ? null : context.themeData.disabledColor,
value: valueNotifier.value,
onChanged: onSwitchChanged,
@@ -45,20 +51,22 @@ class SettingsSwitchListTile extends StatelessWidget {
: null,
title: Text(
title,
- style: context.textTheme.bodyLarge?.copyWith(
- fontWeight: FontWeight.w500,
- color: enabled ? null : context.themeData.disabledColor,
- height: 1.5,
- ),
+ style: titleStyle ??
+ context.textTheme.bodyLarge?.copyWith(
+ fontWeight: FontWeight.w500,
+ color: enabled ? null : context.themeData.disabledColor,
+ height: 1.5,
+ ),
),
subtitle: subtitle != null
? Text(
subtitle!,
- style: context.textTheme.bodyMedium?.copyWith(
- color: enabled
- ? context.colorScheme.onSurfaceSecondary
- : context.themeData.disabledColor,
- ),
+ style: subtitleStyle ??
+ context.textTheme.bodyMedium?.copyWith(
+ color: enabled
+ ? context.colorScheme.onSurfaceSecondary
+ : context.themeData.disabledColor,
+ ),
)
: null,
);
diff --git a/mobile/openapi/.gitignore b/mobile/openapi/.gitignore
index 1be28ced0940a..0f74d293b9895 100644
--- a/mobile/openapi/.gitignore
+++ b/mobile/openapi/.gitignore
@@ -3,7 +3,9 @@
.dart_tool/
.packages
build/
-pubspec.lock # Except for application packages
+
+# Except for application packages
+pubspec.lock
doc/api/
diff --git a/mobile/openapi/.openapi-generator/VERSION b/mobile/openapi/.openapi-generator/VERSION
index 18bb4182dd014..09a6d30847de3 100644
--- a/mobile/openapi/.openapi-generator/VERSION
+++ b/mobile/openapi/.openapi-generator/VERSION
@@ -1 +1 @@
-7.5.0
+7.8.0
diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md
index 1da4463a1225b..bb845157979b6 100644
--- a/mobile/openapi/README.md
+++ b/mobile/openapi/README.md
@@ -3,8 +3,8 @@ Immich API
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
-- API version: 1.112.1
-- Generator version: 7.5.0
+- API version: 1.114.0
+- Generator version: 7.8.0
- Build package: org.openapitools.codegen.languages.DartClientCodegen
## Requirements
@@ -210,14 +210,15 @@ Class | Method | HTTP request | Description
*SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding |
*SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state |
*SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding |
+*TagsApi* | [**bulkTagAssets**](doc//TagsApi.md#bulktagassets) | **PUT** /tags/assets |
*TagsApi* | [**createTag**](doc//TagsApi.md#createtag) | **POST** /tags |
*TagsApi* | [**deleteTag**](doc//TagsApi.md#deletetag) | **DELETE** /tags/{id} |
*TagsApi* | [**getAllTags**](doc//TagsApi.md#getalltags) | **GET** /tags |
-*TagsApi* | [**getTagAssets**](doc//TagsApi.md#gettagassets) | **GET** /tags/{id}/assets |
*TagsApi* | [**getTagById**](doc//TagsApi.md#gettagbyid) | **GET** /tags/{id} |
*TagsApi* | [**tagAssets**](doc//TagsApi.md#tagassets) | **PUT** /tags/{id}/assets |
*TagsApi* | [**untagAssets**](doc//TagsApi.md#untagassets) | **DELETE** /tags/{id}/assets |
-*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PATCH** /tags/{id} |
+*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PUT** /tags/{id} |
+*TagsApi* | [**upsertTags**](doc//TagsApi.md#upserttags) | **PUT** /tags |
*TimelineApi* | [**getTimeBucket**](doc//TimelineApi.md#gettimebucket) | **GET** /timeline/bucket |
*TimelineApi* | [**getTimeBuckets**](doc//TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets |
*TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty |
@@ -305,7 +306,6 @@ Class | Method | HTTP request | Description
- [CreateAlbumDto](doc//CreateAlbumDto.md)
- [CreateLibraryDto](doc//CreateLibraryDto.md)
- [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md)
- - [CreateTagDto](doc//CreateTagDto.md)
- [DownloadArchiveInfo](doc//DownloadArchiveInfo.md)
- [DownloadInfoDto](doc//DownloadInfoDto.md)
- [DownloadResponse](doc//DownloadResponse.md)
@@ -324,6 +324,8 @@ Class | Method | HTTP request | Description
- [FileReportDto](doc//FileReportDto.md)
- [FileReportFixDto](doc//FileReportFixDto.md)
- [FileReportItemDto](doc//FileReportItemDto.md)
+ - [FoldersResponse](doc//FoldersResponse.md)
+ - [FoldersUpdate](doc//FoldersUpdate.md)
- [ImageFormat](doc//ImageFormat.md)
- [JobCommand](doc//JobCommand.md)
- [JobCommandDto](doc//JobCommandDto.md)
@@ -342,12 +344,12 @@ Class | Method | HTTP request | Description
- [MapMarkerResponseDto](doc//MapMarkerResponseDto.md)
- [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md)
- [MapTheme](doc//MapTheme.md)
+ - [MemoriesResponse](doc//MemoriesResponse.md)
+ - [MemoriesUpdate](doc//MemoriesUpdate.md)
- [MemoryCreateDto](doc//MemoryCreateDto.md)
- [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md)
- - [MemoryResponse](doc//MemoryResponse.md)
- [MemoryResponseDto](doc//MemoryResponseDto.md)
- [MemoryType](doc//MemoryType.md)
- - [MemoryUpdate](doc//MemoryUpdate.md)
- [MemoryUpdateDto](doc//MemoryUpdateDto.md)
- [MergePersonDto](doc//MergePersonDto.md)
- [MetadataSearchDto](doc//MetadataSearchDto.md)
@@ -359,7 +361,9 @@ Class | Method | HTTP request | Description
- [PartnerResponseDto](doc//PartnerResponseDto.md)
- [PathEntityType](doc//PathEntityType.md)
- [PathType](doc//PathType.md)
+ - [PeopleResponse](doc//PeopleResponse.md)
- [PeopleResponseDto](doc//PeopleResponseDto.md)
+ - [PeopleUpdate](doc//PeopleUpdate.md)
- [PeopleUpdateDto](doc//PeopleUpdateDto.md)
- [PeopleUpdateItem](doc//PeopleUpdateItem.md)
- [Permission](doc//Permission.md)
@@ -372,8 +376,8 @@ Class | Method | HTTP request | Description
- [PurchaseResponse](doc//PurchaseResponse.md)
- [PurchaseUpdate](doc//PurchaseUpdate.md)
- [QueueStatusDto](doc//QueueStatusDto.md)
- - [RatingResponse](doc//RatingResponse.md)
- - [RatingUpdate](doc//RatingUpdate.md)
+ - [RatingsResponse](doc//RatingsResponse.md)
+ - [RatingsUpdate](doc//RatingsUpdate.md)
- [ReactionLevel](doc//ReactionLevel.md)
- [ReactionType](doc//ReactionType.md)
- [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md)
@@ -403,11 +407,13 @@ Class | Method | HTTP request | Description
- [SignUpDto](doc//SignUpDto.md)
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
- [SmartSearchDto](doc//SmartSearchDto.md)
+ - [SourceType](doc//SourceType.md)
- [StackCreateDto](doc//StackCreateDto.md)
- [StackResponseDto](doc//StackResponseDto.md)
- [StackUpdateDto](doc//StackUpdateDto.md)
- [SystemConfigDto](doc//SystemConfigDto.md)
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
+ - [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
@@ -416,6 +422,7 @@ Class | Method | HTTP request | Description
- [SystemConfigLoggingDto](doc//SystemConfigLoggingDto.md)
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
+ - [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md)
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
- [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md)
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
@@ -429,8 +436,14 @@ Class | Method | HTTP request | Description
- [SystemConfigThemeDto](doc//SystemConfigThemeDto.md)
- [SystemConfigTrashDto](doc//SystemConfigTrashDto.md)
- [SystemConfigUserDto](doc//SystemConfigUserDto.md)
+ - [TagBulkAssetsDto](doc//TagBulkAssetsDto.md)
+ - [TagBulkAssetsResponseDto](doc//TagBulkAssetsResponseDto.md)
+ - [TagCreateDto](doc//TagCreateDto.md)
- [TagResponseDto](doc//TagResponseDto.md)
- - [TagTypeEnum](doc//TagTypeEnum.md)
+ - [TagUpdateDto](doc//TagUpdateDto.md)
+ - [TagUpsertDto](doc//TagUpsertDto.md)
+ - [TagsResponse](doc//TagsResponse.md)
+ - [TagsUpdate](doc//TagsUpdate.md)
- [TimeBucketResponseDto](doc//TimeBucketResponseDto.md)
- [TimeBucketSize](doc//TimeBucketSize.md)
- [ToneMapping](doc//ToneMapping.md)
@@ -441,7 +454,6 @@ Class | Method | HTTP request | Description
- [UpdateAssetDto](doc//UpdateAssetDto.md)
- [UpdateLibraryDto](doc//UpdateLibraryDto.md)
- [UpdatePartnerDto](doc//UpdatePartnerDto.md)
- - [UpdateTagDto](doc//UpdateTagDto.md)
- [UsageByUserDto](doc//UsageByUserDto.md)
- [UserAdminCreateDto](doc//UserAdminCreateDto.md)
- [UserAdminDeleteDto](doc//UserAdminDeleteDto.md)
diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart
index 05a43c8af7031..091e900145ab3 100644
--- a/mobile/openapi/lib/api.dart
+++ b/mobile/openapi/lib/api.dart
@@ -120,7 +120,6 @@ part 'model/colorspace.dart';
part 'model/create_album_dto.dart';
part 'model/create_library_dto.dart';
part 'model/create_profile_image_response_dto.dart';
-part 'model/create_tag_dto.dart';
part 'model/download_archive_info.dart';
part 'model/download_info_dto.dart';
part 'model/download_response.dart';
@@ -139,6 +138,8 @@ part 'model/file_checksum_response_dto.dart';
part 'model/file_report_dto.dart';
part 'model/file_report_fix_dto.dart';
part 'model/file_report_item_dto.dart';
+part 'model/folders_response.dart';
+part 'model/folders_update.dart';
part 'model/image_format.dart';
part 'model/job_command.dart';
part 'model/job_command_dto.dart';
@@ -157,12 +158,12 @@ part 'model/logout_response_dto.dart';
part 'model/map_marker_response_dto.dart';
part 'model/map_reverse_geocode_response_dto.dart';
part 'model/map_theme.dart';
+part 'model/memories_response.dart';
+part 'model/memories_update.dart';
part 'model/memory_create_dto.dart';
part 'model/memory_lane_response_dto.dart';
-part 'model/memory_response.dart';
part 'model/memory_response_dto.dart';
part 'model/memory_type.dart';
-part 'model/memory_update.dart';
part 'model/memory_update_dto.dart';
part 'model/merge_person_dto.dart';
part 'model/metadata_search_dto.dart';
@@ -174,7 +175,9 @@ part 'model/partner_direction.dart';
part 'model/partner_response_dto.dart';
part 'model/path_entity_type.dart';
part 'model/path_type.dart';
+part 'model/people_response.dart';
part 'model/people_response_dto.dart';
+part 'model/people_update.dart';
part 'model/people_update_dto.dart';
part 'model/people_update_item.dart';
part 'model/permission.dart';
@@ -187,8 +190,8 @@ part 'model/places_response_dto.dart';
part 'model/purchase_response.dart';
part 'model/purchase_update.dart';
part 'model/queue_status_dto.dart';
-part 'model/rating_response.dart';
-part 'model/rating_update.dart';
+part 'model/ratings_response.dart';
+part 'model/ratings_update.dart';
part 'model/reaction_level.dart';
part 'model/reaction_type.dart';
part 'model/reverse_geocoding_state_response_dto.dart';
@@ -218,11 +221,13 @@ part 'model/shared_link_type.dart';
part 'model/sign_up_dto.dart';
part 'model/smart_info_response_dto.dart';
part 'model/smart_search_dto.dart';
+part 'model/source_type.dart';
part 'model/stack_create_dto.dart';
part 'model/stack_response_dto.dart';
part 'model/stack_update_dto.dart';
part 'model/system_config_dto.dart';
part 'model/system_config_f_fmpeg_dto.dart';
+part 'model/system_config_faces_dto.dart';
part 'model/system_config_image_dto.dart';
part 'model/system_config_job_dto.dart';
part 'model/system_config_library_dto.dart';
@@ -231,6 +236,7 @@ part 'model/system_config_library_watch_dto.dart';
part 'model/system_config_logging_dto.dart';
part 'model/system_config_machine_learning_dto.dart';
part 'model/system_config_map_dto.dart';
+part 'model/system_config_metadata_dto.dart';
part 'model/system_config_new_version_check_dto.dart';
part 'model/system_config_notifications_dto.dart';
part 'model/system_config_o_auth_dto.dart';
@@ -244,8 +250,14 @@ part 'model/system_config_template_storage_option_dto.dart';
part 'model/system_config_theme_dto.dart';
part 'model/system_config_trash_dto.dart';
part 'model/system_config_user_dto.dart';
+part 'model/tag_bulk_assets_dto.dart';
+part 'model/tag_bulk_assets_response_dto.dart';
+part 'model/tag_create_dto.dart';
part 'model/tag_response_dto.dart';
-part 'model/tag_type_enum.dart';
+part 'model/tag_update_dto.dart';
+part 'model/tag_upsert_dto.dart';
+part 'model/tags_response.dart';
+part 'model/tags_update.dart';
part 'model/time_bucket_response_dto.dart';
part 'model/time_bucket_size.dart';
part 'model/tone_mapping.dart';
@@ -256,7 +268,6 @@ part 'model/update_album_user_dto.dart';
part 'model/update_asset_dto.dart';
part 'model/update_library_dto.dart';
part 'model/update_partner_dto.dart';
-part 'model/update_tag_dto.dart';
part 'model/usage_by_user_dto.dart';
part 'model/user_admin_create_dto.dart';
part 'model/user_admin_delete_dto.dart';
diff --git a/mobile/openapi/lib/api/tags_api.dart b/mobile/openapi/lib/api/tags_api.dart
index e5d1e9c650311..87c9001a3c63e 100644
--- a/mobile/openapi/lib/api/tags_api.dart
+++ b/mobile/openapi/lib/api/tags_api.dart
@@ -16,16 +16,63 @@ class TagsApi {
final ApiClient apiClient;
+ /// Performs an HTTP 'PUT /tags/assets' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [TagBulkAssetsDto] tagBulkAssetsDto (required):
+ Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/tags/assets';
+
+ // ignore: prefer_final_locals
+ Object? postBody = tagBulkAssetsDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'PUT',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [TagBulkAssetsDto] tagBulkAssetsDto (required):
+ Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async {
+ final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TagBulkAssetsResponseDto',) as TagBulkAssetsResponseDto;
+
+ }
+ return null;
+ }
+
/// Performs an HTTP 'POST /tags' operation and returns the [Response].
/// Parameters:
///
- /// * [CreateTagDto] createTagDto (required):
- Future createTagWithHttpInfo(CreateTagDto createTagDto,) async {
+ /// * [TagCreateDto] tagCreateDto (required):
+ Future createTagWithHttpInfo(TagCreateDto tagCreateDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags';
// ignore: prefer_final_locals
- Object? postBody = createTagDto;
+ Object? postBody = tagCreateDto;
final queryParams = [];
final headerParams = {};
@@ -47,9 +94,9 @@ class TagsApi {
/// Parameters:
///
- /// * [CreateTagDto] createTagDto (required):
- Future createTag(CreateTagDto createTagDto,) async {
- final response = await createTagWithHttpInfo(createTagDto,);
+ /// * [TagCreateDto] tagCreateDto (required):
+ Future createTag(TagCreateDto tagCreateDto,) async {
+ final response = await createTagWithHttpInfo(tagCreateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -147,57 +194,6 @@ class TagsApi {
return null;
}
- /// Performs an HTTP 'GET /tags/{id}/assets' operation and returns the [Response].
- /// Parameters:
- ///
- /// * [String] id (required):
- Future getTagAssetsWithHttpInfo(String id,) async {
- // ignore: prefer_const_declarations
- final path = r'/tags/{id}/assets'
- .replaceAll('{id}', id);
-
- // ignore: prefer_final_locals
- Object? postBody;
-
- final queryParams = [];
- final headerParams = {};
- final formParams = {};
-
- const contentTypes = [];
-
-
- return apiClient.invokeAPI(
- path,
- 'GET',
- queryParams,
- postBody,
- headerParams,
- formParams,
- contentTypes.isEmpty ? null : contentTypes.first,
- );
- }
-
- /// Parameters:
- ///
- /// * [String] id (required):
- Future?> getTagAssets(String id,) async {
- final response = await getTagAssetsWithHttpInfo(id,);
- if (response.statusCode >= HttpStatus.badRequest) {
- throw ApiException(response.statusCode, await _decodeBodyBytes(response));
- }
- // When a remote server returns no body with a status of 204, we shall not decode it.
- // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
- // FormatException when trying to decode an empty string.
- if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
- final responseBody = await _decodeBodyBytes(response);
- return (await apiClient.deserializeAsync(responseBody, 'List') as List)
- .cast()
- .toList(growable: false);
-
- }
- return null;
- }
-
/// Performs an HTTP 'GET /tags/{id}' operation and returns the [Response].
/// Parameters:
///
@@ -251,14 +247,14 @@ class TagsApi {
///
/// * [String] id (required):
///
- /// * [AssetIdsDto] assetIdsDto (required):
- Future tagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async {
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
- Object? postBody = assetIdsDto;
+ Object? postBody = bulkIdsDto;
final queryParams = [];
final headerParams = {};
@@ -282,9 +278,9 @@ class TagsApi {
///
/// * [String] id (required):
///
- /// * [AssetIdsDto] assetIdsDto (required):
- Future?> tagAssets(String id, AssetIdsDto assetIdsDto,) async {
- final response = await tagAssetsWithHttpInfo(id, assetIdsDto,);
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async {
+ final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -293,8 +289,8 @@ class TagsApi {
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
- return (await apiClient.deserializeAsync(responseBody, 'List') as List)
- .cast()
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
.toList(growable: false);
}
@@ -306,14 +302,14 @@ class TagsApi {
///
/// * [String] id (required):
///
- /// * [AssetIdsDto] assetIdsDto (required):
- Future untagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async {
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}/assets'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
- Object? postBody = assetIdsDto;
+ Object? postBody = bulkIdsDto;
final queryParams = [];
final headerParams = {};
@@ -337,9 +333,9 @@ class TagsApi {
///
/// * [String] id (required):
///
- /// * [AssetIdsDto] assetIdsDto (required):
- Future?> untagAssets(String id, AssetIdsDto assetIdsDto,) async {
- final response = await untagAssetsWithHttpInfo(id, assetIdsDto,);
+ /// * [BulkIdsDto] bulkIdsDto (required):
+ Future?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async {
+ final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -348,27 +344,27 @@ class TagsApi {
// FormatException when trying to decode an empty string.
if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
final responseBody = await _decodeBodyBytes(response);
- return (await apiClient.deserializeAsync(responseBody, 'List') as List)
- .cast()
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
.toList(growable: false);
}
return null;
}
- /// Performs an HTTP 'PATCH /tags/{id}' operation and returns the [Response].
+ /// Performs an HTTP 'PUT /tags/{id}' operation and returns the [Response].
/// Parameters:
///
/// * [String] id (required):
///
- /// * [UpdateTagDto] updateTagDto (required):
- Future updateTagWithHttpInfo(String id, UpdateTagDto updateTagDto,) async {
+ /// * [TagUpdateDto] tagUpdateDto (required):
+ Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async {
// ignore: prefer_const_declarations
final path = r'/tags/{id}'
.replaceAll('{id}', id);
// ignore: prefer_final_locals
- Object? postBody = updateTagDto;
+ Object? postBody = tagUpdateDto;
final queryParams = [];
final headerParams = {};
@@ -379,7 +375,7 @@ class TagsApi {
return apiClient.invokeAPI(
path,
- 'PATCH',
+ 'PUT',
queryParams,
postBody,
headerParams,
@@ -392,9 +388,9 @@ class TagsApi {
///
/// * [String] id (required):
///
- /// * [UpdateTagDto] updateTagDto (required):
- Future updateTag(String id, UpdateTagDto updateTagDto,) async {
- final response = await updateTagWithHttpInfo(id, updateTagDto,);
+ /// * [TagUpdateDto] tagUpdateDto (required):
+ Future updateTag(String id, TagUpdateDto tagUpdateDto,) async {
+ final response = await updateTagWithHttpInfo(id, tagUpdateDto,);
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -407,4 +403,54 @@ class TagsApi {
}
return null;
}
+
+ /// Performs an HTTP 'PUT /tags' operation and returns the [Response].
+ /// Parameters:
+ ///
+ /// * [TagUpsertDto] tagUpsertDto (required):
+ Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async {
+ // ignore: prefer_const_declarations
+ final path = r'/tags';
+
+ // ignore: prefer_final_locals
+ Object? postBody = tagUpsertDto;
+
+ final queryParams = [];
+ final headerParams = {};
+ final formParams = {};
+
+ const contentTypes = ['application/json'];
+
+
+ return apiClient.invokeAPI(
+ path,
+ 'PUT',
+ queryParams,
+ postBody,
+ headerParams,
+ formParams,
+ contentTypes.isEmpty ? null : contentTypes.first,
+ );
+ }
+
+ /// Parameters:
+ ///
+ /// * [TagUpsertDto] tagUpsertDto (required):
+ Future?> upsertTags(TagUpsertDto tagUpsertDto,) async {
+ final response = await upsertTagsWithHttpInfo(tagUpsertDto,);
+ if (response.statusCode >= HttpStatus.badRequest) {
+ throw ApiException(response.statusCode, await _decodeBodyBytes(response));
+ }
+ // When a remote server returns no body with a status of 204, we shall not decode it.
+ // At the time of writing this, `dart:convert` will throw an "Unexpected end of input"
+ // FormatException when trying to decode an empty string.
+ if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) {
+ final responseBody = await _decodeBodyBytes(response);
+ return (await apiClient.deserializeAsync(responseBody, 'List') as List)
+ .cast()
+ .toList(growable: false);
+
+ }
+ return null;
+ }
}
diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart
index 4acb98bdf2c49..8c94e09bf5c3f 100644
--- a/mobile/openapi/lib/api/timeline_api.dart
+++ b/mobile/openapi/lib/api/timeline_api.dart
@@ -37,12 +37,14 @@ class TimelineApi {
///
/// * [String] personId:
///
+ /// * [String] tagId:
+ ///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
- Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
+ Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final path = r'/timeline/bucket';
@@ -75,6 +77,9 @@ class TimelineApi {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
+ if (tagId != null) {
+ queryParams.addAll(_queryParams('', 'tagId', tagId));
+ }
queryParams.addAll(_queryParams('', 'timeBucket', timeBucket));
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
@@ -120,13 +125,15 @@ class TimelineApi {
///
/// * [String] personId:
///
+ /// * [String] tagId:
+ ///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
- Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
- final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
+ Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
+ final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
@@ -162,12 +169,14 @@ class TimelineApi {
///
/// * [String] personId:
///
+ /// * [String] tagId:
+ ///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
- Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
+ Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
// ignore: prefer_const_declarations
final path = r'/timeline/buckets';
@@ -200,6 +209,9 @@ class TimelineApi {
queryParams.addAll(_queryParams('', 'personId', personId));
}
queryParams.addAll(_queryParams('', 'size', size));
+ if (tagId != null) {
+ queryParams.addAll(_queryParams('', 'tagId', tagId));
+ }
if (userId != null) {
queryParams.addAll(_queryParams('', 'userId', userId));
}
@@ -242,13 +254,15 @@ class TimelineApi {
///
/// * [String] personId:
///
+ /// * [String] tagId:
+ ///
/// * [String] userId:
///
/// * [bool] withPartners:
///
/// * [bool] withStacked:
- Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async {
- final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
+ Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async {
+ final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, );
if (response.statusCode >= HttpStatus.badRequest) {
throw ApiException(response.statusCode, await _decodeBodyBytes(response));
}
diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart
index c9ed2a508d78b..9ec00aecc87aa 100644
--- a/mobile/openapi/lib/api_client.dart
+++ b/mobile/openapi/lib/api_client.dart
@@ -295,8 +295,6 @@ class ApiClient {
return CreateLibraryDto.fromJson(value);
case 'CreateProfileImageResponseDto':
return CreateProfileImageResponseDto.fromJson(value);
- case 'CreateTagDto':
- return CreateTagDto.fromJson(value);
case 'DownloadArchiveInfo':
return DownloadArchiveInfo.fromJson(value);
case 'DownloadInfoDto':
@@ -333,6 +331,10 @@ class ApiClient {
return FileReportFixDto.fromJson(value);
case 'FileReportItemDto':
return FileReportItemDto.fromJson(value);
+ case 'FoldersResponse':
+ return FoldersResponse.fromJson(value);
+ case 'FoldersUpdate':
+ return FoldersUpdate.fromJson(value);
case 'ImageFormat':
return ImageFormatTypeTransformer().decode(value);
case 'JobCommand':
@@ -369,18 +371,18 @@ class ApiClient {
return MapReverseGeocodeResponseDto.fromJson(value);
case 'MapTheme':
return MapThemeTypeTransformer().decode(value);
+ case 'MemoriesResponse':
+ return MemoriesResponse.fromJson(value);
+ case 'MemoriesUpdate':
+ return MemoriesUpdate.fromJson(value);
case 'MemoryCreateDto':
return MemoryCreateDto.fromJson(value);
case 'MemoryLaneResponseDto':
return MemoryLaneResponseDto.fromJson(value);
- case 'MemoryResponse':
- return MemoryResponse.fromJson(value);
case 'MemoryResponseDto':
return MemoryResponseDto.fromJson(value);
case 'MemoryType':
return MemoryTypeTypeTransformer().decode(value);
- case 'MemoryUpdate':
- return MemoryUpdate.fromJson(value);
case 'MemoryUpdateDto':
return MemoryUpdateDto.fromJson(value);
case 'MergePersonDto':
@@ -403,8 +405,12 @@ class ApiClient {
return PathEntityTypeTypeTransformer().decode(value);
case 'PathType':
return PathTypeTypeTransformer().decode(value);
+ case 'PeopleResponse':
+ return PeopleResponse.fromJson(value);
case 'PeopleResponseDto':
return PeopleResponseDto.fromJson(value);
+ case 'PeopleUpdate':
+ return PeopleUpdate.fromJson(value);
case 'PeopleUpdateDto':
return PeopleUpdateDto.fromJson(value);
case 'PeopleUpdateItem':
@@ -429,10 +435,10 @@ class ApiClient {
return PurchaseUpdate.fromJson(value);
case 'QueueStatusDto':
return QueueStatusDto.fromJson(value);
- case 'RatingResponse':
- return RatingResponse.fromJson(value);
- case 'RatingUpdate':
- return RatingUpdate.fromJson(value);
+ case 'RatingsResponse':
+ return RatingsResponse.fromJson(value);
+ case 'RatingsUpdate':
+ return RatingsUpdate.fromJson(value);
case 'ReactionLevel':
return ReactionLevelTypeTransformer().decode(value);
case 'ReactionType':
@@ -491,6 +497,8 @@ class ApiClient {
return SmartInfoResponseDto.fromJson(value);
case 'SmartSearchDto':
return SmartSearchDto.fromJson(value);
+ case 'SourceType':
+ return SourceTypeTypeTransformer().decode(value);
case 'StackCreateDto':
return StackCreateDto.fromJson(value);
case 'StackResponseDto':
@@ -501,6 +509,8 @@ class ApiClient {
return SystemConfigDto.fromJson(value);
case 'SystemConfigFFmpegDto':
return SystemConfigFFmpegDto.fromJson(value);
+ case 'SystemConfigFacesDto':
+ return SystemConfigFacesDto.fromJson(value);
case 'SystemConfigImageDto':
return SystemConfigImageDto.fromJson(value);
case 'SystemConfigJobDto':
@@ -517,6 +527,8 @@ class ApiClient {
return SystemConfigMachineLearningDto.fromJson(value);
case 'SystemConfigMapDto':
return SystemConfigMapDto.fromJson(value);
+ case 'SystemConfigMetadataDto':
+ return SystemConfigMetadataDto.fromJson(value);
case 'SystemConfigNewVersionCheckDto':
return SystemConfigNewVersionCheckDto.fromJson(value);
case 'SystemConfigNotificationsDto':
@@ -543,10 +555,22 @@ class ApiClient {
return SystemConfigTrashDto.fromJson(value);
case 'SystemConfigUserDto':
return SystemConfigUserDto.fromJson(value);
+ case 'TagBulkAssetsDto':
+ return TagBulkAssetsDto.fromJson(value);
+ case 'TagBulkAssetsResponseDto':
+ return TagBulkAssetsResponseDto.fromJson(value);
+ case 'TagCreateDto':
+ return TagCreateDto.fromJson(value);
case 'TagResponseDto':
return TagResponseDto.fromJson(value);
- case 'TagTypeEnum':
- return TagTypeEnumTypeTransformer().decode(value);
+ case 'TagUpdateDto':
+ return TagUpdateDto.fromJson(value);
+ case 'TagUpsertDto':
+ return TagUpsertDto.fromJson(value);
+ case 'TagsResponse':
+ return TagsResponse.fromJson(value);
+ case 'TagsUpdate':
+ return TagsUpdate.fromJson(value);
case 'TimeBucketResponseDto':
return TimeBucketResponseDto.fromJson(value);
case 'TimeBucketSize':
@@ -567,8 +591,6 @@ class ApiClient {
return UpdateLibraryDto.fromJson(value);
case 'UpdatePartnerDto':
return UpdatePartnerDto.fromJson(value);
- case 'UpdateTagDto':
- return UpdateTagDto.fromJson(value);
case 'UsageByUserDto':
return UsageByUserDto.fromJson(value);
case 'UserAdminCreateDto':
diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart
index 7f46e145b15eb..8dcef880f59a4 100644
--- a/mobile/openapi/lib/api_helper.dart
+++ b/mobile/openapi/lib/api_helper.dart
@@ -127,8 +127,8 @@ String parameterToString(dynamic value) {
if (value is SharedLinkType) {
return SharedLinkTypeTypeTransformer().encode(value).toString();
}
- if (value is TagTypeEnum) {
- return TagTypeEnumTypeTransformer().encode(value).toString();
+ if (value is SourceType) {
+ return SourceTypeTypeTransformer().encode(value).toString();
}
if (value is TimeBucketSize) {
return TimeBucketSizeTypeTransformer().encode(value).toString();
diff --git a/mobile/openapi/lib/model/asset_face_response_dto.dart b/mobile/openapi/lib/model/asset_face_response_dto.dart
index 812b165caa0eb..7a8588ce5c4af 100644
--- a/mobile/openapi/lib/model/asset_face_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_face_response_dto.dart
@@ -21,6 +21,7 @@ class AssetFaceResponseDto {
required this.imageHeight,
required this.imageWidth,
required this.person,
+ this.sourceType,
});
int boundingBoxX1;
@@ -39,6 +40,14 @@ class AssetFaceResponseDto {
PersonResponseDto? person;
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ SourceType? sourceType;
+
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto &&
other.boundingBoxX1 == boundingBoxX1 &&
@@ -48,7 +57,8 @@ class AssetFaceResponseDto {
other.id == id &&
other.imageHeight == imageHeight &&
other.imageWidth == imageWidth &&
- other.person == person;
+ other.person == person &&
+ other.sourceType == sourceType;
@override
int get hashCode =>
@@ -60,10 +70,11 @@ class AssetFaceResponseDto {
(id.hashCode) +
(imageHeight.hashCode) +
(imageWidth.hashCode) +
- (person == null ? 0 : person!.hashCode);
+ (person == null ? 0 : person!.hashCode) +
+ (sourceType == null ? 0 : sourceType!.hashCode);
@override
- String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]';
+ String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person, sourceType=$sourceType]';
Map toJson() {
final json = {};
@@ -79,6 +90,11 @@ class AssetFaceResponseDto {
} else {
// json[r'person'] = null;
}
+ if (this.sourceType != null) {
+ json[r'sourceType'] = this.sourceType;
+ } else {
+ // json[r'sourceType'] = null;
+ }
return json;
}
@@ -98,6 +114,7 @@ class AssetFaceResponseDto {
imageHeight: mapValueOfType(json, r'imageHeight')!,
imageWidth: mapValueOfType(json, r'imageWidth')!,
person: PersonResponseDto.fromJson(json[r'person']),
+ sourceType: SourceType.fromJson(json[r'sourceType']),
);
}
return null;
diff --git a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
index 893f8ff3530e1..ecfe06bd7d6ce 100644
--- a/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_face_without_person_response_dto.dart
@@ -20,6 +20,7 @@ class AssetFaceWithoutPersonResponseDto {
required this.id,
required this.imageHeight,
required this.imageWidth,
+ this.sourceType,
});
int boundingBoxX1;
@@ -36,6 +37,14 @@ class AssetFaceWithoutPersonResponseDto {
int imageWidth;
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ SourceType? sourceType;
+
@override
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
other.boundingBoxX1 == boundingBoxX1 &&
@@ -44,7 +53,8 @@ class AssetFaceWithoutPersonResponseDto {
other.boundingBoxY2 == boundingBoxY2 &&
other.id == id &&
other.imageHeight == imageHeight &&
- other.imageWidth == imageWidth;
+ other.imageWidth == imageWidth &&
+ other.sourceType == sourceType;
@override
int get hashCode =>
@@ -55,10 +65,11 @@ class AssetFaceWithoutPersonResponseDto {
(boundingBoxY2.hashCode) +
(id.hashCode) +
(imageHeight.hashCode) +
- (imageWidth.hashCode);
+ (imageWidth.hashCode) +
+ (sourceType == null ? 0 : sourceType!.hashCode);
@override
- String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]';
+ String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, sourceType=$sourceType]';
Map toJson() {
final json = {};
@@ -69,6 +80,11 @@ class AssetFaceWithoutPersonResponseDto {
json[r'id'] = this.id;
json[r'imageHeight'] = this.imageHeight;
json[r'imageWidth'] = this.imageWidth;
+ if (this.sourceType != null) {
+ json[r'sourceType'] = this.sourceType;
+ } else {
+ // json[r'sourceType'] = null;
+ }
return json;
}
@@ -87,6 +103,7 @@ class AssetFaceWithoutPersonResponseDto {
id: mapValueOfType(json, r'id')!,
imageHeight: mapValueOfType(json, r'imageHeight')!,
imageWidth: mapValueOfType(json, r'imageWidth')!,
+ sourceType: SourceType.fromJson(json[r'sourceType']),
);
}
return null;
diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart
index 561a42cc852cf..bfb461efdc4c5 100644
--- a/mobile/openapi/lib/model/asset_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_response_dto.dart
@@ -36,7 +36,7 @@ class AssetResponseDto {
this.owner,
required this.ownerId,
this.people = const [],
- required this.resized,
+ this.resized,
this.smartInfo,
this.stack,
this.tags = const [],
@@ -112,7 +112,14 @@ class AssetResponseDto {
List people;
- bool resized;
+ /// This property was deprecated in v1.113.0
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? resized;
///
/// Please note: This property should have been non-nullable! Since the specification file
@@ -194,7 +201,7 @@ class AssetResponseDto {
(owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) +
(people.hashCode) +
- (resized.hashCode) +
+ (resized == null ? 0 : resized!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) +
@@ -255,7 +262,11 @@ class AssetResponseDto {
}
json[r'ownerId'] = this.ownerId;
json[r'people'] = this.people;
+ if (this.resized != null) {
json[r'resized'] = this.resized;
+ } else {
+ // json[r'resized'] = null;
+ }
if (this.smartInfo != null) {
json[r'smartInfo'] = this.smartInfo;
} else {
@@ -309,7 +320,7 @@ class AssetResponseDto {
owner: UserResponseDto.fromJson(json[r'owner']),
ownerId: mapValueOfType(json, r'ownerId')!,
people: PersonWithFacesResponseDto.listFromJson(json[r'people']),
- resized: mapValueOfType(json, r'resized')!,
+ resized: mapValueOfType(json, r'resized'),
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
@@ -380,7 +391,6 @@ class AssetResponseDto {
'originalFileName',
'originalPath',
'ownerId',
- 'resized',
'thumbhash',
'type',
'updatedAt',
diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart
new file mode 100644
index 0000000000000..5bfc4c793deed
--- /dev/null
+++ b/mobile/openapi/lib/model/folders_response.dart
@@ -0,0 +1,106 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class FoldersResponse {
+ /// Returns a new [FoldersResponse] instance.
+ FoldersResponse({
+ this.enabled = false,
+ this.sidebarWeb = false,
+ });
+
+ bool enabled;
+
+ bool sidebarWeb;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is FoldersResponse &&
+ other.enabled == enabled &&
+ other.sidebarWeb == sidebarWeb;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (enabled.hashCode) +
+ (sidebarWeb.hashCode);
+
+ @override
+ String toString() => 'FoldersResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]';
+
+ Map toJson() {
+ final json = {};
+ json[r'enabled'] = this.enabled;
+ json[r'sidebarWeb'] = this.sidebarWeb;
+ return json;
+ }
+
+ /// Returns a new [FoldersResponse] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static FoldersResponse? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return FoldersResponse(
+ enabled: mapValueOfType(json, r'enabled')!,
+ sidebarWeb: mapValueOfType(json, r'sidebarWeb')!,
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = FoldersResponse.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = FoldersResponse.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of FoldersResponse-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = FoldersResponse.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'enabled',
+ 'sidebarWeb',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart
new file mode 100644
index 0000000000000..088c98a4d8fd2
--- /dev/null
+++ b/mobile/openapi/lib/model/folders_update.dart
@@ -0,0 +1,124 @@
+//
+// AUTO-GENERATED FILE, DO NOT MODIFY!
+//
+// @dart=2.18
+
+// ignore_for_file: unused_element, unused_import
+// ignore_for_file: always_put_required_named_parameters_first
+// ignore_for_file: constant_identifier_names
+// ignore_for_file: lines_longer_than_80_chars
+
+part of openapi.api;
+
+class FoldersUpdate {
+ /// Returns a new [FoldersUpdate] instance.
+ FoldersUpdate({
+ this.enabled,
+ this.sidebarWeb,
+ });
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? enabled;
+
+ ///
+ /// Please note: This property should have been non-nullable! Since the specification file
+ /// does not include a default value (using the "default:" property), however, the generated
+ /// source code must fall back to having a nullable type.
+ /// Consider adding a "default:" property in the specification file to hide this note.
+ ///
+ bool? sidebarWeb;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is FoldersUpdate &&
+ other.enabled == enabled &&
+ other.sidebarWeb == sidebarWeb;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (enabled == null ? 0 : enabled!.hashCode) +
+ (sidebarWeb == null ? 0 : sidebarWeb!.hashCode);
+
+ @override
+ String toString() => 'FoldersUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]';
+
+ Map toJson() {
+ final json = {};
+ if (this.enabled != null) {
+ json[r'enabled'] = this.enabled;
+ } else {
+ // json[r'enabled'] = null;
+ }
+ if (this.sidebarWeb != null) {
+ json[r'sidebarWeb'] = this.sidebarWeb;
+ } else {
+ // json[r'sidebarWeb'] = null;
+ }
+ return json;
+ }
+
+ /// Returns a new [FoldersUpdate] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static FoldersUpdate? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return FoldersUpdate(
+ enabled: mapValueOfType(json, r'enabled'),
+ sidebarWeb: mapValueOfType(json, r'sidebarWeb'),
+ );
+ }
+ return null;
+ }
+
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
+ if (json is List && json.isNotEmpty) {
+ for (final row in json) {
+ final value = FoldersUpdate.fromJson(row);
+ if (value != null) {
+ result.add(value);
+ }
+ }
+ }
+ return result.toList(growable: growable);
+ }
+
+ static Map mapFromJson(dynamic json) {
+ final map = {};
+ if (json is Map && json.isNotEmpty) {
+ json = json.cast(); // ignore: parameter_assignments
+ for (final entry in json.entries) {
+ final value = FoldersUpdate.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of FoldersUpdate-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
+ if (json is Map && json.isNotEmpty) {
+ // ignore: parameter_assignments
+ json = json.cast();
+ for (final entry in json.entries) {
+ map[entry.key] = FoldersUpdate.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ };
+}
+
diff --git a/mobile/openapi/lib/model/memory_response.dart b/mobile/openapi/lib/model/memories_response.dart
similarity index 62%
rename from mobile/openapi/lib/model/memory_response.dart
rename to mobile/openapi/lib/model/memories_response.dart
index fb34bc1518876..e215a66a03f67 100644
--- a/mobile/openapi/lib/model/memory_response.dart
+++ b/mobile/openapi/lib/model/memories_response.dart
@@ -10,16 +10,16 @@
part of openapi.api;
-class MemoryResponse {
- /// Returns a new [MemoryResponse] instance.
- MemoryResponse({
- required this.enabled,
+class MemoriesResponse {
+ /// Returns a new [MemoriesResponse] instance.
+ MemoriesResponse({
+ this.enabled = true,
});
bool enabled;
@override
- bool operator ==(Object other) => identical(this, other) || other is MemoryResponse &&
+ bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse &&
other.enabled == enabled;
@override
@@ -28,7 +28,7 @@ class MemoryResponse {
(enabled.hashCode);
@override
- String toString() => 'MemoryResponse[enabled=$enabled]';
+ String toString() => 'MemoriesResponse[enabled=$enabled]';
Map toJson() {
final json = {};
@@ -36,25 +36,25 @@ class MemoryResponse {
return json;
}
- /// Returns a new [MemoryResponse] instance and imports its values from
+ /// Returns a new [MemoriesResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
- static MemoryResponse? fromJson(dynamic value) {
+ static MemoriesResponse? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast();
- return MemoryResponse(
+ return MemoriesResponse(
enabled: mapValueOfType(json, r'enabled')!,
);
}
return null;
}
- static List listFromJson(dynamic json, {bool growable = false,}) {
- final result = [];
+ static List listFromJson(dynamic json, {bool growable = false,}) {
+ final result = [];
if (json is List && json.isNotEmpty) {
for (final row in json) {
- final value = MemoryResponse.fromJson(row);
+ final value = MemoriesResponse.fromJson(row);
if (value != null) {
result.add(value);
}
@@ -63,12 +63,12 @@ class MemoryResponse {
return result.toList(growable: growable);
}
- static Map mapFromJson(dynamic json) {
- final map = {};
+ static Map mapFromJson(dynamic json) {
+ final map = {};
if (json is Map && json.isNotEmpty) {
json = json.cast(); // ignore: parameter_assignments
for (final entry in json.entries) {
- final value = MemoryResponse.fromJson(entry.value);
+ final value = MemoriesResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@@ -77,14 +77,14 @@ class MemoryResponse {
return map;
}
- // maps a json object with a list of MemoryResponse-objects as value to a dart map
- static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
- final map = >{};
+ // maps a json object with a list of MemoriesResponse-objects as value to a dart map
+ static Map> mapListFromJson(dynamic json, {bool growable = false,}) {
+ final map = >{};
if (json is Map && json.isNotEmpty) {
// ignore: parameter_assignments
json = json.cast();
for (final entry in json.entries) {
- map[entry.key] = MemoryResponse.listFromJson(entry.value, growable: growable,);
+ map[entry.key] = MemoriesResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/memories_update.dart
similarity index 68%
rename from mobile/openapi/lib/model/memory_update.dart
rename to mobile/openapi/lib/model/memories_update.dart
index f2529186c0432..d30949136197e 100644
--- a/mobile/openapi/lib/model/memory_update.dart
+++ b/mobile/openapi/lib/model/memories_update.dart
@@ -10,9 +10,9 @@
part of openapi.api;
-class MemoryUpdate {
- /// Returns a new [MemoryUpdate] instance.
- MemoryUpdate({
+class MemoriesUpdate {
+ /// Returns a new [MemoriesUpdate] instance.
+ MemoriesUpdate({
this.enabled,
});
@@ -25,7 +25,7 @@ class MemoryUpdate {
bool? enabled;
@override
- bool operator ==(Object other) => identical(this, other) || other is MemoryUpdate &&
+ bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate &&
other.enabled == enabled;
@override
@@ -34,7 +34,7 @@ class MemoryUpdate {
(enabled == null ? 0 : enabled!.hashCode);
@override
- String toString() => 'MemoryUpdate[enabled=$enabled]';
+ String toString() => 'MemoriesUpdate[enabled=$enabled]';
Map toJson() {
final json =