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/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..6be26c9bbe62f 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -17,47 +17,58 @@ 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"
+
+ 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 +104,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 +122,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 +143,107 @@ 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:
+ name: Docker Build & Push Success
+ needs: [build_and_push_ml, build_and_push_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) }}"
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 6668976bcf0fe..fc03b24d085b7 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -29,17 +29,6 @@ jobs:
ref: ${{ steps.push-tag.outputs.commit_long_sha }}
steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- token: ${{ secrets.ORG_RELEASE_TOKEN }}
-
- - name: Install Poetry
- run: pipx install poetry
-
- - name: Bump version
- run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
-
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
@@ -47,6 +36,17 @@ jobs:
app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
+ - name: Checkout
+ uses: actions/checkout@v4
+ with:
+ token: ${{ steps.generate-token.outputs.token }}
+
+ - name: Install Poetry
+ run: pipx install poetry
+
+ - name: Bump version
+ run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}"
+
- name: Commit and tag
id: push-tag
uses: EndBug/add-and-commit@v9
@@ -55,7 +55,6 @@ jobs:
message: 'chore: version ${{ env.IMMICH_VERSION }}'
tag: ${{ env.IMMICH_VERSION }}
push: true
- github-token: ${{ steps.generate-token.outputs.token }}
build_mobile:
uses: ./.github/workflows/build-mobile.yml
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/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 cce73afa37d1b..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",
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 733905e01dad7..509674f328b35 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -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/docs/FAQ.mdx b/docs/docs/FAQ.mdx
index 501a67d5f2a58..b1a24e1788a2f 100644
--- a/docs/docs/FAQ.mdx
+++ b/docs/docs/FAQ.mdx
@@ -67,6 +67,11 @@ 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?
Yes, with an [External Library](/docs/features/libraries.md).
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/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/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 0f30bac60f66e..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',
@@ -75,8 +80,7 @@ const projects: CommunityProjectProps[] = [
},
{
title: 'Immich Power Tools',
- description:
- 'An unofficial immich client providing tools to speed up your workflows in Immich to organize your people and albums.',
+ description: 'Power tools for organizing your immich library.',
url: 'https://github.com/varun-raj/immich-power-tools',
},
];
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/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 be072e44f3e23..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",
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 82ce17865a565..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,
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/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 31949aee84ccb..bd09bd8469e67 100644
--- a/machine-learning/poetry.lock
+++ b/machine-learning/poetry.lock
@@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
[[package]]
name = "fastapi-slim"
-version = "0.112.1"
+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.1-py3-none-any.whl", hash = "sha256:cc227cf9402d0ba54a24f80eb205c33bcb25d3ea18d53fdac3fd76ea5af8e76d"},
- {file = "fastapi_slim-0.112.1.tar.gz", hash = "sha256:876ebd24e72273986709db2d469b75dc18f04c3ab9140ffd78b29d7785d26687"},
+ {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]
@@ -695,8 +695,8 @@ 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,6 +1233,7 @@ 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"
@@ -1530,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
-version = "2.31.3"
+version = "2.31.5"
description = "Developer-friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
- {file = "locust-2.31.3-py3-none-any.whl", hash = "sha256:03122e007519b371a5a553d578af502826755de83551d79ea8a412ea1c660115"},
- {file = "locust-2.31.3.tar.gz", hash = "sha256:25f4603f24afa11ef1ee1f26b1c86a232eb9a1140be30b2a4642c12d7a7af8ae"},
+ {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]
@@ -2373,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]
@@ -2494,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)"]
@@ -2512,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]
@@ -2526,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"
@@ -2815,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]
@@ -2833,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
-version = "0.6.2"
+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.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
- {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
- {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
- {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
- {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
- {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
- {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
- {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
- {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
+ {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]]
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/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/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/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 5d3ae5bc22677..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;
diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart
index 17508cba5153e..c4f258e259129 100644
--- a/mobile/lib/services/asset.service.dart
+++ b/mobile/lib/services/asset.service.dart
@@ -8,7 +8,6 @@ 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/store.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';
@@ -309,18 +308,6 @@ class AssetService {
useTimeFilter: false,
);
- final duplicates = await _apiService.assetsApi.checkExistingAssets(
- CheckExistingAssetsDto(
- deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
- deviceId: Store.get(StoreKey.deviceId),
- ),
- );
-
- if (duplicates != null) {
- candidates
- .removeWhere((c) => !duplicates.existingIds.contains(c.asset.id));
- }
-
await refreshRemoteAssets();
final remoteAssets = await _db.assets
.where()
diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart
index b27ed34b946ce..fc3feb174d582 100644
--- a/mobile/lib/services/background.service.dart
+++ b/mobile/lib/services/background.service.dart
@@ -349,6 +349,7 @@ 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();
diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart
index 12edd14d609ca..858499443ee1d 100644
--- a/mobile/lib/services/backup.service.dart
+++ b/mobile/lib/services/backup.service.dart
@@ -484,7 +484,7 @@ class BackupService {
),
);
- if (shouldSyncAlbums && !isDuplicate) {
+ if (shouldSyncAlbums) {
await _albumService.syncUploadAlbums(
candidate.albumNames,
[responseBody['id'] as String],
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/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart
index 7a2f7396eb3ab..349b2322afac1 100644
--- a/mobile/lib/utils/openapi_patching.dart
+++ b/mobile/lib/utils/openapi_patching.dart
@@ -4,14 +4,30 @@ dynamic upgradeDto(dynamic value, String targetType) {
switch (targetType) {
case 'UserPreferencesResponseDto':
if (value is Map) {
- if (value['rating'] == null) {
- value['rating'] = RatingResponse().toJson();
- }
-
- if (value['download']['includeEmbeddedVideos'] == null) {
- value['download']['includeEmbeddedVideos'] = false;
- }
+ 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/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/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/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 4217e133b8c34..bfb461efdc4c5 100644
--- a/mobile/openapi/lib/model/asset_response_dto.dart
+++ b/mobile/openapi/lib/model/asset_response_dto.dart
@@ -36,6 +36,7 @@ class AssetResponseDto {
this.owner,
required this.ownerId,
this.people = const [],
+ this.resized,
this.smartInfo,
this.stack,
this.tags = const [],
@@ -111,6 +112,15 @@ class AssetResponseDto {
List people;
+ /// 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
/// does not include a default value (using the "default:" property), however, the generated
@@ -156,6 +166,7 @@ class AssetResponseDto {
other.owner == owner &&
other.ownerId == ownerId &&
_deepEquality.equals(other.people, people) &&
+ other.resized == resized &&
other.smartInfo == smartInfo &&
other.stack == stack &&
_deepEquality.equals(other.tags, tags) &&
@@ -190,6 +201,7 @@ class AssetResponseDto {
(owner == null ? 0 : owner!.hashCode) +
(ownerId.hashCode) +
(people.hashCode) +
+ (resized == null ? 0 : resized!.hashCode) +
(smartInfo == null ? 0 : smartInfo!.hashCode) +
(stack == null ? 0 : stack!.hashCode) +
(tags.hashCode) +
@@ -199,7 +211,7 @@ class AssetResponseDto {
(updatedAt.hashCode);
@override
- String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
+ String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]';
Map toJson() {
final json = {};
@@ -250,6 +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 {
@@ -303,6 +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'),
smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']),
stack: AssetStackResponseDto.fromJson(json[r'stack']),
tags: TagResponseDto.listFromJson(json[r'tags']),
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 = {};
@@ -46,25 +46,25 @@ class MemoryUpdate {
return json;
}
- /// Returns a new [MemoryUpdate] instance and imports its values from
+ /// Returns a new [MemoriesUpdate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
- static MemoryUpdate? fromJson(dynamic value) {
+ static MemoriesUpdate? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast();
- return MemoryUpdate(
+ return MemoriesUpdate(
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 = MemoryUpdate.fromJson(row);
+ final value = MemoriesUpdate.fromJson(row);
if (value != null) {
result.add(value);
}
@@ -73,12 +73,12 @@ class MemoryUpdate {
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 = MemoryUpdate.fromJson(entry.value);
+ final value = MemoriesUpdate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@@ -87,14 +87,14 @@ class MemoryUpdate {
return map;
}
- // maps a json object with a list of MemoryUpdate-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 MemoriesUpdate-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] = MemoryUpdate.listFromJson(entry.value, growable: growable,);
+ map[entry.key] = MemoriesUpdate.listFromJson(entry.value, growable: growable,);
}
}
return map;
diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart
new file mode 100644
index 0000000000000..e12f86eeab5ba
--- /dev/null
+++ b/mobile/openapi/lib/model/people_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 PeopleResponse {
+ /// Returns a new [PeopleResponse] instance.
+ PeopleResponse({
+ this.enabled = true,
+ this.sidebarWeb = false,
+ });
+
+ bool enabled;
+
+ bool sidebarWeb;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is PeopleResponse &&
+ other.enabled == enabled &&
+ other.sidebarWeb == sidebarWeb;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (enabled.hashCode) +
+ (sidebarWeb.hashCode);
+
+ @override
+ String toString() => 'PeopleResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]';
+
+ Map toJson() {
+ final json = {};
+ json[r'enabled'] = this.enabled;
+ json[r'sidebarWeb'] = this.sidebarWeb;
+ return json;
+ }
+
+ /// Returns a new [PeopleResponse] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static PeopleResponse? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return PeopleResponse(
+ 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 = PeopleResponse.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 = PeopleResponse.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of PeopleResponse-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] = PeopleResponse.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/people_update.dart b/mobile/openapi/lib/model/people_update.dart
new file mode 100644
index 0000000000000..7803e6297036a
--- /dev/null
+++ b/mobile/openapi/lib/model/people_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 PeopleUpdate {
+ /// Returns a new [PeopleUpdate] instance.
+ PeopleUpdate({
+ 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 PeopleUpdate &&
+ 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() => 'PeopleUpdate[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 [PeopleUpdate] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static PeopleUpdate? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return PeopleUpdate(
+ 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 = PeopleUpdate.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 = PeopleUpdate.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of PeopleUpdate-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] = PeopleUpdate.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/permission.dart b/mobile/openapi/lib/model/permission.dart
index 3f89c9826d645..1244a434b6ee7 100644
--- a/mobile/openapi/lib/model/permission.dart
+++ b/mobile/openapi/lib/model/permission.dart
@@ -96,6 +96,7 @@ class Permission {
static const tagPeriodRead = Permission._(r'tag.read');
static const tagPeriodUpdate = Permission._(r'tag.update');
static const tagPeriodDelete = Permission._(r'tag.delete');
+ static const tagPeriodAsset = Permission._(r'tag.asset');
static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create');
static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read');
static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update');
@@ -176,6 +177,7 @@ class Permission {
tagPeriodRead,
tagPeriodUpdate,
tagPeriodDelete,
+ tagPeriodAsset,
adminPeriodUserPeriodCreate,
adminPeriodUserPeriodRead,
adminPeriodUserPeriodUpdate,
@@ -291,6 +293,7 @@ class PermissionTypeTransformer {
case r'tag.read': return Permission.tagPeriodRead;
case r'tag.update': return Permission.tagPeriodUpdate;
case r'tag.delete': return Permission.tagPeriodDelete;
+ case r'tag.asset': return Permission.tagPeriodAsset;
case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate;
case r'admin.user.read': return Permission.adminPeriodUserPeriodRead;
case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate;
diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/ratings_response.dart
similarity index 63%
rename from mobile/openapi/lib/model/rating_response.dart
rename to mobile/openapi/lib/model/ratings_response.dart
index 31505550eff9c..c8791aa91a5ee 100644
--- a/mobile/openapi/lib/model/rating_response.dart
+++ b/mobile/openapi/lib/model/ratings_response.dart
@@ -10,16 +10,16 @@
part of openapi.api;
-class RatingResponse {
- /// Returns a new [RatingResponse] instance.
- RatingResponse({
+class RatingsResponse {
+ /// Returns a new [RatingsResponse] instance.
+ RatingsResponse({
this.enabled = false,
});
bool enabled;
@override
- bool operator ==(Object other) => identical(this, other) || other is RatingResponse &&
+ bool operator ==(Object other) => identical(this, other) || other is RatingsResponse &&
other.enabled == enabled;
@override
@@ -28,7 +28,7 @@ class RatingResponse {
(enabled.hashCode);
@override
- String toString() => 'RatingResponse[enabled=$enabled]';
+ String toString() => 'RatingsResponse[enabled=$enabled]';
Map toJson() {
final json = {};
@@ -36,25 +36,25 @@ class RatingResponse {
return json;
}
- /// Returns a new [RatingResponse] instance and imports its values from
+ /// Returns a new [RatingsResponse] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
- static RatingResponse? fromJson(dynamic value) {
+ static RatingsResponse? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast();
- return RatingResponse(
+ return RatingsResponse(
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 = RatingResponse.fromJson(row);
+ final value = RatingsResponse.fromJson(row);
if (value != null) {
result.add(value);
}
@@ -63,12 +63,12 @@ class RatingResponse {
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 = RatingResponse.fromJson(entry.value);
+ final value = RatingsResponse.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@@ -77,14 +77,14 @@ class RatingResponse {
return map;
}
- // maps a json object with a list of RatingResponse-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 RatingsResponse-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] = RatingResponse.listFromJson(entry.value, growable: growable,);
+ map[entry.key] = RatingsResponse.listFromJson(entry.value, growable: growable,);
}
}
return map;
diff --git a/mobile/openapi/lib/model/rating_update.dart b/mobile/openapi/lib/model/ratings_update.dart
similarity index 69%
rename from mobile/openapi/lib/model/rating_update.dart
rename to mobile/openapi/lib/model/ratings_update.dart
index bb8f7eadc2f55..bde51bad1b360 100644
--- a/mobile/openapi/lib/model/rating_update.dart
+++ b/mobile/openapi/lib/model/ratings_update.dart
@@ -10,9 +10,9 @@
part of openapi.api;
-class RatingUpdate {
- /// Returns a new [RatingUpdate] instance.
- RatingUpdate({
+class RatingsUpdate {
+ /// Returns a new [RatingsUpdate] instance.
+ RatingsUpdate({
this.enabled,
});
@@ -25,7 +25,7 @@ class RatingUpdate {
bool? enabled;
@override
- bool operator ==(Object other) => identical(this, other) || other is RatingUpdate &&
+ bool operator ==(Object other) => identical(this, other) || other is RatingsUpdate &&
other.enabled == enabled;
@override
@@ -34,7 +34,7 @@ class RatingUpdate {
(enabled == null ? 0 : enabled!.hashCode);
@override
- String toString() => 'RatingUpdate[enabled=$enabled]';
+ String toString() => 'RatingsUpdate[enabled=$enabled]';
Map toJson() {
final json = {};
@@ -46,25 +46,25 @@ class RatingUpdate {
return json;
}
- /// Returns a new [RatingUpdate] instance and imports its values from
+ /// Returns a new [RatingsUpdate] instance and imports its values from
/// [value] if it's a [Map], null otherwise.
// ignore: prefer_constructors_over_static_methods
- static RatingUpdate? fromJson(dynamic value) {
+ static RatingsUpdate? fromJson(dynamic value) {
if (value is Map) {
final json = value.cast();
- return RatingUpdate(
+ return RatingsUpdate(
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 = RatingUpdate.fromJson(row);
+ final value = RatingsUpdate.fromJson(row);
if (value != null) {
result.add(value);
}
@@ -73,12 +73,12 @@ class RatingUpdate {
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 = RatingUpdate.fromJson(entry.value);
+ final value = RatingsUpdate.fromJson(entry.value);
if (value != null) {
map[entry.key] = value;
}
@@ -87,14 +87,14 @@ class RatingUpdate {
return map;
}
- // maps a json object with a list of RatingUpdate-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 RatingsUpdate-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] = RatingUpdate.listFromJson(entry.value, growable: growable,);
+ map[entry.key] = RatingsUpdate.listFromJson(entry.value, growable: growable,);
}
}
return map;
diff --git a/mobile/openapi/lib/model/server_features_dto.dart b/mobile/openapi/lib/model/server_features_dto.dart
index 3e5466237a28f..0a7d8a4b4774a 100644
--- a/mobile/openapi/lib/model/server_features_dto.dart
+++ b/mobile/openapi/lib/model/server_features_dto.dart
@@ -17,6 +17,7 @@ class ServerFeaturesDto {
required this.duplicateDetection,
required this.email,
required this.facialRecognition,
+ required this.importFaces,
required this.map,
required this.oauth,
required this.oauthAutoLaunch,
@@ -36,6 +37,8 @@ class ServerFeaturesDto {
bool facialRecognition;
+ bool importFaces;
+
bool map;
bool oauth;
@@ -60,6 +63,7 @@ class ServerFeaturesDto {
other.duplicateDetection == duplicateDetection &&
other.email == email &&
other.facialRecognition == facialRecognition &&
+ other.importFaces == importFaces &&
other.map == map &&
other.oauth == oauth &&
other.oauthAutoLaunch == oauthAutoLaunch &&
@@ -77,6 +81,7 @@ class ServerFeaturesDto {
(duplicateDetection.hashCode) +
(email.hashCode) +
(facialRecognition.hashCode) +
+ (importFaces.hashCode) +
(map.hashCode) +
(oauth.hashCode) +
(oauthAutoLaunch.hashCode) +
@@ -88,7 +93,7 @@ class ServerFeaturesDto {
(trash.hashCode);
@override
- String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
+ String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
Map toJson() {
final json = {};
@@ -96,6 +101,7 @@ class ServerFeaturesDto {
json[r'duplicateDetection'] = this.duplicateDetection;
json[r'email'] = this.email;
json[r'facialRecognition'] = this.facialRecognition;
+ json[r'importFaces'] = this.importFaces;
json[r'map'] = this.map;
json[r'oauth'] = this.oauth;
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
@@ -120,6 +126,7 @@ class ServerFeaturesDto {
duplicateDetection: mapValueOfType(json, r'duplicateDetection')!,
email: mapValueOfType(json, r'email')!,
facialRecognition: mapValueOfType(json, r'facialRecognition')!,
+ importFaces: mapValueOfType(json, r'importFaces')!,
map: mapValueOfType(json, r'map')!,
oauth: mapValueOfType(json, r'oauth')!,
oauthAutoLaunch: mapValueOfType(json, r'oauthAutoLaunch')!,
@@ -180,6 +187,7 @@ class ServerFeaturesDto {
'duplicateDetection',
'email',
'facialRecognition',
+ 'importFaces',
'map',
'oauth',
'oauthAutoLaunch',
diff --git a/mobile/openapi/lib/model/tag_type_enum.dart b/mobile/openapi/lib/model/source_type.dart
similarity index 50%
rename from mobile/openapi/lib/model/tag_type_enum.dart
rename to mobile/openapi/lib/model/source_type.dart
index 3f2e723796b81..13c450b010d5c 100644
--- a/mobile/openapi/lib/model/tag_type_enum.dart
+++ b/mobile/openapi/lib/model/source_type.dart
@@ -11,9 +11,9 @@
part of openapi.api;
-class TagTypeEnum {
+class SourceType {
/// Instantiate a new enum with the provided [value].
- const TagTypeEnum._(this.value);
+ const SourceType._(this.value);
/// The underlying value of this enum member.
final String value;
@@ -23,24 +23,22 @@ class TagTypeEnum {
String toJson() => value;
- static const OBJECT = TagTypeEnum._(r'OBJECT');
- static const FACE = TagTypeEnum._(r'FACE');
- static const CUSTOM = TagTypeEnum._(r'CUSTOM');
+ static const machineLearning = SourceType._(r'machine-learning');
+ static const exif = SourceType._(r'exif');
- /// List of all possible values in this [enum][TagTypeEnum].
- static const values = [
- OBJECT,
- FACE,
- CUSTOM,
+ /// List of all possible values in this [enum][SourceType].
+ static const values = [
+ machineLearning,
+ exif,
];
- static TagTypeEnum? fromJson(dynamic value) => TagTypeEnumTypeTransformer().decode(value);
+ static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value);
- 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 = TagTypeEnum.fromJson(row);
+ final value = SourceType.fromJson(row);
if (value != null) {
result.add(value);
}
@@ -50,16 +48,16 @@ class TagTypeEnum {
}
}
-/// Transformation class that can [encode] an instance of [TagTypeEnum] to String,
-/// and [decode] dynamic data back to [TagTypeEnum].
-class TagTypeEnumTypeTransformer {
- factory TagTypeEnumTypeTransformer() => _instance ??= const TagTypeEnumTypeTransformer._();
+/// Transformation class that can [encode] an instance of [SourceType] to String,
+/// and [decode] dynamic data back to [SourceType].
+class SourceTypeTypeTransformer {
+ factory SourceTypeTypeTransformer() => _instance ??= const SourceTypeTypeTransformer._();
- const TagTypeEnumTypeTransformer._();
+ const SourceTypeTypeTransformer._();
- String encode(TagTypeEnum data) => data.value;
+ String encode(SourceType data) => data.value;
- /// Decodes a [dynamic value][data] to a TagTypeEnum.
+ /// Decodes a [dynamic value][data] to a SourceType.
///
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
@@ -67,12 +65,11 @@ class TagTypeEnumTypeTransformer {
///
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
/// and users are still using an old app with the old code.
- TagTypeEnum? decode(dynamic data, {bool allowNull = true}) {
+ SourceType? decode(dynamic data, {bool allowNull = true}) {
if (data != null) {
switch (data) {
- case r'OBJECT': return TagTypeEnum.OBJECT;
- case r'FACE': return TagTypeEnum.FACE;
- case r'CUSTOM': return TagTypeEnum.CUSTOM;
+ case r'machine-learning': return SourceType.machineLearning;
+ case r'exif': return SourceType.exif;
default:
if (!allowNull) {
throw ArgumentError('Unknown enum value to decode: $data');
@@ -82,7 +79,7 @@ class TagTypeEnumTypeTransformer {
return null;
}
- /// Singleton [TagTypeEnumTypeTransformer] instance.
- static TagTypeEnumTypeTransformer? _instance;
+ /// Singleton [SourceTypeTypeTransformer] instance.
+ static SourceTypeTypeTransformer? _instance;
}
diff --git a/mobile/openapi/lib/model/system_config_dto.dart b/mobile/openapi/lib/model/system_config_dto.dart
index e56169742a7e0..aff8062c8a139 100644
--- a/mobile/openapi/lib/model/system_config_dto.dart
+++ b/mobile/openapi/lib/model/system_config_dto.dart
@@ -20,6 +20,7 @@ class SystemConfigDto {
required this.logging,
required this.machineLearning,
required this.map,
+ required this.metadata,
required this.newVersionCheck,
required this.notifications,
required this.oauth,
@@ -46,6 +47,8 @@ class SystemConfigDto {
SystemConfigMapDto map;
+ SystemConfigMetadataDto metadata;
+
SystemConfigNewVersionCheckDto newVersionCheck;
SystemConfigNotificationsDto notifications;
@@ -75,6 +78,7 @@ class SystemConfigDto {
other.logging == logging &&
other.machineLearning == machineLearning &&
other.map == map &&
+ other.metadata == metadata &&
other.newVersionCheck == newVersionCheck &&
other.notifications == notifications &&
other.oauth == oauth &&
@@ -96,6 +100,7 @@ class SystemConfigDto {
(logging.hashCode) +
(machineLearning.hashCode) +
(map.hashCode) +
+ (metadata.hashCode) +
(newVersionCheck.hashCode) +
(notifications.hashCode) +
(oauth.hashCode) +
@@ -108,7 +113,7 @@ class SystemConfigDto {
(user.hashCode);
@override
- String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]';
+ String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]';
Map toJson() {
final json = {};
@@ -119,6 +124,7 @@ class SystemConfigDto {
json[r'logging'] = this.logging;
json[r'machineLearning'] = this.machineLearning;
json[r'map'] = this.map;
+ json[r'metadata'] = this.metadata;
json[r'newVersionCheck'] = this.newVersionCheck;
json[r'notifications'] = this.notifications;
json[r'oauth'] = this.oauth;
@@ -147,6 +153,7 @@ class SystemConfigDto {
logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!,
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
map: SystemConfigMapDto.fromJson(json[r'map'])!,
+ metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!,
newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!,
notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!,
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
@@ -211,6 +218,7 @@ class SystemConfigDto {
'logging',
'machineLearning',
'map',
+ 'metadata',
'newVersionCheck',
'notifications',
'oauth',
diff --git a/mobile/openapi/lib/model/system_config_faces_dto.dart b/mobile/openapi/lib/model/system_config_faces_dto.dart
new file mode 100644
index 0000000000000..980e494fb70b0
--- /dev/null
+++ b/mobile/openapi/lib/model/system_config_faces_dto.dart
@@ -0,0 +1,98 @@
+//
+// 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 SystemConfigFacesDto {
+ /// Returns a new [SystemConfigFacesDto] instance.
+ SystemConfigFacesDto({
+ required this.import_,
+ });
+
+ bool import_;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is SystemConfigFacesDto &&
+ other.import_ == import_;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (import_.hashCode);
+
+ @override
+ String toString() => 'SystemConfigFacesDto[import_=$import_]';
+
+ Map toJson() {
+ final json = {};
+ json[r'import'] = this.import_;
+ return json;
+ }
+
+ /// Returns a new [SystemConfigFacesDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static SystemConfigFacesDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return SystemConfigFacesDto(
+ import_: mapValueOfType(json, r'import')!,
+ );
+ }
+ 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 = SystemConfigFacesDto.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 = SystemConfigFacesDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of SystemConfigFacesDto-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] = SystemConfigFacesDto.listFromJson(entry.value, growable: growable,);
+ }
+ }
+ return map;
+ }
+
+ /// The list of required keys that must be present in a JSON.
+ static const requiredKeys = {
+ 'import',
+ };
+}
+
diff --git a/mobile/openapi/lib/model/system_config_metadata_dto.dart b/mobile/openapi/lib/model/system_config_metadata_dto.dart
new file mode 100644
index 0000000000000..60ca35c835bdb
--- /dev/null
+++ b/mobile/openapi/lib/model/system_config_metadata_dto.dart
@@ -0,0 +1,98 @@
+//
+// 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 SystemConfigMetadataDto {
+ /// Returns a new [SystemConfigMetadataDto] instance.
+ SystemConfigMetadataDto({
+ required this.faces,
+ });
+
+ SystemConfigFacesDto faces;
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || other is SystemConfigMetadataDto &&
+ other.faces == faces;
+
+ @override
+ int get hashCode =>
+ // ignore: unnecessary_parenthesis
+ (faces.hashCode);
+
+ @override
+ String toString() => 'SystemConfigMetadataDto[faces=$faces]';
+
+ Map toJson() {
+ final json = {};
+ json[r'faces'] = this.faces;
+ return json;
+ }
+
+ /// Returns a new [SystemConfigMetadataDto] instance and imports its values from
+ /// [value] if it's a [Map], null otherwise.
+ // ignore: prefer_constructors_over_static_methods
+ static SystemConfigMetadataDto? fromJson(dynamic value) {
+ if (value is Map) {
+ final json = value.cast();
+
+ return SystemConfigMetadataDto(
+ faces: SystemConfigFacesDto.fromJson(json[r'faces'])!,
+ );
+ }
+ 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 = SystemConfigMetadataDto.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 = SystemConfigMetadataDto.fromJson(entry.value);
+ if (value != null) {
+ map[entry.key] = value;
+ }
+ }
+ }
+ return map;
+ }
+
+ // maps a json object with a list of SystemConfigMetadataDto-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