diff --git a/.github/workflows/build-mobile.yml b/.github/workflows/build-mobile.yml
index 72e8b10aeb..a5d1fd358e 100644
--- a/.github/workflows/build-mobile.yml
+++ b/.github/workflows/build-mobile.yml
@@ -51,16 +51,17 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
- github-token: ${{ steps.token.outputs.token }}
+ github-token: ${{ steps.token.outputs.token || github.token }}
filters: |
mobile:
- 'mobile/**'
@@ -73,24 +74,26 @@ jobs:
needs: pre-job
permissions:
contents: read
- # Skip when PR from a fork
- if: ${{ !github.event.pull_request.head.repo.fork && github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
+ pull-requests: write
+ if: ${{ github.actor != 'dependabot[bot]' && fromJSON(needs.pre-job.outputs.should_run).mobile == true }}
runs-on: mich
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ if: ${{ !github.event.pull_request.head.repo.fork }}
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ inputs.ref || github.sha }}
persist-credentials: false
- token: ${{ steps.token.outputs.token }}
+ token: ${{ steps.token.outputs.token || github.token }}
- name: Create the Keystore
+ if: ${{ !github.event.pull_request.head.repo.fork }}
env:
KEY_JKS: ${{ secrets.KEY_JKS }}
working-directory: ./mobile
@@ -144,20 +147,40 @@ jobs:
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
IS_MAIN: ${{ github.ref == 'refs/heads/main' }}
+ PR_NUMBER: ${{ github.event.pull_request.number }}
run: |
if [[ $IS_MAIN == 'true' ]]; then
flutter build apk --release
flutter build apk --release --split-per-abi --target-platform android-arm,android-arm64,android-x64
else
- flutter build apk --debug --split-per-abi --target-platform android-arm64
+ flutter build apk --release
fi
- name: Publish Android Artifact
+ id: upload-apk
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-apk-signed
path: mobile/build/app/outputs/flutter-apk/*.apk
+ - name: Comment APK download link on PR
+ if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
+ uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
+ env:
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ APK_URL: ${{ steps.upload-apk.outputs.artifact-url }}
+ with:
+ github-token: ${{ steps.token.outputs.token }}
+ message-id: 'mobile-android-apk'
+ message: |
+ 📱 **Android release APK (universal)** — `${{ env.HEAD_SHA }}`
+
+ Download: ${{ env.APK_URL }}
+
+
+
+ GitHub login required. Downloads as a zip containing a single `app-release.apk` — extract and install. Installs as a separate app (applicationId `app.alextran.immich.pr${{ github.event.pull_request.number }}`), so it coexists with the Play Store version and any other PR builds.
+
- name: Save Gradle Cache
id: cache-gradle-save
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
@@ -210,7 +233,7 @@ jobs:
working-directory: ./mobile
- name: Setup Ruby
- uses: ruby/setup-ruby@7372622e62b60b3cb750dcd2b9e32c247ffec26a # v1.302.0
+ uses: ruby/setup-ruby@c4e5b1316158f92e3d49443a9d58b31d25ac0f8f # v1.306.0
with:
ruby-version: '3.3'
bundler-cache: true
diff --git a/.github/workflows/cache-cleanup.yml b/.github/workflows/cache-cleanup.yml
index e093cf9bf0..4ecd758f6d 100644
--- a/.github/workflows/cache-cleanup.yml
+++ b/.github/workflows/cache-cleanup.yml
@@ -19,9 +19,9 @@ jobs:
actions: write
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check out code
diff --git a/.github/workflows/check-openapi.yml b/.github/workflows/check-openapi.yml
index f2d03f7400..47ac1d3730 100644
--- a/.github/workflows/check-openapi.yml
+++ b/.github/workflows/check-openapi.yml
@@ -24,7 +24,7 @@ jobs:
persist-credentials: false
- name: Check for breaking API changes
- uses: oasdiff/oasdiff-action/breaking@f8cb9308b42121e793f835bd14c0b8090420430c # v0.0.39
+ uses: oasdiff/oasdiff-action/breaking@37bf9ff785c7315df88216660826e71be4cc03da # v0.0.44
with:
base: https://raw.githubusercontent.com/${{ github.repository }}/main/open-api/immich-openapi-specs.json
revision: open-api/immich-openapi-specs.json
diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml
index 2a334af89d..a1d725296b 100644
--- a/.github/workflows/cli.yml
+++ b/.github/workflows/cli.yml
@@ -31,35 +31,25 @@ jobs:
working-directory: ./cli
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './cli/.nvmrc'
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- - name: Setup typescript-sdk
- run: pnpm install && pnpm run build
- working-directory: ./open-api/typescript-sdk
-
- - run: pnpm install --frozen-lockfile
- - run: pnpm build
- - run: pnpm publish --provenance --no-git-checks
+ - name: Publish
if: ${{ github.event_name == 'release' }}
+ run: mise run ci-publish
docker:
name: Docker
@@ -71,9 +61,9 @@ jobs:
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
diff --git a/.github/workflows/close-duplicates.yml b/.github/workflows/close-duplicates.yml
index 839e5b3ceb..55fd5ea0ca 100644
--- a/.github/workflows/close-duplicates.yml
+++ b/.github/workflows/close-duplicates.yml
@@ -35,7 +35,7 @@ jobs:
needs: [get_body, should_run]
if: ${{ needs.should_run.outputs.should_run == 'true' }}
container:
- image: ghcr.io/immich-app/mdq:main@sha256:557cca601891b8b7d78b940071d35aaf7aaeb9b327d19b22cf282118edbc5272
+ image: ghcr.io/immich-app/mdq:main@sha256:32abe582452b12dff55055e1d6bc24508a8f17164f9d1831db7bb70953c014c6
outputs:
checked: ${{ steps.get_checkbox.outputs.checked }}
steps:
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index f2d99ae1e8..827864cec5 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -44,9 +44,9 @@ jobs:
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout repository
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 84509103be..8e16894b49 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -23,14 +23,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -132,7 +132,7 @@ jobs:
suffixes: '-rocm'
platforms: linux/amd64
runner-mapping: '{"linux/amd64": "pokedex-large"}'
- uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
+ uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions:
contents: read
actions: read
@@ -155,7 +155,7 @@ jobs:
name: Build and Push Server
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).server == true }}
- uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@61a0fc2b41524edcc7c9fffb8bb178e6b0ccf21d # multi-runner-build-workflow-v2.3.0
+ uses: immich-app/devtools/.github/workflows/multi-runner-build.yml@5813c7c4f7016c748ae7ac5d5f684846649d4d20 # multi-runner-build-workflow-v2.4.0
permissions:
contents: read
actions: read
@@ -178,7 +178,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
+ - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
@@ -189,6 +189,6 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
+ - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
diff --git a/.github/workflows/docs-build.yml b/.github/workflows/docs-build.yml
index 0ccebfb363..c6afd7277d 100644
--- a/.github/workflows/docs-build.yml
+++ b/.github/workflows/docs-build.yml
@@ -21,14 +21,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -54,9 +54,9 @@ jobs:
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -64,17 +64,11 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- fetch-depth: 0
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './docs/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- name: Run install
run: pnpm install
diff --git a/.github/workflows/docs-deploy.yml b/.github/workflows/docs-deploy.yml
index 57ea2d41d4..5204b61d0e 100644
--- a/.github/workflows/docs-deploy.yml
+++ b/.github/workflows/docs-deploy.yml
@@ -20,9 +20,9 @@ jobs:
artifact: ${{ steps.get-artifact.outputs.result }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- if: ${{ github.event.workflow_run.conclusion != 'success' }}
@@ -119,9 +119,9 @@ jobs:
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -131,7 +131,9 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ with:
+ github_token: ${{ steps.token.outputs.token }}
- name: Load parameters
id: parameters
diff --git a/.github/workflows/docs-destroy.yml b/.github/workflows/docs-destroy.yml
index 0e9c37a66c..db3d34f22f 100644
--- a/.github/workflows/docs-destroy.yml
+++ b/.github/workflows/docs-destroy.yml
@@ -17,9 +17,9 @@ jobs:
pull-requests: write
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -29,7 +29,9 @@ jobs:
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
- uses: immich-app/devtools/actions/use-mise@035e80a7d4355d5f087ffb95db9e4a0944c04e56 # use-mise-action-v1.1.3
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
+ with:
+ github_token: ${{ steps.token.outputs.token }}
- name: Destroy Docs Subdomain
env:
diff --git a/.github/workflows/fix-format.yml b/.github/workflows/fix-format.yml
index 59cbb28fa8..6c90f99bf7 100644
--- a/.github/workflows/fix-format.yml
+++ b/.github/workflows/fix-format.yml
@@ -14,29 +14,23 @@ jobs:
contents: write
pull-requests: write
steps:
- - name: Generate a token
- id: generate-token
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
+ - id: token
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - name: 'Checkout'
+ - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event.pull_request.head.ref }}
- token: ${{ steps.generate-token.outputs.token }}
persist-credentials: true
+ token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0
-
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
- name: Fix formatting
run: pnpm --recursive install && pnpm run --recursive --if-present --parallel format:fix
diff --git a/.github/workflows/merge-translations.yml b/.github/workflows/merge-translations.yml
index 08d3192f8b..685dfc6abe 100644
--- a/.github/workflows/merge-translations.yml
+++ b/.github/workflows/merge-translations.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
workflow_call:
secrets:
- PUSH_O_MATIC_APP_ID:
+ PUSH_O_MATIC_APP_CLIENT_ID:
required: true
PUSH_O_MATIC_APP_KEY:
required: true
@@ -33,7 +33,7 @@ jobs:
if: ${{ inputs.skip != true }}
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Find translation PR
diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml
index 416e40df0d..f5c1802bbc 100644
--- a/.github/workflows/pr-label-validation.yml
+++ b/.github/workflows/pr-label-validation.yml
@@ -14,9 +14,9 @@ jobs:
pull-requests: write
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Require PR to have a changelog label
diff --git a/.github/workflows/pr-labeler.yml b/.github/workflows/pr-labeler.yml
index 75ee750e9f..5e547fb47a 100644
--- a/.github/workflows/pr-labeler.yml
+++ b/.github/workflows/pr-labeler.yml
@@ -12,9 +12,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/labeler@634933edcd8ababfe52f92936142cc22ac488b1b # v6.0.1
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
index 2aa028b22e..82b5925947 100644
--- a/.github/workflows/prepare-release.yml
+++ b/.github/workflows/prepare-release.yml
@@ -36,7 +36,7 @@ jobs:
permissions:
pull-requests: write
secrets:
- PUSH_O_MATIC_APP_ID: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ PUSH_O_MATIC_APP_CLIENT_ID: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
PUSH_O_MATIC_APP_KEY: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
WEBLATE_TOKEN: ${{ secrets.WEBLATE_TOKEN }}
@@ -48,32 +48,27 @@ jobs:
version: ${{ steps.output.outputs.version }}
permissions: {} # No job-level permissions are needed because it uses the app-token
steps:
- - name: Generate a token
- id: generate-token
- uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
+ - id: token
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - name: Checkout
+ - name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
- token: ${{ steps.generate-token.outputs.token }}
+ token: ${{ steps.token.outputs.token }}
persist-credentials: true
ref: main
- - name: Install uv
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
-
- - name: Setup pnpm
- uses: pnpm/action-setup@08c4be7e2e672a47d11bd04269e27e5f3e8529cb # v6.0.0
-
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
+ # TODO move to mise
+ - name: Install uv
+ uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
- name: Bump version
env:
@@ -126,7 +121,7 @@ jobs:
id: generate-token
uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout
diff --git a/.github/workflows/preview-label.yaml b/.github/workflows/preview-label.yaml
index 5cf0008597..f4a03c3013 100644
--- a/.github/workflows/preview-label.yaml
+++ b/.github/workflows/preview-label.yaml
@@ -14,12 +14,12 @@ jobs:
pull-requests: write
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
+ - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
@@ -32,9 +32,9 @@ jobs:
pull-requests: write
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
@@ -48,14 +48,14 @@ jobs:
name: 'preview'
})
- - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
+ - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
message-id: 'preview-status'
message: 'PRs from forks cannot have preview environments.'
- - uses: mshick/add-pr-comment@64b8e914979889d746c99dea15a76e77ef64580a # v3.10.0
+ - uses: mshick/add-pr-comment@8e4927817251f1ff60c001f04568532b38e0b4a0 # v3.11.0
if: ${{ !github.event.pull_request.head.repo.fork }}
with:
github-token: ${{ steps.token.outputs.token }}
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index d9b6ffb7f5..6eb3742e7a 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -19,29 +19,27 @@ jobs:
working-directory: ./open-api/typescript-sdk
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+ - name: Checkout code
+ uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
-
- # Setup .npmrc file to publish to npm
- - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './open-api/typescript-sdk/.nvmrc'
- registry-url: 'https://registry.npmjs.org'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install deps
run: pnpm install --frozen-lockfile
+
- name: Build
run: pnpm build
+
- name: Publish
run: pnpm publish --provenance --no-git-checks
diff --git a/.github/workflows/static_analysis.yml b/.github/workflows/static_analysis.yml
index 21e5e25bc6..70a7294424 100644
--- a/.github/workflows/static_analysis.yml
+++ b/.github/workflows/static_analysis.yml
@@ -20,14 +20,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -49,9 +49,9 @@ jobs:
working-directory: ./mobile
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4558b90866..ea4602c660 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,14 +17,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -34,13 +34,17 @@ jobs:
- 'web/**'
- 'i18n/**'
- 'open-api/typescript-sdk/**'
+ - 'pnpm-lock.yaml'
server:
- 'server/**'
+ - 'pnpm-lock.yaml'
cli:
- 'cli/**'
- 'open-api/typescript-sdk/**'
+ - 'pnpm-lock.yaml'
e2e:
- 'e2e/**'
+ - 'pnpm-lock.yaml'
mobile:
- 'mobile/**'
machine-learning:
@@ -63,9 +67,9 @@ jobs:
working-directory: ./server
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -74,28 +78,14 @@ jobs:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run package manager install
- run: pnpm install
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
- if: ${{ !cancelled() }}
- - name: Run small tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
cli-unit-tests:
name: Unit Test CLI
needs: pre-job
@@ -108,9 +98,9 @@ jobs:
working-directory: ./cli
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -118,31 +108,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './cli/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Setup typescript-sdk
- run: pnpm install && pnpm run build
- working-directory: ./open-api/typescript-sdk
- - name: Install deps
- run: pnpm install
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
- if: ${{ !cancelled() }}
- - name: Run unit tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
cli-unit-tests-win:
name: Unit Test CLI (Windows)
needs: pre-job
@@ -155,9 +129,9 @@ jobs:
working-directory: ./cli
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -165,26 +139,33 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
+
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+
- name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
- node-version-file: './cli/.nvmrc'
+ node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
+
- name: Install deps
run: pnpm install --frozen-lockfile
+
# Skip linter & formatter in Windows test.
+
- name: Run tsc
run: pnpm check
if: ${{ !cancelled() }}
+
- name: Run unit tests & coverage
run: pnpm test
if: ${{ !cancelled() }}
+
web-lint:
name: Lint Web
needs: pre-job
@@ -197,9 +178,9 @@ jobs:
working-directory: ./web
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -207,28 +188,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
+ run: mise run //:sdk:install && mise run //:sdk:build
+
- name: Run pnpm install
- run: pnpm rebuild && pnpm install --frozen-lockfile
+ run: pnpm install --frozen-lockfile
+
- name: Run linter
run: pnpm lint
if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run svelte checks
- run: pnpm check:svelte
- if: ${{ !cancelled() }}
+
web-unit-tests:
name: Test Web
needs: pre-job
@@ -241,9 +216,9 @@ jobs:
working-directory: ./web
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -251,25 +226,15 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- - name: Run npm install
- run: pnpm install --frozen-lockfile
- - name: Run tsc
- run: pnpm check:typescript
- if: ${{ !cancelled() }}
- - name: Run unit tests & coverage
- run: pnpm test
- if: ${{ !cancelled() }}
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
i18n-tests:
name: Test i18n
needs: pre-job
@@ -279,9 +244,9 @@ jobs:
contents: read
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -289,24 +254,25 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './web/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install dependencies
run: pnpm --filter=immich-i18n install --frozen-lockfile
+
- name: Format
run: pnpm --filter=immich-i18n format:fix
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
i18n/**
+
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -315,6 +281,7 @@ jobs:
echo "ERROR: i18n files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
+
e2e-tests-lint:
name: End-to-End Lint
needs: pre-job
@@ -327,9 +294,9 @@ jobs:
working-directory: ./e2e
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -337,30 +304,16 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './e2e/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
- run: pnpm install --frozen-lockfile && pnpm build
- working-directory: ./open-api/typescript-sdk
- if: ${{ !cancelled() }}
- - name: Install dependencies
- run: pnpm install --frozen-lockfile
- if: ${{ !cancelled() }}
- - name: Run linter
- run: pnpm lint
- if: ${{ !cancelled() }}
- - name: Run formatter
- run: pnpm format
- if: ${{ !cancelled() }}
- - name: Run tsc
- run: pnpm check
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
if: ${{ !cancelled() }}
+
server-medium-tests:
name: Medium Tests (Server)
needs: pre-job
@@ -373,9 +326,9 @@ jobs:
working-directory: ./server
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -384,19 +337,16 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run pnpm install
- run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
- - name: Run medium tests
- run: pnpm test:medium
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-medium
+ run: mise run ci-medium
if: ${{ !cancelled() }}
+
e2e-tests-server-cli:
name: End-to-End Tests (Server & CLI)
needs: pre-job
@@ -412,9 +362,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -423,52 +373,63 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
+
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+
- name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
- node-version-file: './e2e/.nvmrc'
+ node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
- - name: Run setup typescript-sdk
+
+ - name: Setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
- if: ${{ !cancelled() }}
+
- name: Run setup web
run: pnpm install --frozen-lockfile && pnpm exec svelte-kit sync
working-directory: ./web
if: ${{ !cancelled() }}
+
- name: Run setup cli
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./cli
if: ${{ !cancelled() }}
+
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
+
- name: Start Docker Compose
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
+
- name: Run e2e tests (api & cli)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test
if: ${{ !cancelled() }}
+
- name: Run e2e tests (maintenance)
env:
VITEST_DISABLE_DOCKER_SETUP: true
run: pnpm test:maintenance
if: ${{ !cancelled() }}
+
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
+
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-server-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
+
e2e-tests-web:
name: End-to-End Tests (Web)
needs: pre-job
@@ -484,9 +445,9 @@ jobs:
runner: [ubuntu-latest, ubuntu-24.04-arm]
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -495,70 +456,85 @@ jobs:
persist-credentials: false
submodules: 'recursive'
token: ${{ steps.token.outputs.token }}
+
- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
+
- name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
- node-version-file: './e2e/.nvmrc'
+ node-version-file: '.nvmrc'
cache: 'pnpm'
cache-dependency-path: '**/pnpm-lock.yaml'
+
- name: Run setup typescript-sdk
run: pnpm install --frozen-lockfile && pnpm build
working-directory: ./open-api/typescript-sdk
+
if: ${{ !cancelled() }}
- name: Install dependencies
run: pnpm install --frozen-lockfile
if: ${{ !cancelled() }}
+
- name: Install Playwright Browsers
run: pnpm exec playwright install chromium --only-shell
if: ${{ !cancelled() }}
+
- name: Docker build
run: docker compose up -d --build --renew-anon-volumes --force-recreate --remove-orphans --wait --wait-timeout 300
if: ${{ !cancelled() }}
+
- name: Run e2e tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web
if: ${{ !cancelled() }}
+
- name: Archive e2e test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-web-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Run ui tests (web)
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:ui
if: ${{ !cancelled() }}
+
- name: Archive ui test (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-ui-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Run maintenance tests
env:
PLAYWRIGHT_DISABLE_WEBSERVER: true
run: pnpm test:web:maintenance
if: ${{ !cancelled() }}
+
- name: Archive maintenance tests (web) results
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: success() || failure()
with:
name: e2e-maintenance-isolated-test-results-${{ matrix.runner }}
path: e2e/playwright-report/
+
- name: Capture Docker logs
if: always()
run: docker compose logs --no-color > docker-compose-logs.txt
working-directory: ./e2e
+
- name: Archive Docker logs
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: always()
with:
name: e2e-web-docker-logs-${{ matrix.runner }}
path: e2e/docker-compose-logs.txt
+
success-check-e2e:
name: End-to-End Tests Success
needs: [e2e-tests-server-cli, e2e-tests-web]
@@ -566,7 +542,7 @@ jobs:
runs-on: ubuntu-latest
if: always()
steps:
- - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
+ - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
mobile-unit-tests:
@@ -578,9 +554,9 @@ jobs:
contents: read
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -610,34 +586,24 @@ jobs:
working-directory: ./machine-learning
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Install uv
- uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- python-version: 3.11
- - name: Install dependencies
- run: |
- uv sync --extra cpu
- - name: Lint with ruff
- run: |
- uv run ruff check --output-format=github immich_ml
- - name: Format with ruff
- run: |
- uv run ruff format --check immich_ml
- - name: Run mypy type checking
- run: |
- uv run mypy --strict immich_ml/
- - name: Run tests and coverage
- run: |
- uv run pytest --cov=immich_ml --cov-report term-missing
+ github_token: ${{ steps.token.outputs.token }}
+
+ - name: Run ci-unit
+ run: mise run ci-unit
+
github-files-formatting:
name: .github Files Formatting
needs: pre-job
@@ -650,9 +616,9 @@ jobs:
working-directory: ./.github
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -660,19 +626,19 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './.github/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Run pnpm install
run: pnpm install --frozen-lockfile
+
- name: Run formatter
run: pnpm format
if: ${{ !cancelled() }}
+
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
@@ -680,9 +646,9 @@ jobs:
contents: read
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
@@ -701,9 +667,9 @@ jobs:
contents: read
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -711,21 +677,22 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm --filter immich install --frozen-lockfile
+
- name: Build the app
run: pnpm --filter immich build
+
- name: Run API generation
run: ./bin/generate-open-api.sh
working-directory: open-api
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
@@ -734,6 +701,7 @@ jobs:
mobile/openapi
open-api/typescript-sdk
open-api/immich-openapi-specs.json
+
- name: Verify files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -742,6 +710,7 @@ jobs:
echo "ERROR: Generated files not up to date!"
echo "Changed files: ${CHANGED_FILES}"
exit 1
+
sql-schema-up-to-date:
name: SQL Schema Checks
runs-on: ubuntu-latest
@@ -763,9 +732,9 @@ jobs:
working-directory: ./server
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
@@ -773,31 +742,35 @@ jobs:
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- - name: Setup pnpm
- uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0
- - name: Setup Node
- uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
+
+ - name: Setup Mise
+ uses: immich-app/devtools/actions/use-mise@01a4d354b70f99a6baf4a1b72827f6d4922e4978 # use-mise-action-v2.0.0
with:
- node-version-file: './server/.nvmrc'
- cache: 'pnpm'
- cache-dependency-path: '**/pnpm-lock.yaml'
+ github_token: ${{ steps.token.outputs.token }}
+
- name: Install server dependencies
run: SHARP_IGNORE_GLOBAL_LIBVIPS=true pnpm install --frozen-lockfile
+
- name: Build the app
run: pnpm build
+
- name: Run existing migrations
run: pnpm migrations:run
+
- name: Test npm run schema:reset command works
run: pnpm schema:reset
+
- name: Generate new migrations
continue-on-error: true
run: pnpm migrations:generate src/TestMigration
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-files
with:
files: |
server/src
+
- name: Verify migration files have not changed
if: steps.verify-changed-files.outputs.files_changed == 'true'
env:
@@ -807,16 +780,19 @@ jobs:
echo "Changed files: ${CHANGED_FILES}"
cat ./src/*-TestMigration.ts
exit 1
+
- name: Run SQL generation
run: pnpm sync:sql
env:
DB_URL: postgres://postgres:postgres@localhost:5432/immich
+
- name: Find file changes
uses: tj-actions/verify-changed-files@a1c6acee9df209257a246f2cc6ae8cb6581c1edf # v20.0.4
id: verify-changed-sql-files
with:
files: |
server/src/queries
+
- name: Verify SQL files have not changed
if: steps.verify-changed-sql-files.outputs.files_changed == 'true'
env:
diff --git a/.github/workflows/weblate-lock.yml b/.github/workflows/weblate-lock.yml
index 09024063c0..b05a20360c 100644
--- a/.github/workflows/weblate-lock.yml
+++ b/.github/workflows/weblate-lock.yml
@@ -24,14 +24,14 @@ jobs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Check what should run
id: check
- uses: immich-app/devtools/actions/pre-job@f50e3b600b6ac1763ddb8f3dfc69093512b967a1 # pre-job-action-v2.0.3
+ uses: immich-app/devtools/actions/pre-job@91f342bb4477c4bc10c576ae739da875d85aa164 # pre-job-action-v2.0.4
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
@@ -47,9 +47,9 @@ jobs:
if: ${{ fromJSON(needs.pre-job.outputs.should_run).i18n == true }}
steps:
- id: token
- uses: immich-app/devtools/actions/create-workflow-token@57ff6ebfd507b045514442683ff06ff1b2f6efbd # create-workflow-token-action-v1.0.2
+ uses: immich-app/devtools/actions/create-workflow-token@caa599d954228439ea3e8ce1c3328f41ab120ee6 # create-workflow-token-action-v2.0.0
with:
- app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }}
+ client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Bot review status
@@ -68,6 +68,6 @@ jobs:
permissions: {}
if: always()
steps:
- - uses: immich-app/devtools/actions/success-check@53bb77345ee9f953f93bd6fd9980f07a2f24965e # success-check-action-v0.0.5
+ - uses: immich-app/devtools/actions/success-check@81113db03f6d743efee81e0058c0b43f6cd6f36d # success-check-action-v0.0.6
with:
needs: ${{ toJSON(needs) }}
diff --git a/.github/.nvmrc b/.nvmrc
similarity index 100%
rename from .github/.nvmrc
rename to .nvmrc
diff --git a/.vscode/settings.json b/.vscode/settings.json
index eeb80649ba..e20930cacf 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -26,7 +26,11 @@
},
"[svelte]": {
"editor.defaultFormatter": "svelte.svelte-vscode",
- "editor.formatOnSave": true
+ "editor.formatOnSave": true,
+ "tailwindCSS.lint.suggestCanonicalClasses": "ignore"
+ },
+ "svelte.plugin.svelte.compilerWarnings": {
+ "state_referenced_locally": "ignore"
},
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode",
diff --git a/cli/.nvmrc b/cli/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/cli/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/cli/mise.toml b/cli/mise.toml
index 740184e03d..28d5e1858f 100644
--- a/cli/mise.toml
+++ b/cli/mise.toml
@@ -7,7 +7,7 @@ run = "vite build"
[tasks.test]
env._.path = "./node_modules/.bin"
-run = "vite"
+run = "vitest"
[tasks.lint]
env._.path = "./node_modules/.bin"
@@ -27,3 +27,26 @@ run = "prettier --write ."
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
+
+[tasks.ci-publish]
+depends = ["//:sdk:install", "//:sdk:build"]
+run = [
+ { task = ":install" },
+ { task = ":build" },
+ "pnpm publish --provenance --no-git-checks",
+]
+
+[tasks.ci-unit]
+depends = ["//:sdk:install", "//:sdk:build"]
+run = [
+ { task = ":install" },
+ { task = ":format" },
+ { task = ":lint" },
+ { task = ":check" },
+ { task = ":test --run" },
+]
+
+[tasks.checklist]
+run = [
+ { task = ":ci-unit" },
+]
diff --git a/cli/package.json b/cli/package.json
index 7b42f77ff1..8035639a2a 100644
--- a/cli/package.json
+++ b/cli/package.json
@@ -66,8 +66,5 @@
"fastq": "^1.17.1",
"lodash-es": "^4.17.21",
"micromatch": "^4.0.8"
- },
- "volta": {
- "node": "24.15.0"
}
}
diff --git a/deployment/mise.toml b/deployment/mise.toml
index 00c6826343..fadc28dc2e 100644
--- a/deployment/mise.toml
+++ b/deployment/mise.toml
@@ -1,5 +1,5 @@
[tools]
-terragrunt = "1.0.1"
+terragrunt = "1.0.3"
opentofu = "1.11.6"
[tasks."tg:fmt"]
diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml
index 979d7fc0ee..d85186b6f1 100644
--- a/docker/docker-compose.prod.yml
+++ b/docker/docker-compose.prod.yml
@@ -85,7 +85,7 @@ services:
container_name: immich_prometheus
ports:
- 9090:9090
- image: prom/prometheus@sha256:5550dc63da361dc30f6fe02ac0e4dfc736ededfef3c8d12a634db04a67824d78
+ image: prom/prometheus@sha256:e4254400b85610324913f0dc4acf92603d9984e7519414c5a12811aa6146acc3
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus-data:/prometheus
@@ -97,7 +97,7 @@ services:
command: ['./run.sh', '-disable-reporting']
ports:
- 3000:3000
- image: grafana/grafana:12.4.2-ubuntu@sha256:78839fe49e1425c02416fa8072591533a72bd9598e563b54a07d78f9e27fb5d3
+ image: grafana/grafana:12.4.3-ubuntu@sha256:ca3f764fdc48cebdf22dd206f33ecb0795a9a7210eacd1b5c02204aebd78b223
volumes:
- grafana-data:/var/lib/grafana
diff --git a/docs/.nvmrc b/docs/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/docs/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/docs/docs/administration/postgres-standalone.md b/docs/docs/administration/postgres-standalone.md
index 84681fdfa6..aa19e28cc1 100644
--- a/docs/docs/administration/postgres-standalone.md
+++ b/docs/docs/administration/postgres-standalone.md
@@ -81,7 +81,7 @@ VectorChord is the successor extension to pgvecto.rs, allowing for higher perfor
### Migrating from pgvecto.rs
-Support for pgvecto.rs will be dropped in a later release, hence we recommend all users currently using pgvecto.rs to migrate to VectorChord at their convenience. There are two primary approaches to do so.
+Support for pgvecto.rs has been dropped as of 3.0, hence all users currently using pgvecto.rs should migrate to VectorChord. There are two primary approaches to do so.
The easiest option is to have both extensions installed during the migration:
diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md
index 6e8246b06c..62831ab089 100644
--- a/docs/docs/features/libraries.md
+++ b/docs/docs/features/libraries.md
@@ -50,6 +50,8 @@ Some basic examples:
- `**/Raw/**` will exclude all files in any directory named `Raw`
- `**/*.{tif,jpg}` will exclude all files with the extension `.tif` or `.jpg`
+Note that `*` is a wildcard matching zero or more characters (i.e., withinin a filename or single directory name). `**` matches zero or more subdirectories, recursively. It also includes any/all files within a subdirectory, i.e., when used at the end of a pattern. For example, `**/exclude_me/**` will exclude all files in any directory named `exclude_me`, as well as all files in any subdirectories of `exclude_me`, recursively.
+
Special characters such as @ should be escaped, for instance:
- `**/\@eaDir/**` will exclude all files in any directory named `@eaDir`
diff --git a/docs/docs/features/ml-hardware-acceleration.md b/docs/docs/features/ml-hardware-acceleration.md
index bd4fe49e96..5ad0bcd11f 100644
--- a/docs/docs/features/ml-hardware-acceleration.md
+++ b/docs/docs/features/ml-hardware-acceleration.md
@@ -47,6 +47,7 @@ You do not need to redo any machine learning jobs after enabling hardware accele
#### ROCm
+- On Linux, The [AMDGPU driver module](https://rocm.docs.amd.com/projects/install-on-linux/en/latest/how-to/docker.html) needs to be installed on the server and, if secure boot is used, the signing key of DKMS [needs to be enrolled in UEFI BIOS](https://wiki.debian.org/SecureBoot)
- The GPU must be supported by ROCm. If it isn't officially supported, you can attempt to use the `HSA_OVERRIDE_GFX_VERSION` environmental variable: `HSA_OVERRIDE_GFX_VERSION=`. If this doesn't work, you might need to also set `HSA_USE_SVM=0`.
- The ROCm image is quite large and requires at least 35GiB of free disk space. However, pulling later updates to the service through Docker will generally only amount to a few hundred megabytes as the rest will be cached.
- This backend is new and may experience some issues. For example, GPU power consumption can be higher than usual after running inference, even if the machine learning service is idle. In this case, it will only go back to normal after being idle for 5 minutes (configurable with the [MACHINE_LEARNING_MODEL_TTL](/install/environment-variables) setting).
diff --git a/docs/docs/features/searching.md b/docs/docs/features/searching.md
index 92eb01c39d..1bdfeca8ba 100644
--- a/docs/docs/features/searching.md
+++ b/docs/docs/features/searching.md
@@ -18,6 +18,7 @@ You can search the following types of content:
| People | Faces that are recognized in your photos/videos. |
| Contextual | Content of the photos and videos. |
| File name or extension | Full or partial file's name, or file's extension |
+| Full path or folder | Full or partial folder names from the original path. |
| Description | Description added to assets. |
| Optical Character Recognition (OCR) | Text in images |
| Locations | Cities, states, and countries from reverse geocoding. |
@@ -30,6 +31,12 @@ You can search the following types of content:
+### Full path or folder
+
+Use this mode when you know a folder name or part of the original asset path.
+
+Example: for /John/Projects/3D_Printing/2026-07-01/IMG_0001.jpg, searches like Projects, 3D, Printing, or 2026 match the asset.
+
## Configuration
Navigating to `Administration > Settings > Machine Learning Settings > Smart Search` will show the options available.
diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md
index 518b003c3a..09e34c5107 100644
--- a/docs/docs/guides/remote-access.md
+++ b/docs/docs/guides/remote-access.md
@@ -39,7 +39,7 @@ You can learn how to set up Tailscale together with Immich with the [tutorial vi
### Cons
- The Tailscale client usually needs to run as root on your devices and it increases the attack surface slightly compared to a minimal Wireguard server. e.g., an [RCE vulnerability](https://github.com/tailscale/tailscale/security/advisories/GHSA-vqp6-rc3h-83cp) was discovered in the Windows Tailscale client in November 2022.
-- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) that permits up to 3 users and up to 100 devices.
+- Tailscale is a paid service. However, there is a generous [free tier](https://tailscale.com/pricing/) suitable for personal use.
- Tailscale needs to be installed and running on both server-side and client-side.
## Option 3: Reverse Proxy
diff --git a/docs/docs/install/config-file.md b/docs/docs/install/config-file.md
index 4754497d90..b0491a21e3 100644
--- a/docs/docs/install/config-file.md
+++ b/docs/docs/install/config-file.md
@@ -264,4 +264,4 @@ volumes:
- ./configuration.yml:${IMMICH_CONFIG_FILE}
```
-::
+:::
diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md
index b29c233153..ca22c5ad34 100644
--- a/docs/docs/install/environment-variables.md
+++ b/docs/docs/install/environment-variables.md
@@ -29,29 +29,31 @@ These environment variables are used by the `docker-compose.yml` file and do **N
## General
-| Variable | Description | Default | Containers | Workers |
-| :---------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
-| `TZ` | Timezone | \*1 | server | microservices |
-| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
-| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
-| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
-| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/data` | server | api, microservices |
-| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
-| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`. | `false` | server | api |
-| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
-| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
-| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
-| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
-| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
-| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
-| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
-| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
+| Variable | Description | Default | Containers | Workers |
+| :---------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------: | :----------------------- | :----------------- |
+| `TZ` | Timezone | \*1 | server | microservices |
+| `IMMICH_ENV` | Environment (production, development) | `production` | server, machine learning | api, microservices |
+| `IMMICH_LOG_LEVEL` | Log level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices |
+| `IMMICH_LOG_FORMAT` | Log output format (`console`, `json`) | `console` | server | api, microservices |
+| `IMMICH_MEDIA_LOCATION` | Media location inside the container ⚠️**You probably shouldn't set this**\*2⚠️ | `/data` | server | api, microservices |
+| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
+| `IMMICH_HELMET_FILE` | Path to a json file with [helmet](https://www.npmjs.com/package/helmet) options. Set to `false` to disable. Set to `true` to use `server/helmet.json`\*3. | `false` | server | api |
+| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
+| `CPU_CORES` | Number of cores available to the Immich server | auto-detected CPU core count | server | |
+| `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api |
+| `IMMICH_MICROSERVICES_METRICS_PORT` | Port for the OTEL metrics | `8082` | server | microservices |
+| `IMMICH_PROCESS_INVALID_IMAGES` | When `true`, generate thumbnails for invalid images | | server | microservices |
+| `IMMICH_TRUSTED_PROXIES` | List of comma-separated IPs set as trusted proxies | | server | api |
+| `IMMICH_IGNORE_MOUNT_CHECK_ERRORS` | See [System Integrity](/administration/system-integrity) | | server | api, microservices |
+| `IMMICH_ALLOW_SETUP` | When `false` disables the `/auth/admin-sign-up` endpoint | `true` | server | api |
\*1: `TZ` should be set to a `TZ identifier` from [this list][tz-list]. For example, `TZ="Etc/UTC"`.
`TZ` is used by `exiftool` as a fallback in case the timezone cannot be determined from the image metadata. It is also used for logfile timestamps and cron job execution.
\*2: This path is where the Immich code looks for the files, which is internal to the docker container. Setting it to a path on your host will certainly break things, you should use the `UPLOAD_LOCATION` variable instead.
+\*3: The [default configuration](https://helmetjs.github.io/#content-security-policy) sets `upgrade-insecure-requests`, which tells the browser to upgrade all requests to HTTPS. This breaks on HTTP-only deployments. If you cannot use HTTPS, you should use a custom helmet config file with `"upgrade-insecure-requests": null`.
+
## Workers
| Variable | Description | Default | Containers |
@@ -81,7 +83,7 @@ Information on the current workers can be found [here](/administration/jobs-work
| `DB_PASSWORD` | Database password | `postgres` | server, database\*1 |
| `DB_DATABASE_NAME` | Database name | `immich` | server, database\*1 |
| `DB_SSL_MODE` | Database SSL mode | | server |
-| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`vectorchord`, `pgvector`, `pgvecto.rs`]) | | server |
+| `DB_VECTOR_EXTENSION`\*2 | Database vector extension (one of [`vectorchord`, `pgvector`]) | | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| `DB_STORAGE_TYPE` | Optimize concurrent IO on SSDs or sequential IO on HDDs ([`SSD`, `HDD`])\*3 | `SSD` | database |
diff --git a/docs/docs/install/upgrading.md b/docs/docs/install/upgrading.md
index 12e5c9c342..38fc056f80 100644
--- a/docs/docs/install/upgrading.md
+++ b/docs/docs/install/upgrading.md
@@ -130,7 +130,3 @@ These storage mediums have different performance characteristics. As a result, t
#### Can I use the new database image as a general PostgreSQL image outside of Immich?
It’s a standard PostgreSQL container image that additionally contains the VectorChord, pgvector, and (optionally) pgvecto.rs extensions. If you were using the previous pgvecto.rs image for other purposes, you can similarly do so with this image.
-
-#### If pgvecto.rs and pgvector still work, why should I switch to VectorChord?
-
-VectorChord is faster, more stable, uses less RAM, and (with the settings Immich uses) offers higher-quality results than pgvector and pgvecto.rs. This translates to better search and facial recognition experiences. In addition, pgvecto.rs support will be dropped in the future, so changing it sooner will avoid disruption.
diff --git a/docs/package.json b/docs/package.json
index d469d1ffef..e1d26532db 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -56,8 +56,5 @@
},
"engines": {
"node": ">=20"
- },
- "volta": {
- "node": "24.15.0"
}
}
diff --git a/e2e-auth-server/package.json b/e2e-auth-server/package.json
index f8ea7243fd..8aa24c04ec 100644
--- a/e2e-auth-server/package.json
+++ b/e2e-auth-server/package.json
@@ -11,5 +11,6 @@
"@types/oidc-provider": "^9.0.0",
"oidc-provider": "^9.0.0",
"tsx": "^4.20.6"
- }
+ },
+ "packageManager": "pnpm@10.33.1"
}
diff --git a/e2e/.nvmrc b/e2e/.nvmrc
deleted file mode 100644
index 5bf4400f22..0000000000
--- a/e2e/.nvmrc
+++ /dev/null
@@ -1 +0,0 @@
-24.15.0
diff --git a/e2e/mise.toml b/e2e/mise.toml
index c298115e40..99056f9ead 100644
--- a/e2e/mise.toml
+++ b/e2e/mise.toml
@@ -27,3 +27,18 @@ run = { task = "lint --fix" }
[tasks.check]
env._.path = "./node_modules/.bin"
run = "tsc --noEmit"
+
+
+[tasks.ci-setup]
+depends = ["//:sdk:install", "//:sdk:build", "//cli:install", "//cli:build"]
+run = { task = ":install" }
+
+
+[tasks.ci-unit]
+depends = ["//:sdk:install", "//:sdk:build"]
+run = [
+ { task = ":install" },
+ { task = ":format" },
+ { task = ":lint" },
+ { task = ":check" },
+]
diff --git a/e2e/package.json b/e2e/package.json
index a58c709a6d..00868d001d 100644
--- a/e2e/package.json
+++ b/e2e/package.json
@@ -56,8 +56,5 @@
"utimes": "^5.2.1",
"vite-tsconfig-paths": "^6.1.1",
"vitest": "^4.0.0"
- },
- "volta": {
- "node": "24.15.0"
}
}
diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts
index 3d7971d6f0..5fd887c44b 100644
--- a/e2e/src/responses.ts
+++ b/e2e/src/responses.ts
@@ -2,82 +2,44 @@ import { expect } from 'vitest';
export const errorDto = {
unauthorized: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Authentication required',
- correlationId: expect.any(String),
},
unauthorizedWithMessage: (message: string) => ({
- error: 'Unauthorized',
- statusCode: 401,
message,
- correlationId: expect.any(String),
}),
forbidden: {
- error: 'Forbidden',
- statusCode: 403,
message: expect.any(String),
- correlationId: expect.any(String),
},
missingPermission: (permission: string) => ({
- error: 'Forbidden',
- statusCode: 403,
message: `Missing required permission: ${permission}`,
- correlationId: expect.any(String),
}),
wrongPassword: {
- error: 'Bad Request',
- statusCode: 400,
message: 'Wrong password',
- correlationId: expect.any(String),
},
invalidToken: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid user token',
- correlationId: expect.any(String),
},
invalidShareKey: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Invalid share key',
- correlationId: expect.any(String),
},
passwordRequired: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Password required',
- correlationId: expect.any(String),
},
badRequest: (message: any = null) => ({
- error: 'Bad Request',
- statusCode: 400,
message: message ?? expect.anything(),
- correlationId: expect.any(String),
+ }),
+ validationError: (errors?: ReadonlyArray<{ path: ReadonlyArray; message: string }>) => ({
+ message: 'Validation failed',
+ errors: errors ? expect.arrayContaining(errors.map((e) => expect.objectContaining(e))) : expect.any(Array),
}),
noPermission: {
- error: 'Bad Request',
- statusCode: 400,
message: expect.stringContaining('Not found or no'),
- correlationId: expect.any(String),
},
incorrectLogin: {
- error: 'Unauthorized',
- statusCode: 401,
message: 'Incorrect email or password',
- correlationId: expect.any(String),
},
alreadyHasAdmin: {
- error: 'Bad Request',
- statusCode: 400,
message: 'The server already has an admin',
- correlationId: expect.any(String),
- },
- invalidEmail: {
- error: 'Bad Request',
- statusCode: 400,
- message: ['email must be an email'],
- correlationId: expect.any(String),
},
};
diff --git a/e2e/src/specs/server/api/album.e2e-spec.ts b/e2e/src/specs/server/api/album.e2e-spec.ts
index e1e5178476..55b9c44b70 100644
--- a/e2e/src/specs/server/api/album.e2e-spec.ts
+++ b/e2e/src/specs/server/api/album.e2e-spec.ts
@@ -146,7 +146,7 @@ describe('/albums', () => {
it('should not return shared albums with a deleted owner', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=true')
+ .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -188,7 +188,7 @@ describe('/albums', () => {
it('should return the album collection including owned and shared', async () => {
const { status, body } = await request(app).get('/albums').set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
- expect(body).toHaveLength(4);
+ expect(body).toHaveLength(5);
expect(body).toEqual(
expect.arrayContaining([
expect.objectContaining({
@@ -219,13 +219,20 @@ describe('/albums', () => {
]),
shared: false,
}),
+ expect.objectContaining({
+ albumName: user2SharedUser,
+ albumUsers: expect.arrayContaining([
+ { role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
+ ]),
+ shared: true,
+ }),
]),
);
});
- it('should return the album collection filtered by shared', async () => {
+ it('should return the album collection filtered by isShared', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=true')
+ .get('/albums?isShared=true')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(4);
@@ -263,9 +270,9 @@ describe('/albums', () => {
);
});
- it('should return the album collection filtered by NOT shared', async () => {
+ it('should return the album collection filtered by NOT isShared', async () => {
const { status, body } = await request(app)
- .get('/albums?shared=false')
+ .get('/albums?isShared=false')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(1);
@@ -282,6 +289,63 @@ describe('/albums', () => {
);
});
+ it('should return only owned albums when filtered by isOwned=true', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=true')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(4);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ albumName: user1SharedEditorUser }),
+ expect.objectContaining({ albumName: user1SharedViewerUser }),
+ expect.objectContaining({ albumName: user1SharedLink }),
+ expect.objectContaining({ albumName: user1NotShared }),
+ ]),
+ );
+ });
+
+ it('should return only shared-with-me albums when filtered by isOwned=false', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=false')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(1);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({
+ albumName: user2SharedUser,
+ albumUsers: expect.arrayContaining([
+ { role: AlbumUserRole.Owner, user: expect.objectContaining({ id: user2.userId }) },
+ ]),
+ }),
+ ]),
+ );
+ });
+
+ it('should return owned shared-out albums when filtered by isOwned=true&ishared=true', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=true&isShared=true')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(3);
+ expect(body).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ albumName: user1SharedEditorUser }),
+ expect.objectContaining({ albumName: user1SharedViewerUser }),
+ expect.objectContaining({ albumName: user1SharedLink }),
+ ]),
+ );
+ });
+
+ it('should return empty list when filtered by isOwned=false&isShared=false', async () => {
+ const { status, body } = await request(app)
+ .get('/albums?isOwned=false&isShared=false')
+ .set('Authorization', `Bearer ${user1.accessToken}`);
+ expect(status).toBe(200);
+ expect(body).toHaveLength(0);
+ });
+
it('should return the album collection filtered by assetId', async () => {
const { status, body } = await request(app)
.get(`/albums?assetId=${user1Asset2.id}`)
@@ -290,17 +354,17 @@ describe('/albums', () => {
expect(body).toHaveLength(2);
});
- it('should return the album collection filtered by assetId and ignores shared=true', async () => {
+ it('should return the album collection filtered by assetId and ignores isShared=true', async () => {
const { status, body } = await request(app)
- .get(`/albums?shared=true&assetId=${user1Asset1.id}`)
+ .get(`/albums?isShared=true&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
});
- it('should return the album collection filtered by assetId and ignores shared=false', async () => {
+ it('should return the album collection filtered by assetId and ignores isShared=false', async () => {
const { status, body } = await request(app)
- .get(`/albums?shared=false&assetId=${user1Asset1.id}`)
+ .get(`/albums?isShared=false&assetId=${user1Asset1.id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(5);
diff --git a/e2e/src/specs/server/api/library.e2e-spec.ts b/e2e/src/specs/server/api/library.e2e-spec.ts
index 719436a66d..ccb594610c 100644
--- a/e2e/src/specs/server/api/library.e2e-spec.ts
+++ b/e2e/src/specs/server/api/library.e2e-spec.ts
@@ -110,7 +110,9 @@ describe('/libraries', () => {
});
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
+ );
});
it('should not create an external library with duplicate exclusion patterns', async () => {
@@ -125,7 +127,9 @@ describe('/libraries', () => {
});
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
+ );
});
});
@@ -157,7 +161,9 @@ describe('/libraries', () => {
.send({ name: '' });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[name] Too small: expected string to have >=1 characters']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['name'], message: 'Too small: expected string to have >=1 characters' }]),
+ );
});
it('should change the import paths', async () => {
@@ -181,7 +187,9 @@ describe('/libraries', () => {
.send({ importPaths: [''] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[importPaths] Array items must not be empty']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['importPaths'], message: 'Array items must not be empty' }]),
+ );
});
it('should reject duplicate import paths', async () => {
@@ -191,7 +199,9 @@ describe('/libraries', () => {
.send({ importPaths: ['/path', '/path'] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[importPaths] Array must have unique items']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['importPaths'], message: 'Array must have unique items' }]),
+ );
});
it('should change the exclusion pattern', async () => {
@@ -215,7 +225,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: ['**/*.jpg', '**/*.jpg'] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array must have unique items']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array must have unique items' }]),
+ );
});
it('should reject an empty exclusion pattern', async () => {
@@ -225,7 +237,9 @@ describe('/libraries', () => {
.send({ exclusionPatterns: [''] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[exclusionPatterns] Array items must not be empty']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['exclusionPatterns'], message: 'Array items must not be empty' }]),
+ );
});
});
diff --git a/e2e/src/specs/server/api/map.e2e-spec.ts b/e2e/src/specs/server/api/map.e2e-spec.ts
index c280deb134..86664b2dc4 100644
--- a/e2e/src/specs/server/api/map.e2e-spec.ts
+++ b/e2e/src/specs/server/api/map.e2e-spec.ts
@@ -109,7 +109,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lon=123')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
+ );
});
it('should throw an error if a lat is not a number', async () => {
@@ -117,7 +119,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=abc&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[lat] Invalid input: expected number, received NaN']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['lat'], message: 'Invalid input: expected number, received NaN' }]),
+ );
});
it('should throw an error if a lat is out of range', async () => {
@@ -125,7 +129,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=91&lon=123.456')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[lat] Too big: expected number to be <=90']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['lat'], message: 'Too big: expected number to be <=90' }]),
+ );
});
it('should throw an error if a lon is not provided', async () => {
@@ -133,7 +139,9 @@ describe('/map', () => {
.get('/map/reverse-geocode?lat=75')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[lon] Invalid input: expected number, received NaN']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['lon'], message: 'Invalid input: expected number, received NaN' }]),
+ );
});
const reverseGeocodeTestCases = [
diff --git a/e2e/src/specs/server/api/oauth.e2e-spec.ts b/e2e/src/specs/server/api/oauth.e2e-spec.ts
index 9dcb431a4b..4bf4f197b1 100644
--- a/e2e/src/specs/server/api/oauth.e2e-spec.ts
+++ b/e2e/src/specs/server/api/oauth.e2e-spec.ts
@@ -105,7 +105,11 @@ describe(`/oauth`, () => {
it(`should throw an error if a redirect uri is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/authorize').send({});
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[redirectUri] Invalid input: expected string, received undefined']));
+ expect(body).toEqual(
+ errorDto.validationError([
+ { path: ['redirectUri'], message: 'Invalid input: expected string, received undefined' },
+ ]),
+ );
});
it('should return a redirect uri', async () => {
@@ -164,13 +168,17 @@ describe(`/oauth`, () => {
it(`should throw an error if a url is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({});
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[url] Invalid input: expected string, received undefined']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['url'], message: 'Invalid input: expected string, received undefined' }]),
+ );
});
it(`should throw an error if the url is empty`, async () => {
const { status, body } = await request(app).post('/oauth/callback').send({ url: '' });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[url] Too small: expected string to have >=1 characters']));
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['url'], message: 'Too small: expected string to have >=1 characters' }]),
+ );
});
it(`should throw an error if the state is not provided`, async () => {
@@ -332,9 +340,7 @@ describe(`/oauth`, () => {
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(500);
expect(body).toMatchObject({
- error: 'Internal Server Error',
message: 'Failed to finish oauth',
- statusCode: 500,
});
});
@@ -353,7 +359,7 @@ describe(`/oauth`, () => {
const callbackParams = await loginWithOAuth('oauth-no-auto-register');
const { status, body } = await request(app).post('/oauth/callback').send(callbackParams);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest('User does not exist and auto registering is disabled.'));
+ expect(body).toEqual(errorDto.badRequest('OAuth authentication failed'));
});
it('should link to an existing user by email', async () => {
@@ -377,7 +383,11 @@ describe(`/oauth`, () => {
it(`should throw an error if the logout_token is not provided`, async () => {
const { status, body } = await request(app).post('/oauth/backchannel-logout').send({});
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[logout_token] Invalid input: expected string, received undefined']));
+ expect(body).toEqual(
+ errorDto.validationError([
+ { path: ['logout_token'], message: 'Invalid input: expected string, received undefined' },
+ ]),
+ );
});
it(`should throw an error if an invalid logout token is provided`, async () => {
@@ -495,11 +505,10 @@ describe(`/oauth`, () => {
});
it('should reject OAuth discovery over HTTP', async () => {
- const { status, body } = await request(app)
+ const { status } = await request(app)
.post('/oauth/authorize')
.send({ redirectUri: 'http://127.0.0.1:2285/auth/login' });
expect(status).toBe(500);
- expect(body).toMatchObject({ statusCode: 500 });
});
});
});
diff --git a/e2e/src/specs/server/api/shared-link.e2e-spec.ts b/e2e/src/specs/server/api/shared-link.e2e-spec.ts
index 1d069d0f54..8cdf2dc03c 100644
--- a/e2e/src/specs/server/api/shared-link.e2e-spec.ts
+++ b/e2e/src/specs/server/api/shared-link.e2e-spec.ts
@@ -341,7 +341,9 @@ describe('/shared-links', () => {
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest());
+ expect(body).toEqual(
+ errorDto.validationError([{ path: [], message: 'Invalid input: expected object, received undefined' }]),
+ );
});
it('should require an asset/album id', async () => {
diff --git a/e2e/src/specs/server/api/stack.e2e-spec.ts b/e2e/src/specs/server/api/stack.e2e-spec.ts
index 91dd0d2a8e..76bf514dc8 100644
--- a/e2e/src/specs/server/api/stack.e2e-spec.ts
+++ b/e2e/src/specs/server/api/stack.e2e-spec.ts
@@ -41,7 +41,9 @@ describe('/stacks', () => {
.send({ assetIds: [asset.id] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest());
+ expect(body).toEqual(
+ errorDto.validationError([{ path: ['assetIds'], message: 'Too small: expected array to have >=2 items' }]),
+ );
});
it('should require a valid id', async () => {
@@ -51,7 +53,12 @@ describe('/stacks', () => {
.send({ assetIds: [uuidDto.invalid, uuidDto.invalid] });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest());
+ expect(body).toEqual(
+ errorDto.validationError([
+ { path: ['assetIds', 0], message: 'Invalid UUID' },
+ { path: ['assetIds', 1], message: 'Invalid UUID' },
+ ]),
+ );
});
it('should require access', async () => {
diff --git a/e2e/src/specs/server/api/tag.e2e-spec.ts b/e2e/src/specs/server/api/tag.e2e-spec.ts
index 7b5a2f16de..d303a1e98d 100644
--- a/e2e/src/specs/server/api/tag.e2e-spec.ts
+++ b/e2e/src/specs/server/api/tag.e2e-spec.ts
@@ -309,7 +309,7 @@ describe('/tags', () => {
.get(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
+ expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should get tag details', async () => {
@@ -427,7 +427,7 @@ describe('/tags', () => {
.delete(`/tags/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest(['[id] Invalid UUID']));
+ expect(body).toEqual(errorDto.validationError([{ path: ['id'], message: 'Invalid UUID' }]));
});
it('should delete a tag', async () => {
diff --git a/e2e/src/specs/server/api/user-admin.e2e-spec.ts b/e2e/src/specs/server/api/user-admin.e2e-spec.ts
index 6751b21e84..df6fea84bc 100644
--- a/e2e/src/specs/server/api/user-admin.e2e-spec.ts
+++ b/e2e/src/specs/server/api/user-admin.e2e-spec.ts
@@ -108,14 +108,20 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
- for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
+ for (const [key, message] of [
+ ['password', 'Invalid input: expected string, received null'],
+ ['email', 'Invalid input: expected email, received object'],
+ ['name', 'Invalid input: expected string, received null'],
+ ['shouldChangePassword', 'Invalid input: expected boolean, received null'],
+ ['notify', 'Invalid input: expected boolean, received null'],
+ ] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ ...createUserDto.user1, [key]: null });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest());
+ expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
@@ -153,14 +159,19 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
- for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
+ for (const [key, message] of [
+ ['password', 'Invalid input: expected string, received null'],
+ ['email', 'Invalid input: expected email, received object'],
+ ['name', 'Invalid input: expected string, received null'],
+ ['shouldChangePassword', 'Invalid input: expected boolean, received null'],
+ ] as const) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
.set('Authorization', `Bearer ${admin.accessToken}`)
.send({ [key]: null });
expect(status).toBe(400);
- expect(body).toEqual(errorDto.badRequest());
+ expect(body).toEqual(errorDto.validationError([{ path: [key], message }]));
});
}
diff --git a/e2e/src/specs/server/api/user.e2e-spec.ts b/e2e/src/specs/server/api/user.e2e-spec.ts
index ee13a29c1b..8a2197efde 100644
--- a/e2e/src/specs/server/api/user.e2e-spec.ts
+++ b/e2e/src/specs/server/api/user.e2e-spec.ts
@@ -120,7 +120,7 @@ describe('/users', () => {
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(400);
- expect(body).toMatchObject(errorDto.badRequest('Email already in use by another account'));
+ expect(body).toMatchObject(errorDto.badRequest('Email is not available'));
});
it('should update my email', async () => {
@@ -179,7 +179,9 @@ describe('/users', () => {
expect(status).toBe(400);
expect(body).toEqual(
- errorDto.badRequest(['[download.archiveSize] Invalid input: expected int, received number']),
+ errorDto.validationError([
+ { path: ['download', 'archiveSize'], message: 'Invalid input: expected int, received number' },
+ ]),
);
});
@@ -207,7 +209,9 @@ describe('/users', () => {
expect(status).toBe(400);
expect(body).toEqual(
- errorDto.badRequest(['[download.includeEmbeddedVideos] Invalid input: expected boolean, received number']),
+ errorDto.validationError([
+ { path: ['download', 'includeEmbeddedVideos'], message: 'Invalid input: expected boolean, received number' },
+ ]),
);
});
diff --git a/e2e/src/ui/generators/timeline/model-objects.ts b/e2e/src/ui/generators/timeline/model-objects.ts
index e300de1161..f5654afd5e 100644
--- a/e2e/src/ui/generators/timeline/model-objects.ts
+++ b/e2e/src/ui/generators/timeline/model-objects.ts
@@ -32,8 +32,12 @@ export function generateThumbhash(rng: SeededRandom): string {
return Array.from({ length: 10 }, () => rng.nextInt(0, 256).toString(16).padStart(2, '0')).join('');
}
-export function generateDuration(rng: SeededRandom): string {
- return `${rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS)}.${rng.nextInt(0, 1000).toString().padStart(3, '0')}`;
+export function generateDuration(rng: SeededRandom): number {
+ return (
+ rng.nextInt(GENERATION_CONSTANTS.MIN_VIDEO_DURATION_SECONDS, GENERATION_CONSTANTS.MAX_VIDEO_DURATION_SECONDS) *
+ 1000 +
+ rng.nextInt(0, 1000)
+ );
}
export function generateUUID(): string {
diff --git a/e2e/src/ui/generators/timeline/timeline-config.ts b/e2e/src/ui/generators/timeline/timeline-config.ts
index 992480eef9..4dea2f4f78 100644
--- a/e2e/src/ui/generators/timeline/timeline-config.ts
+++ b/e2e/src/ui/generators/timeline/timeline-config.ts
@@ -43,7 +43,7 @@ export type MockTimelineAsset = {
isTrashed: boolean;
isVideo: boolean;
isImage: boolean;
- duration: string | null;
+ duration: number | null;
projectionType: string | null;
livePhotoVideoId: string | null;
city: string | null;
diff --git a/e2e/src/ui/mock-network/base-network.ts b/e2e/src/ui/mock-network/base-network.ts
index 3dc3580396..6680b83dd1 100644
--- a/e2e/src/ui/mock-network/base-network.ts
+++ b/e2e/src/ui/mock-network/base-network.ts
@@ -240,7 +240,8 @@ export const setupBaseMockApiRoutes = async (context: BrowserContext, adminUserI
});
});
await context.route('**/api/albums*', async (route, request) => {
- if (request.url().endsWith('albums?shared=true') || request.url().endsWith('albums')) {
+ const url = request.url();
+ if (url.endsWith('albums?isShared=true') || url.endsWith('albums?isOwned=true') || url.endsWith('albums')) {
return route.fulfill({
status: 200,
contentType: 'application/json',
diff --git a/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts
index 5069a46a91..c2a3b8e724 100644
--- a/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts
+++ b/e2e/src/ui/specs/timeline/timeline.e2e-spec.ts
@@ -304,7 +304,7 @@ test.describe('Timeline', () => {
await page.keyboard.down('Shift');
await thumbnailUtils.withAssetId(page, assets[2].id).hover();
await expect(
- thumbnailUtils.locator(page).locator('.absolute.top-0.h-full.w-full.bg-immich-primary.opacity-40'),
+ thumbnailUtils.locator(page).locator('.absolute.top-0.size-full.bg-immich-primary.opacity-40'),
).toHaveCount(3);
await thumbnailUtils.selectButton(page, assets[2].id).click();
await page.keyboard.up('Shift');
diff --git a/e2e/test-assets b/e2e/test-assets
index 0eac5a3738..6742055402 160000
--- a/e2e/test-assets
+++ b/e2e/test-assets
@@ -1 +1 @@
-Subproject commit 0eac5a37384c151be88381b41f9e28d8d59a4466
+Subproject commit 6742055402de1aa48f93d12ded7d18f4057f9d1f
diff --git a/i18n/en.json b/i18n/en.json
index add755c05d..5e073559ac 100644
--- a/i18n/en.json
+++ b/i18n/en.json
@@ -1240,6 +1240,7 @@
"free_up_space_description": "Move backed-up photos and videos to your device's trash to free up space. Your copies on the server remain safe.",
"free_up_space_settings_subtitle": "Free up device storage",
"full_path": "Full path: {path}",
+ "full_path_or_folder": "Full path or folder",
"gcast_enabled": "Google Cast",
"gcast_enabled_description": "This feature loads external resources from Google in order to work.",
"general": "General",
@@ -1523,6 +1524,38 @@
"marked_all_as_read": "Marked all as read",
"matches": "Matches",
"matching_assets": "Matching Assets",
+ "media_chrome": {
+ "auto": "Auto",
+ "captions": "Captions",
+ "captions_off": "Off",
+ "closed_captions": "closed captions",
+ "decode_error": "Decode error",
+ "disable_captions": "Disable captions",
+ "enable_captions": "Enable captions",
+ "enter_fullscreen_mode": "Enter fullscreen mode",
+ "exit_fullscreen_mode": "Exit fullscreen mode",
+ "loop": "Loop",
+ "media_error_description": "A media error caused playback to be aborted. The media could be corrupt or your browser does not support this format.",
+ "media_loading": "media loading",
+ "mute": "Mute",
+ "network_error": "Network error",
+ "network_error_description": "A network error caused the media download to fail.",
+ "not_supported_error": "Source Not Supported",
+ "playback_rate": "Playback rate",
+ "playback_rate_current": "current playback rate",
+ "playback_rate_value": "Playback rate {playbackRate}",
+ "playback_time": "playback time",
+ "quality": "Quality",
+ "second": "second",
+ "seconds": "seconds",
+ "time_value_of_total_time": "{currentTime} of {totalTime}",
+ "time_value_remaining": "{time} remaining",
+ "unmute": "Unmute",
+ "unsupported_error_description": "An unsupported error occurred. The server or network failed, or your browser does not support this format.",
+ "video_not_loaded_unknown_time": "video not loaded, unknown time.",
+ "video_player": "video player",
+ "volume": "volume"
+ },
"media_type": "Media type",
"memories": "Memories",
"memories_all_caught_up": "All caught up",
@@ -1942,6 +1975,8 @@
"search_by_description_example": "Hiking day in Sapa",
"search_by_filename": "Search by file name or extension",
"search_by_filename_example": "i.e. IMG_1234.JPG or PNG",
+ "search_by_full_path": "Search by full path or folder",
+ "search_by_full_path_example": "/John/Projects/3D_Printing/2026-07-01 - you can search for Projects, 3D, Printing, 2026 etc.",
"search_by_ocr": "Search by OCR",
"search_by_ocr_example": "Latte",
"search_camera_lens_model": "Search lens model...",
@@ -2436,6 +2471,7 @@
"workflows": "Workflows",
"workflows_help_text": "Workflows automate actions on your assets based on triggers and filters",
"wrong_pin_code": "Wrong PIN code",
+ "x_of_total": "{x}/{total}",
"year": "Year",
"years_ago": "{years, plural, one {# year} other {# years}} ago",
"yes": "Yes",
diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile
index 46c32f3d6a..c6f9f01675 100644
--- a/machine-learning/Dockerfile
+++ b/machine-learning/Dockerfile
@@ -68,7 +68,7 @@ ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
RUN apt-get update && \
# Pascal support was dropped in 9.11
- apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 && \
+ apt-get install --no-install-recommends -yqq libcudnn9-cuda-12=9.10.2.21-1 tzdata && \
apt-get clean && \
rm -rf /var/lib/apt/lists/*
@@ -112,7 +112,7 @@ ARG RKNN_TOOLKIT_VERSION="v2.3.0"
ENV LD_PRELOAD=/usr/lib/libmimalloc.so.2 \
MACHINE_LEARNING_MODEL_ARENA=false
-ADD --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
+ADD --chmod=644 --checksum=sha256:73993ed4b440460825f21611731564503cc1d5a0c123746477da6cd574f34885 "https://github.com/airockchip/rknn-toolkit2/raw/refs/tags/${RKNN_TOOLKIT_VERSION}/rknpu2/runtime/Linux/librknn_api/aarch64/librknnrt.so" /usr/lib/
FROM prod-${DEVICE} AS prod
diff --git a/machine-learning/immich_ml/main.py b/machine-learning/immich_ml/main.py
index e7e3a719bb..4fca7a2e2b 100644
--- a/machine-learning/immich_ml/main.py
+++ b/machine-learning/immich_ml/main.py
@@ -183,7 +183,10 @@ async def predict(
text: str | None = Form(default=None),
) -> Any:
if image is not None:
- inputs: Image | str = await run(lambda: decode_pil(image))
+ decoded = await run(lambda: decode_pil(image))
+ if decoded.width == 0 or decoded.height == 0:
+ raise HTTPException(400, "Image has zero width or height")
+ inputs: Image | str = decoded
elif text is not None:
inputs = text
else:
diff --git a/machine-learning/mise.toml b/machine-learning/mise.toml
new file mode 100644
index 0000000000..e5e30c4fc2
--- /dev/null
+++ b/machine-learning/mise.toml
@@ -0,0 +1,36 @@
+[tools]
+python = "3.11"
+uv = "0.8.15"
+
+[tasks.install]
+run = "uv sync --locked"
+
+[tasks.lint]
+run = "uv run ruff check immich_ml"
+
+[tasks.test]
+run = "uv run pytest --cov=immich_ml --cov-report term-missing"
+
+[tasks.format]
+run = "uv run ruff format immich_ml"
+
+[tasks.check]
+run = "uv run mypy --strict immich_ml/"
+
+[tasks.ci-unit]
+run = [
+ { task = ":install --extra cpu" },
+ { task = ":format" },
+ { task = ":lint --output-format=github" },
+ { task = ":check" },
+ { task = ":test" },
+]
+
+[tasks.checklist]
+run = [
+ { task = ":install" },
+ { task = ":format" },
+ { task = ":lint" },
+ { task = ":check" },
+ { task = ":test" },
+]
diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml
index d61df51e38..f706a1f125 100644
--- a/machine-learning/pyproject.toml
+++ b/machine-learning/pyproject.toml
@@ -11,7 +11,7 @@ dependencies = [
"gunicorn>=21.1.0",
"huggingface-hub>=1.0,<2.0",
"insightface>=0.7.3,<1.0",
- "numpy<2.4.0",
+ "numpy>=2.4.0,<3.0",
"opencv-python-headless>=4.7.0.72,<5.0",
"orjson>=3.9.5",
"pillow>=12.2,<13",
diff --git a/machine-learning/test_main.py b/machine-learning/test_main.py
index 0182c57c67..cce334e40e 100644
--- a/machine-learning/test_main.py
+++ b/machine-learning/test_main.py
@@ -1198,6 +1198,19 @@ class TestLoad:
mock_model.model_format = ModelFormat.ONNX
+@pytest.mark.parametrize("size", [(0, 100), (100, 0), (0, 0)])
+def test_predict_rejects_empty_image(size: tuple[int, int], deployed_app: TestClient) -> None:
+ with mock.patch("immich_ml.main.decode_pil", return_value=Image.new("RGB", size)):
+ response = deployed_app.post(
+ "http://localhost:3003/predict",
+ data={"entries": json.dumps({"clip": {"visual": {"modelName": "ViT-B-32__openai"}}})},
+ files={"image": b"fake image bytes"},
+ )
+
+ assert response.status_code == 400
+ assert "zero" in response.json()["detail"].lower()
+
+
def test_root_endpoint(deployed_app: TestClient) -> None:
response = deployed_app.get("http://localhost:3003")
diff --git a/machine-learning/uv.lock b/machine-learning/uv.lock
index 894acf77f5..5623c553df 100644
--- a/machine-learning/uv.lock
+++ b/machine-learning/uv.lock
@@ -243,14 +243,14 @@ wheels = [
[[package]]
name = "click"
-version = "8.1.7"
+version = "8.3.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "colorama", marker = "sys_platform == 'win32'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/96/d3/f04c7bfcf5c1862a2a5b845c6b2b360488cf47af55dfa79c98f6a6bf98b5/click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de", size = 336121, upload-time = "2023-08-17T17:29:11.868Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", size = 97941, upload-time = "2023-08-17T17:29:10.08Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" },
]
[[package]]
@@ -785,17 +785,34 @@ wheels = [
[[package]]
name = "hf-xet"
-version = "1.1.7"
+version = "1.5.0"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/b2/0a/a0f56735940fde6dd627602fec9ab3bad23f66a272397560abd65aba416e/hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80", size = 477719, upload-time = "2025-08-06T00:30:55.741Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/74/d8/5c06fc76461418326a7decf8367480c35be11a41fd938633929c60a9ec6b/hf_xet-1.5.0.tar.gz", hash = "sha256:e0fb0a34d9f406eed88233e829a67ec016bec5af19e480eac65a233ea289a948", size = 837196, upload-time = "2026-05-06T06:18:15.583Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/b1/7c/8d7803995caf14e7d19a392a486a040f923e2cfeff824e9b800b92072f76/hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde", size = 2761743, upload-time = "2025-08-06T00:30:50.634Z" },
- { url = "https://files.pythonhosted.org/packages/51/a3/fa5897099454aa287022a34a30e68dbff0e617760f774f8bd1db17f06bd4/hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e", size = 2624331, upload-time = "2025-08-06T00:30:49.212Z" },
- { url = "https://files.pythonhosted.org/packages/86/50/2446a132267e60b8a48b2e5835d6e24fd988000d0f5b9b15ebd6d64ef769/hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f", size = 3183844, upload-time = "2025-08-06T00:30:47.582Z" },
- { url = "https://files.pythonhosted.org/packages/20/8f/ccc670616bb9beee867c6bb7139f7eab2b1370fe426503c25f5cbb27b148/hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513", size = 3074209, upload-time = "2025-08-06T00:30:45.509Z" },
- { url = "https://files.pythonhosted.org/packages/21/0a/4c30e1eb77205565b854f5e4a82cf1f056214e4dc87f2918ebf83d47ae14/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8", size = 3239602, upload-time = "2025-08-06T00:30:52.41Z" },
- { url = "https://files.pythonhosted.org/packages/f5/1e/fc7e9baf14152662ef0b35fa52a6e889f770a7ed14ac239de3c829ecb47e/hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604", size = 3348184, upload-time = "2025-08-06T00:30:54.105Z" },
- { url = "https://files.pythonhosted.org/packages/a3/73/e354eae84ceff117ec3560141224724794828927fcc013c5b449bf0b8745/hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34", size = 2820008, upload-time = "2025-08-06T00:30:57.056Z" },
+ { url = "https://files.pythonhosted.org/packages/68/9b/6912c99070915a4f28119e3c5b52a9abd1eec0ad5cb293b8c967a0c6f5a2/hf_xet-1.5.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7d70fe2ce97b9db73b9c9b9c81fe3693640aec83416a966c446afea54acfae3c", size = 4023383, upload-time = "2026-05-06T06:17:53.947Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/6d/9563cfde59b5d8128a9c7ec972a087f4c782e4f7bac5a85234edfd5d5e49/hf_xet-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:73a0dae8c71de3b0633a45c73f4a4a5ed09e94b43441d82981a781d4f12baa42", size = 3792751, upload-time = "2026-05-06T06:17:51.791Z" },
+ { url = "https://files.pythonhosted.org/packages/07/a5/ed5a0cf35b49a0571af5a8f53416dad1877a718c021c9937c3a53cb45781/hf_xet-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a60290ec57e9b71767fba7c3645ddafdd0759974b540441510c629c6db6db24a", size = 4456058, upload-time = "2026-05-06T06:17:40.735Z" },
+ { url = "https://files.pythonhosted.org/packages/60/fb/3ae8bf2a7a37a4197d0195d7247fd25b3952e15cb8a599e285dfaa6f52b3/hf_xet-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e5de0f6deada0dada870bb376a11bcd1f08abf3a968a6d118f33e72d1b1eb480", size = 4250783, upload-time = "2026-05-06T06:17:38.412Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/9b/8bae40d4d91525085137196e84eb0ed49cf65b5e96e5c3ecdadd8bd0fac2/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c799d49f1a5544a0ef7591c0ee75e0d6b93d6f56dc7a4979f59f7518d2872216", size = 4445594, upload-time = "2026-05-06T06:18:04.219Z" },
+ { url = "https://files.pythonhosted.org/packages/13/59/c74efbbd4e8728172b2cc72a2bc014d2947a4b7bdced932fbd3f5da1a4e5/hf_xet-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2baea1b0b989e5c152fe81425f7745ddc8901280ba3d97c98d8cdece7b706c60", size = 4663995, upload-time = "2026-05-06T06:18:06.1Z" },
+ { url = "https://files.pythonhosted.org/packages/73/32/8e1e0410af64cda9b139d1dcebdc993a8ff9c8c7c0e2696ae356d75ccc0d/hf_xet-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:526345b3ed45f374f6317349df489167606736c876241ba984105afe7fd4839d", size = 3966608, upload-time = "2026-05-06T06:18:19.74Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/34/a8febc8f4edbea8b3e21b02ebc8b628679b84ba7e45cde624a7736b51500/hf_xet-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:786d28e2eb8315d5035544b9d137b4a842d600c434bb91bf7d0d953cce906ad4", size = 3796946, upload-time = "2026-05-06T06:18:17.568Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/20/8fc8996afe5815fa1a6be8e9e5c02f24500f409d599e905800d498a4e14d/hf_xet-1.5.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:872d5601e6deea30d15865ede55d29eac6daf5a534ab417b99b6ef6b076dd96c", size = 4023495, upload-time = "2026-05-06T06:18:01.94Z" },
+ { url = "https://files.pythonhosted.org/packages/32/6a/93d84463c00cecb561a7508aa6303e35ee2894294eac14245526924415fe/hf_xet-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9929561f5abf4581c8ea79587881dfef6b8abb2a0d8a51915936fc2a614f4e73", size = 3792731, upload-time = "2026-05-06T06:18:00.021Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/5a/8ec8e0c863b382d00b3c2e2af6ded6b06371be617144a625903a6d562f4b/hf_xet-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f7b7bbae318e583a86fb21e5a4a175d6721d628a2874f4bd022d0e660c32a682", size = 4456738, upload-time = "2026-05-06T06:17:49.574Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/ca/f7effa1a67717da2bcc6b6c28f71c6ca648c77acaec4e2c32f40cbe16d85/hf_xet-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:cf7b2dc6f31a4ea754bb50f74cde482dcf5d366d184076d8530b9872787f3761", size = 4251622, upload-time = "2026-05-06T06:17:47.096Z" },
+ { url = "https://files.pythonhosted.org/packages/65/f2/19247dba3e231cf77dec59ddfb878f00057635ff773d099c9b59d37812c3/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8dbcbab554c9ef158ef2c991545c3e970ddd8cc7acdcd0a78c5a41095dab4ded", size = 4445667, upload-time = "2026-05-06T06:18:11.983Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/64/6f116801a3bcfb6f59f5c251f48cadc47ea54026441c4a385079286a94fa/hf_xet-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5906bf7718d3636dc13402914736abe723492cb730f744834f5f5b67d3a12702", size = 4664619, upload-time = "2026-05-06T06:18:13.771Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e8/069542d37946ed08669b127e1496fa99e78196d71de8d41eda5e9f1b7a58/hf_xet-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:5f3dc2248fc01cc0a00cd392ab497f1ca373fcbc7e3f2da1f452480b384e839e", size = 3966802, upload-time = "2026-05-06T06:18:28.162Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/91/fc6fdec27b14d04e88c386ac0a0129732b53fa23f7c4a78f4b83a039c567/hf_xet-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b285cea1b5bab46b758772716ba8d6854a1a0310fed1c249d678a8b38601e5a0", size = 3797168, upload-time = "2026-05-06T06:18:26.287Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/fb/69ff198a82cae7eb1a69fb84d93b3a3e4816564d76817fe541ddc96874eb/hf_xet-1.5.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:dad0dc84e941b8ba3c860659fe1fdc35c049d47cce293f003287757e971a8f56", size = 4030814, upload-time = "2026-05-06T06:17:57.933Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/ff/edcc2b40162bef3ff78e14ab637e5f3b89243d6aee72f5949d3bb6a5af83/hf_xet-1.5.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fd6e5a9b0fdac4ed03ed45ef79254a655b1aaab514a02202617fbf643f5fdf7a", size = 3798444, upload-time = "2026-05-06T06:17:55.79Z" },
+ { url = "https://files.pythonhosted.org/packages/49/4d/103f76b04310e5e57656696cc184690d20c466af0bca3ca88f8c8ea5d4f3/hf_xet-1.5.0-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949", size = 4465986, upload-time = "2026-05-06T06:17:44.886Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/a2/546f47f464737b3edbab6f8ddb57f2599b93d2cbb66f06abb475ccb48651/hf_xet-1.5.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9a0ee58cd18d5ea799f7ed11290bbccbe56bdd8b1d97ca74b9cc49a3945d7a3b", size = 4259865, upload-time = "2026-05-06T06:17:42.639Z" },
+ { url = "https://files.pythonhosted.org/packages/95/7f/1be593c1f28613be2e196473481cd81bfc5910795e30a34e8f744f6cac4f/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1e60df5a42e9bed8628b6416af2cba4cba57ae9f02de226a06b020d98e1aab18", size = 4459835, upload-time = "2026-05-06T06:18:08.026Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b2/703569fc881f3284487e68cda7b42179978480da3c438042a6bbbb4a671c/hf_xet-1.5.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4b35549ce62601b84da4ff9b24d970032ace3d4430f52d91bcbb26c901d6c690", size = 4672414, upload-time = "2026-05-06T06:18:09.864Z" },
+ { url = "https://files.pythonhosted.org/packages/af/37/1b6def445c567286b50aa3b33828158e135b1be44938dde59f11382a500c/hf_xet-1.5.0-cp37-abi3-win_amd64.whl", hash = "sha256:2806c7c17b4d23f8d88f7c4814f838c3b6150773fe339c20af23e1cfaf2797e4", size = 3977238, upload-time = "2026-05-06T06:18:23.621Z" },
+ { url = "https://files.pythonhosted.org/packages/62/94/3b66b148778ee100dcfd69c2ca22b57b41b44d3063ceec934f209e9184ce/hf_xet-1.5.0-cp37-abi3-win_arm64.whl", hash = "sha256:b6c9df403040248c76d808d3e047d64db2d923bae593eb244c41e425cf6cd7be", size = 3806916, upload-time = "2026-05-06T06:18:21.7Z" },
]
[[package]]
@@ -857,21 +874,22 @@ wheels = [
[[package]]
name = "huggingface-hub"
-version = "0.36.2"
+version = "1.13.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "filelock" },
{ name = "fsspec" },
- { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" },
+ { name = "httpx" },
{ name = "packaging" },
{ name = "pyyaml" },
- { name = "requests" },
{ name = "tqdm" },
+ { name = "typer" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" },
+ { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" },
]
[[package]]
@@ -985,9 +1003,9 @@ requires-dist = [
{ name = "aiocache", specifier = ">=0.12.1,<1.0" },
{ name = "fastapi", specifier = ">=0.95.2,<1.0" },
{ name = "gunicorn", specifier = ">=21.1.0" },
- { name = "huggingface-hub", specifier = ">=0.20.1,<1.0" },
+ { name = "huggingface-hub", specifier = ">=1.0,<2.0" },
{ name = "insightface", specifier = ">=0.7.3,<1.0" },
- { name = "numpy", specifier = "<2.4.0" },
+ { name = "numpy", specifier = ">=2.4.0,<3.0" },
{ name = "onnxruntime", marker = "extra == 'armnn'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'cpu'", specifier = ">=1.23.2,<2" },
{ name = "onnxruntime", marker = "extra == 'rknn'", specifier = ">=1.23.2,<2" },
@@ -996,7 +1014,7 @@ requires-dist = [
{ name = "onnxruntime-openvino", marker = "extra == 'openvino'", specifier = ">=1.24.1,<2" },
{ name = "opencv-python-headless", specifier = ">=4.7.0.72,<5.0" },
{ name = "orjson", specifier = ">=3.9.5" },
- { name = "pillow", specifier = ">=12.2,<12.3" },
+ { name = "pillow", specifier = ">=12.2,<13" },
{ name = "pydantic", specifier = ">=2.0.0,<3" },
{ name = "pydantic-settings", specifier = ">=2.5.2,<3" },
{ name = "python-multipart", specifier = ">=0.0.6,<1.0" },
@@ -1540,83 +1558,81 @@ wheels = [
[[package]]
name = "numpy"
-version = "2.3.5"
+version = "2.4.4"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950, upload-time = "2025-11-16T22:52:42.067Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641, upload-time = "2025-11-16T22:49:19.336Z" },
- { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324, upload-time = "2025-11-16T22:49:22.582Z" },
- { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872, upload-time = "2025-11-16T22:49:25.408Z" },
- { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148, upload-time = "2025-11-16T22:49:27.549Z" },
- { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282, upload-time = "2025-11-16T22:49:30.964Z" },
- { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903, upload-time = "2025-11-16T22:49:34.191Z" },
- { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672, upload-time = "2025-11-16T22:49:37.2Z" },
- { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896, upload-time = "2025-11-16T22:49:39.727Z" },
- { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608, upload-time = "2025-11-16T22:49:42.079Z" },
- { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442, upload-time = "2025-11-16T22:49:43.99Z" },
- { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555, upload-time = "2025-11-16T22:49:47.092Z" },
- { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873, upload-time = "2025-11-16T22:49:49.84Z" },
- { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838, upload-time = "2025-11-16T22:49:52.863Z" },
- { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378, upload-time = "2025-11-16T22:49:55.055Z" },
- { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559, upload-time = "2025-11-16T22:49:57.371Z" },
- { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702, upload-time = "2025-11-16T22:49:59.632Z" },
- { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086, upload-time = "2025-11-16T22:50:02.127Z" },
- { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985, upload-time = "2025-11-16T22:50:04.536Z" },
- { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976, upload-time = "2025-11-16T22:50:07.557Z" },
- { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274, upload-time = "2025-11-16T22:50:10.746Z" },
- { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922, upload-time = "2025-11-16T22:50:12.811Z" },
- { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667, upload-time = "2025-11-16T22:50:16.16Z" },
- { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251, upload-time = "2025-11-16T22:50:19.013Z" },
- { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652, upload-time = "2025-11-16T22:50:21.487Z" },
- { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172, upload-time = "2025-11-16T22:50:24.562Z" },
- { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990, upload-time = "2025-11-16T22:50:26.47Z" },
- { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902, upload-time = "2025-11-16T22:50:28.861Z" },
- { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430, upload-time = "2025-11-16T22:50:31.56Z" },
- { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551, upload-time = "2025-11-16T22:50:34.242Z" },
- { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275, upload-time = "2025-11-16T22:50:37.651Z" },
- { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637, upload-time = "2025-11-16T22:50:40.11Z" },
- { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090, upload-time = "2025-11-16T22:50:42.503Z" },
- { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710, upload-time = "2025-11-16T22:50:44.971Z" },
- { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292, upload-time = "2025-11-16T22:50:47.715Z" },
- { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897, upload-time = "2025-11-16T22:50:51.327Z" },
- { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391, upload-time = "2025-11-16T22:50:54.542Z" },
- { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275, upload-time = "2025-11-16T22:50:56.794Z" },
- { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855, upload-time = "2025-11-16T22:50:59.208Z" },
- { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359, upload-time = "2025-11-16T22:51:01.991Z" },
- { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374, upload-time = "2025-11-16T22:51:05.291Z" },
- { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587, upload-time = "2025-11-16T22:51:08.585Z" },
- { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940, upload-time = "2025-11-16T22:51:11.541Z" },
- { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341, upload-time = "2025-11-16T22:51:14.312Z" },
- { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507, upload-time = "2025-11-16T22:51:16.846Z" },
- { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706, upload-time = "2025-11-16T22:51:19.558Z" },
- { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507, upload-time = "2025-11-16T22:51:22.492Z" },
- { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049, upload-time = "2025-11-16T22:51:25.171Z" },
- { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603, upload-time = "2025-11-16T22:51:27Z" },
- { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696, upload-time = "2025-11-16T22:51:29.402Z" },
- { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350, upload-time = "2025-11-16T22:51:32.167Z" },
- { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190, upload-time = "2025-11-16T22:51:35.403Z" },
- { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749, upload-time = "2025-11-16T22:51:39.698Z" },
- { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432, upload-time = "2025-11-16T22:51:42.476Z" },
- { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388, upload-time = "2025-11-16T22:51:45.275Z" },
- { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651, upload-time = "2025-11-16T22:51:47.749Z" },
- { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503, upload-time = "2025-11-16T22:51:50.443Z" },
- { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612, upload-time = "2025-11-16T22:51:53.609Z" },
- { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042, upload-time = "2025-11-16T22:51:56.213Z" },
- { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502, upload-time = "2025-11-16T22:51:58.584Z" },
- { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962, upload-time = "2025-11-16T22:52:01.698Z" },
- { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054, upload-time = "2025-11-16T22:52:04.267Z" },
- { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613, upload-time = "2025-11-16T22:52:08.651Z" },
- { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147, upload-time = "2025-11-16T22:52:11.453Z" },
- { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806, upload-time = "2025-11-16T22:52:14.641Z" },
- { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760, upload-time = "2025-11-16T22:52:17.975Z" },
- { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459, upload-time = "2025-11-16T22:52:20.55Z" },
- { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689, upload-time = "2025-11-16T22:52:23.247Z" },
- { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053, upload-time = "2025-11-16T22:52:26.367Z" },
- { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635, upload-time = "2025-11-16T22:52:29.266Z" },
- { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770, upload-time = "2025-11-16T22:52:31.421Z" },
- { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768, upload-time = "2025-11-16T22:52:33.593Z" },
- { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263, upload-time = "2025-11-16T22:52:36.369Z" },
- { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213, upload-time = "2025-11-16T22:52:39.38Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/c6/4218570d8c8ecc9704b5157a3348e486e84ef4be0ed3e38218ab473c83d2/numpy-2.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f983334aea213c99992053ede6168500e5f086ce74fbc4acc3f2b00f5762e9db", size = 16976799, upload-time = "2026-03-29T13:18:15.438Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/92/b4d922c4a5f5dab9ed44e6153908a5c665b71acf183a83b93b690996e39b/numpy-2.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:72944b19f2324114e9dc86a159787333b77874143efcf89a5167ef83cfee8af0", size = 14971552, upload-time = "2026-03-29T13:18:18.606Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/dc/df98c095978fa6ee7b9a9387d1d58cbb3d232d0e69ad169a4ce784bde4fd/numpy-2.4.4-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:86b6f55f5a352b48d7fbfd2dbc3d5b780b2d79f4d3c121f33eb6efb22e9a2015", size = 5476566, upload-time = "2026-03-29T13:18:21.532Z" },
+ { url = "https://files.pythonhosted.org/packages/28/34/b3fdcec6e725409223dd27356bdf5a3c2cc2282e428218ecc9cb7acc9763/numpy-2.4.4-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:ba1f4fc670ed79f876f70082eff4f9583c15fb9a4b89d6188412de4d18ae2f40", size = 6806482, upload-time = "2026-03-29T13:18:23.634Z" },
+ { url = "https://files.pythonhosted.org/packages/68/62/63417c13aa35d57bee1337c67446761dc25ea6543130cf868eace6e8157b/numpy-2.4.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a87ec22c87be071b6bdbd27920b129b94f2fc964358ce38f3822635a3e2e03d", size = 15973376, upload-time = "2026-03-29T13:18:26.677Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/c5/9fcb7e0e69cef59cf10c746b84f7d58b08bc66a6b7d459783c5a4f6101a6/numpy-2.4.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:df3775294accfdd75f32c74ae39fcba920c9a378a2fc18a12b6820aa8c1fb502", size = 16925137, upload-time = "2026-03-29T13:18:30.14Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/43/80020edacb3f84b9efdd1591120a4296462c23fd8db0dde1666f6ef66f13/numpy-2.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0d4e437e295f18ec29bc79daf55e8a47a9113df44d66f702f02a293d93a2d6dd", size = 17329414, upload-time = "2026-03-29T13:18:33.733Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/06/af0658593b18a5f73532d377188b964f239eb0894e664a6c12f484472f97/numpy-2.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6aa3236c78803afbcb255045fbef97a9e25a1f6c9888357d205ddc42f4d6eba5", size = 18658397, upload-time = "2026-03-29T13:18:37.511Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/ce/13a09ed65f5d0ce5c7dd0669250374c6e379910f97af2c08c57b0608eee4/numpy-2.4.4-cp311-cp311-win32.whl", hash = "sha256:30caa73029a225b2d40d9fae193e008e24b2026b7ee1a867b7ee8d96ca1a448e", size = 6239499, upload-time = "2026-03-29T13:18:40.372Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/63/05d193dbb4b5eec1eca73822d80da98b511f8328ad4ae3ca4caf0f4db91d/numpy-2.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:6bbe4eb67390b0a0265a2c25458f6b90a409d5d069f1041e6aff1e27e3d9a79e", size = 12614257, upload-time = "2026-03-29T13:18:42.95Z" },
+ { url = "https://files.pythonhosted.org/packages/87/c5/8168052f080c26fa984c413305012be54741c9d0d74abd7fbeeccae3889f/numpy-2.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:fcfe2045fd2e8f3cb0ce9d4ba6dba6333b8fa05bb8a4939c908cd43322d14c7e", size = 10486775, upload-time = "2026-03-29T13:18:45.835Z" },
+ { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" },
+ { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" },
+ { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" },
+ { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" },
+ { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" },
+ { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" },
+ { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" },
+ { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" },
+ { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" },
+ { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" },
+ { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" },
+ { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" },
+ { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" },
+ { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" },
+ { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" },
+ { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" },
+ { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" },
+ { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" },
+ { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" },
+ { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/33/8fae8f964a4f63ed528264ddf25d2b683d0b663e3cba26961eb838a7c1bd/numpy-2.4.4-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:58c8b5929fcb8287cbd6f0a3fae19c6e03a5c48402ae792962ac465224a629a4", size = 16854491, upload-time = "2026-03-29T13:21:38.03Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/d0/1aabee441380b981cf8cdda3ae7a46aa827d1b5a8cce84d14598bc94d6d9/numpy-2.4.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:eea7ac5d2dce4189771cedb559c738a71512768210dc4e4753b107a2048b3d0e", size = 14895830, upload-time = "2026-03-29T13:21:41.509Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/b8/aafb0d1065416894fccf4df6b49ef22b8db045187949545bced89c034b8e/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:51fc224f7ca4d92656d5a5eb315f12eb5fe2c97a66249aa7b5f562528a3be38c", size = 5400927, upload-time = "2026-03-29T13:21:44.747Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/77/063baa20b08b431038c7f9ff5435540c7b7265c78cf56012a483019ca72d/numpy-2.4.4-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:28a650663f7314afc3e6ec620f44f333c386aad9f6fc472030865dc0ebb26ee3", size = 6715557, upload-time = "2026-03-29T13:21:47.406Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/a8/379542d45a14f149444c5c4c4e7714707239ce9cc1de8c2803958889da14/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19710a9ca9992d7174e9c52f643d4272dcd1558c5f7af7f6f8190f633bd651a7", size = 15804253, upload-time = "2026-03-29T13:21:50.753Z" },
+ { url = "https://files.pythonhosted.org/packages/a2/c8/f0a45426d6d21e7ea3310a15cf90c43a14d9232c31a837702dba437f3373/numpy-2.4.4-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9b2aec6af35c113b05695ebb5749a787acd63cafc83086a05771d1e1cd1e555f", size = 16753552, upload-time = "2026-03-29T13:21:54.344Z" },
+ { url = "https://files.pythonhosted.org/packages/04/74/f4c001f4714c3ad9ce037e18cf2b9c64871a84951eaa0baf683a9ca9301c/numpy-2.4.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f2cf083b324a467e1ab358c105f6cad5ea950f50524668a80c486ff1db24e119", size = 12509075, upload-time = "2026-03-29T13:21:57.644Z" },
]
[[package]]
@@ -2296,11 +2312,11 @@ wheels = [
[[package]]
name = "python-multipart"
-version = "0.0.26"
+version = "0.0.27"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/88/71/b145a380824a960ebd60e1014256dbb7d2253f2316ff2d73dfd8928ec2c3/python_multipart-0.0.26.tar.gz", hash = "sha256:08fadc45918cd615e26846437f50c5d6d23304da32c341f289a617127b081f17", size = 43501, upload-time = "2026-04-10T14:09:59.473Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/69/9b/f23807317a113dc36e74e75eb265a02dd1a4d9082abc3c1064acd22997c4/python_multipart-0.0.27.tar.gz", hash = "sha256:9870a6a8c5a20a5bf4f07c017bd1489006ff8836cff097b6933355ee2b49b602", size = 44043, upload-time = "2026-04-27T10:51:26.649Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/9a/22/f1925cdda983ab66fc8ec6ec8014b959262747e58bdca26a4e3d1da29d56/python_multipart-0.0.26-py3-none-any.whl", hash = "sha256:c0b169f8c4484c13b0dcf2ef0ec3a4adb255c4b7d18d8e420477d2b1dd03f185", size = 28847, upload-time = "2026-04-10T14:09:58.131Z" },
+ { url = "https://files.pythonhosted.org/packages/99/78/4126abcbdbd3c559d43e0db7f7b9173fc6befe45d39a2856cc0b8ec2a5a6/python_multipart-0.0.27-py3-none-any.whl", hash = "sha256:6fccfad17a27334bd0193681b369f476eda3409f17381a2d65aa7df3f7275645", size = 29254, upload-time = "2026-04-27T10:51:24.997Z" },
]
[[package]]
@@ -2769,6 +2785,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ea/f1/5e9b3ba5c7aa7ebfaf269657e728067d16a7c99401c7973ddf5f0cf121bd/shapely-2.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:8cb8f17c377260452e9d7720eeaf59082c5f8ea48cf104524d953e5d36d4bdb7", size = 1723061, upload-time = "2025-05-19T11:04:40.082Z" },
]
+[[package]]
+name = "shellingham"
+version = "1.5.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" },
+]
+
[[package]]
name = "simple-websocket"
version = "1.1.0"
@@ -2932,6 +2957,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/ad/7d47bbf2cae78ff79f29db0bed5016ec9c56b212a93fca624bb88b551a7c/tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53", size = 78374, upload-time = "2024-05-02T21:44:01.541Z" },
]
+[[package]]
+name = "typer"
+version = "0.25.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "annotated-doc" },
+ { name = "click" },
+ { name = "rich" },
+ { name = "shellingham" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" },
+]
+
[[package]]
name = "types-pyyaml"
version = "6.0.12.20260408"
diff --git a/mise.toml b/mise.toml
index 367eb75da9..5aa7054c27 100644
--- a/mise.toml
+++ b/mise.toml
@@ -11,21 +11,31 @@ config_roots = [
"web",
"docs",
".github",
+ "machine-learning",
]
[tools]
node = "24.15.0"
-flutter = "3.41.6"
-pnpm = "10.33.0"
-terragrunt = "1.0.1"
+flutter = "3.41.9"
+pnpm = "10.33.1"
+terragrunt = "1.0.3"
opentofu = "1.11.6"
java = "21.0.2"
[tools."github:CQLabs/homebrew-dcm"]
-version = "1.35.1"
+version = "1.37.0"
bin = "dcm"
postinstall = "chmod +x $MISE_TOOL_INSTALL_PATH/dcm"
+[tools."github:jellyfin/jellyfin-ffmpeg"]
+version = "7.1.3-6"
+
+[tools."github:jellyfin/jellyfin-ffmpeg".platforms]
+linux-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linux64-gpl.tar.xz" }
+linux-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_linuxarm64-gpl.tar.xz" }
+macos-x64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_mac64-gpl.tar.xz" }
+macos-arm64 = { asset_pattern = "jellyfin-ffmpeg_*_portable_macarm64-gpl.tar.xz" }
+
[settings]
experimental = true
pin = true
diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json
index 051c18ce6a..517086e98a 100644
--- a/mobile/.vscode/settings.json
+++ b/mobile/.vscode/settings.json
@@ -1,5 +1,5 @@
{
- "dart.flutterSdkPath": ".fvm/versions/3.41.7",
+ "dart.flutterSdkPath": ".fvm/versions/3.41.9",
"dart.lineLength": 120,
"[dart]": {
"editor.rulers": [
diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle
index e879b54ae5..7e3d67fa81 100644
--- a/mobile/android/app/build.gradle
+++ b/mobile/android/app/build.gradle
@@ -64,8 +64,15 @@ android {
}
release {
- signingConfig signingConfigs.release
+ def hasKeystore = file("../key.jks").exists() && file("../key.jks").length() > 0
+ signingConfig hasKeystore ? signingConfigs.release : signingConfigs.debug
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+
+ def prNumber = System.getenv("PR_NUMBER")
+ if (prNumber) {
+ applicationIdSuffix ".pr${prNumber}"
+ versionNameSuffix "-pr${prNumber}"
+ }
}
}
namespace 'app.alextran.immich'
diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile
index 7312a8ca68..0f55eeec26 100644
--- a/mobile/android/fastlane/Fastfile
+++ b/mobile/android/fastlane/Fastfile
@@ -35,8 +35,8 @@ platform :android do
task: 'bundle',
build_type: 'Release',
properties: {
- "android.injected.version.code" => 3046,
- "android.injected.version.name" => "2.7.5",
+ "android.injected.version.code" => 3047,
+ "android.injected.version.name" => "3.0.0",
}
)
upload_to_play_store(skip_upload_apk: true, skip_upload_images: true, skip_upload_screenshots: true, aab: '../build/app/outputs/bundle/release/app-release.aab')
diff --git a/mobile/dcm_global.yaml b/mobile/dcm_global.yaml
index ffe77eede8..0518849062 100644
--- a/mobile/dcm_global.yaml
+++ b/mobile/dcm_global.yaml
@@ -1 +1 @@
-version: '>=1.29.0 <=1.36.0'
+version: '>=1.29.0 <=1.37.0'
diff --git a/mobile/drift_schemas/main/drift_schema_v25.json b/mobile/drift_schemas/main/drift_schema_v25.json
new file mode 100644
index 0000000000..95fa57d4d2
--- /dev/null
+++ b/mobile/drift_schemas/main/drift_schema_v25.json
@@ -0,0 +1,3358 @@
+{
+ "_meta": {
+ "description": "This file contains a serialized version of schema entities for drift.",
+ "version": "1.3.0"
+ },
+ "options": {
+ "store_date_time_values_as_text": true
+ },
+ "entities": [
+ {
+ "id": 0,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "email",
+ "getter_name": "email",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "has_profile_image",
+ "getter_name": "hasProfileImage",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "profile_changed_at",
+ "getter_name": "profileChangedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "avatar_color",
+ "getter_name": "avatarColor",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AvatarColor.values)",
+ "dart_type_name": "AvatarColor"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 1,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "local_date_time",
+ "getter_name": "localDateTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "thumb_hash",
+ "getter_name": "thumbHash",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "live_photo_video_id",
+ "getter_name": "livePhotoVideoId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "visibility",
+ "getter_name": "visibility",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetVisibility.values)",
+ "dart_type_name": "AssetVisibility"
+ }
+ },
+ {
+ "name": "stack_id",
+ "getter_name": "stackId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "library_id",
+ "getter_name": "libraryId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_edited",
+ "getter_name": "isEdited",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_edited\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_edited\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 2,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "stack_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "primary_asset_id",
+ "getter_name": "primaryAssetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 3,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "local_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "i_cloud_id",
+ "getter_name": "iCloudId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "adjustment_time",
+ "getter_name": "adjustmentTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "playback_style",
+ "getter_name": "playbackStyle",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)",
+ "dart_type_name": "AssetPlaybackStyle"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 4,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "description",
+ "getter_name": "description",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('\\'\\'')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "thumbnail_asset_id",
+ "getter_name": "thumbnailAssetId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "is_activity_enabled",
+ "getter_name": "isActivityEnabled",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_activity_enabled\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_activity_enabled\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('1')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "order",
+ "getter_name": "order",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AlbumAssetOrder.values)",
+ "dart_type_name": "AlbumAssetOrder"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 5,
+ "references": [
+ 4
+ ],
+ "type": "table",
+ "data": {
+ "name": "local_album_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "backup_selection",
+ "getter_name": "backupSelection",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(BackupSelection.values)",
+ "dart_type_name": "BackupSelection"
+ }
+ },
+ {
+ "name": "is_ios_shared_album",
+ "getter_name": "isIosSharedAlbum",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_ios_shared_album\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_ios_shared_album\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "linked_remote_album_id",
+ "getter_name": "linkedRemoteAlbumId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "marker",
+ "getter_name": "marker_",
+ "moor_type": "bool",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"marker\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"marker\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 6,
+ "references": [
+ 3,
+ 5
+ ],
+ "type": "table",
+ "data": {
+ "name": "local_album_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES local_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES local_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "local_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES local_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES local_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "local_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "marker",
+ "getter_name": "marker_",
+ "moor_type": "bool",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"marker\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"marker\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 7,
+ "references": [
+ 6
+ ],
+ "type": "index",
+ "data": {
+ "on": 6,
+ "name": "idx_local_album_asset_album_asset",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 8,
+ "references": [
+ 3
+ ],
+ "type": "index",
+ "data": {
+ "on": 3,
+ "name": "idx_local_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 9,
+ "references": [
+ 3
+ ],
+ "type": "index",
+ "data": {
+ "on": 3,
+ "name": "idx_local_asset_cloud_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 10,
+ "references": [
+ 2
+ ],
+ "type": "index",
+ "data": {
+ "on": 2,
+ "name": "idx_stack_primary_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 11,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_owner_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 12,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "UQ_remote_assets_owner_checksum",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum\nON remote_asset_entity (owner_id, checksum)\nWHERE (library_id IS NULL);\n",
+ "unique": true,
+ "columns": []
+ }
+ },
+ {
+ "id": 13,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "UQ_remote_assets_owner_library_checksum",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum\nON remote_asset_entity (owner_id, library_id, checksum)\nWHERE (library_id IS NOT NULL);\n",
+ "unique": true,
+ "columns": []
+ }
+ },
+ {
+ "id": 14,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 15,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_stack_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 16,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_local_date_time_day",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 17,
+ "references": [
+ 1
+ ],
+ "type": "index",
+ "data": {
+ "on": 1,
+ "name": "idx_remote_asset_local_date_time_month",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 18,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "auth_user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "email",
+ "getter_name": "email",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_admin",
+ "getter_name": "isAdmin",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_admin\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_admin\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "has_profile_image",
+ "getter_name": "hasProfileImage",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"has_profile_image\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"has_profile_image\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "profile_changed_at",
+ "getter_name": "profileChangedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "avatar_color",
+ "getter_name": "avatarColor",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AvatarColor.values)",
+ "dart_type_name": "AvatarColor"
+ }
+ },
+ {
+ "name": "quota_size_in_bytes",
+ "getter_name": "quotaSizeInBytes",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "quota_usage_in_bytes",
+ "getter_name": "quotaUsageInBytes",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "pin_code",
+ "getter_name": "pinCode",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 19,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "user_metadata_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "user_id",
+ "getter_name": "userId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "key",
+ "getter_name": "key",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(UserMetadataKey.values)",
+ "dart_type_name": "UserMetadataKey"
+ }
+ },
+ {
+ "name": "value",
+ "getter_name": "value",
+ "moor_type": "blob",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "userMetadataConverter",
+ "dart_type_name": "Map"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "user_id",
+ "key"
+ ]
+ }
+ },
+ {
+ "id": 20,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "partner_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "shared_by_id",
+ "getter_name": "sharedById",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "shared_with_id",
+ "getter_name": "sharedWithId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "in_timeline",
+ "getter_name": "inTimeline",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"in_timeline\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"in_timeline\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "shared_by_id",
+ "shared_with_id"
+ ]
+ }
+ },
+ {
+ "id": 21,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_exif_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "city",
+ "getter_name": "city",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "state",
+ "getter_name": "state",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "country",
+ "getter_name": "country",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "date_time_original",
+ "getter_name": "dateTimeOriginal",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "description",
+ "getter_name": "description",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "exposure_time",
+ "getter_name": "exposureTime",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "f_number",
+ "getter_name": "fNumber",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "file_size",
+ "getter_name": "fileSize",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "focal_length",
+ "getter_name": "focalLength",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "iso",
+ "getter_name": "iso",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "make",
+ "getter_name": "make",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "model",
+ "getter_name": "model",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "lens",
+ "getter_name": "lens",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "time_zone",
+ "getter_name": "timeZone",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "rating",
+ "getter_name": "rating",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "projection_type",
+ "getter_name": "projectionType",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id"
+ ]
+ }
+ },
+ {
+ "id": 22,
+ "references": [
+ 1,
+ 4
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 23,
+ "references": [
+ 4,
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_album_user_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_album_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_album_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_album_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "user_id",
+ "getter_name": "userId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "role",
+ "getter_name": "role",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AlbumUserRole.values)",
+ "dart_type_name": "AlbumUserRole"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "album_id",
+ "user_id"
+ ]
+ }
+ },
+ {
+ "id": 24,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "remote_asset_cloud_id_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "cloud_id",
+ "getter_name": "cloudId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "adjustment_time",
+ "getter_name": "adjustmentTime",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "latitude",
+ "getter_name": "latitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "longitude",
+ "getter_name": "longitude",
+ "moor_type": "double",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id"
+ ]
+ }
+ },
+ {
+ "id": 25,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "memory_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(MemoryTypeEnum.values)",
+ "dart_type_name": "MemoryTypeEnum"
+ }
+ },
+ {
+ "name": "data",
+ "getter_name": "data",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_saved",
+ "getter_name": "isSaved",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_saved\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_saved\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "memory_at",
+ "getter_name": "memoryAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "seen_at",
+ "getter_name": "seenAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "show_at",
+ "getter_name": "showAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "hide_at",
+ "getter_name": "hideAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 26,
+ "references": [
+ 1,
+ 25
+ ],
+ "type": "table",
+ "data": {
+ "name": "memory_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "memory_id",
+ "getter_name": "memoryId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES memory_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES memory_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "memory_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "asset_id",
+ "memory_id"
+ ]
+ }
+ },
+ {
+ "id": 27,
+ "references": [
+ 0
+ ],
+ "type": "table",
+ "data": {
+ "name": "person_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "owner_id",
+ "getter_name": "ownerId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES user_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES user_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "user_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "face_asset_id",
+ "getter_name": "faceAssetId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_hidden",
+ "getter_name": "isHidden",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_hidden\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_hidden\" IN (0, 1))"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "color",
+ "getter_name": "color",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "birth_date",
+ "getter_name": "birthDate",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 28,
+ "references": [
+ 1,
+ 27
+ ],
+ "type": "table",
+ "data": {
+ "name": "asset_face_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "person_id",
+ "getter_name": "personId",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES person_entity (id) ON DELETE SET NULL",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES person_entity (id) ON DELETE SET NULL"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "person_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "setNull"
+ }
+ }
+ ]
+ },
+ {
+ "name": "image_width",
+ "getter_name": "imageWidth",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "image_height",
+ "getter_name": "imageHeight",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_x1",
+ "getter_name": "boundingBoxX1",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_y1",
+ "getter_name": "boundingBoxY1",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_x2",
+ "getter_name": "boundingBoxX2",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "bounding_box_y2",
+ "getter_name": "boundingBoxY2",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "source_type",
+ "getter_name": "sourceType",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_visible",
+ "getter_name": "isVisible",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_visible\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_visible\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('1')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "deleted_at",
+ "getter_name": "deletedAt",
+ "moor_type": "dateTime",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 29,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "store_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "string_value",
+ "getter_name": "stringValue",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "int_value",
+ "getter_name": "intValue",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 30,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "trashed_local_asset_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "name",
+ "getter_name": "name",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "type",
+ "getter_name": "type",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetType.values)",
+ "dart_type_name": "AssetType"
+ }
+ },
+ {
+ "name": "created_at",
+ "getter_name": "createdAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "width",
+ "getter_name": "width",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "height",
+ "getter_name": "height",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "duration_ms",
+ "getter_name": "durationMs",
+ "moor_type": "int",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "album_id",
+ "getter_name": "albumId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "checksum",
+ "getter_name": "checksum",
+ "moor_type": "string",
+ "nullable": true,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "is_favorite",
+ "getter_name": "isFavorite",
+ "moor_type": "bool",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "CHECK (\"is_favorite\" IN (0, 1))",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "CHECK (\"is_favorite\" IN (0, 1))"
+ },
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "orientation",
+ "getter_name": "orientation",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "source",
+ "getter_name": "source",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(TrashOrigin.values)",
+ "dart_type_name": "TrashOrigin"
+ }
+ },
+ {
+ "name": "playback_style",
+ "getter_name": "playbackStyle",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('0')",
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetPlaybackStyle.values)",
+ "dart_type_name": "AssetPlaybackStyle"
+ }
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id",
+ "album_id"
+ ]
+ }
+ },
+ {
+ "id": 31,
+ "references": [
+ 1
+ ],
+ "type": "table",
+ "data": {
+ "name": "asset_edit_entity",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "id",
+ "getter_name": "id",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "asset_id",
+ "getter_name": "assetId",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "defaultConstraints": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE",
+ "dialectAwareDefaultConstraints": {
+ "sqlite": "REFERENCES remote_asset_entity (id) ON DELETE CASCADE"
+ },
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [
+ {
+ "foreign_key": {
+ "to": {
+ "table": "remote_asset_entity",
+ "column": "id"
+ },
+ "initially_deferred": false,
+ "on_update": null,
+ "on_delete": "cascade"
+ }
+ }
+ ]
+ },
+ {
+ "name": "action",
+ "getter_name": "action",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "const EnumIndexConverter(AssetEditAction.values)",
+ "dart_type_name": "AssetEditAction"
+ }
+ },
+ {
+ "name": "parameters",
+ "getter_name": "parameters",
+ "moor_type": "blob",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": [],
+ "type_converter": {
+ "dart_expr": "editParameterConverter",
+ "dart_type_name": "Map"
+ }
+ },
+ {
+ "name": "sequence",
+ "getter_name": "sequence",
+ "moor_type": "int",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "id"
+ ]
+ }
+ },
+ {
+ "id": 32,
+ "references": [],
+ "type": "table",
+ "data": {
+ "name": "metadata",
+ "was_declared_in_moor": false,
+ "columns": [
+ {
+ "name": "key",
+ "getter_name": "key",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "value",
+ "getter_name": "value",
+ "moor_type": "string",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": null,
+ "default_client_dart": null,
+ "dsl_features": []
+ },
+ {
+ "name": "updated_at",
+ "getter_name": "updatedAt",
+ "moor_type": "dateTime",
+ "nullable": false,
+ "customConstraints": null,
+ "default_dart": "const CustomExpression('CURRENT_TIMESTAMP')",
+ "default_client_dart": null,
+ "dsl_features": []
+ }
+ ],
+ "is_virtual": false,
+ "without_rowid": true,
+ "constraints": [],
+ "strict": true,
+ "explicit_pk": [
+ "key"
+ ]
+ }
+ },
+ {
+ "id": 33,
+ "references": [
+ 20
+ ],
+ "type": "index",
+ "data": {
+ "on": 20,
+ "name": "idx_partner_shared_with_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 34,
+ "references": [
+ 21
+ ],
+ "type": "index",
+ "data": {
+ "on": 21,
+ "name": "idx_lat_lng",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 35,
+ "references": [
+ 22
+ ],
+ "type": "index",
+ "data": {
+ "on": 22,
+ "name": "idx_remote_album_asset_album_asset",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 36,
+ "references": [
+ 24
+ ],
+ "type": "index",
+ "data": {
+ "on": 24,
+ "name": "idx_remote_asset_cloud_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 37,
+ "references": [
+ 27
+ ],
+ "type": "index",
+ "data": {
+ "on": 27,
+ "name": "idx_person_owner_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 38,
+ "references": [
+ 28
+ ],
+ "type": "index",
+ "data": {
+ "on": 28,
+ "name": "idx_asset_face_person_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 39,
+ "references": [
+ 28
+ ],
+ "type": "index",
+ "data": {
+ "on": 28,
+ "name": "idx_asset_face_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 40,
+ "references": [
+ 30
+ ],
+ "type": "index",
+ "data": {
+ "on": 30,
+ "name": "idx_trashed_local_asset_checksum",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 41,
+ "references": [
+ 30
+ ],
+ "type": "index",
+ "data": {
+ "on": 30,
+ "name": "idx_trashed_local_asset_album",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)",
+ "unique": false,
+ "columns": []
+ }
+ },
+ {
+ "id": 42,
+ "references": [
+ 31
+ ],
+ "type": "index",
+ "data": {
+ "on": 31,
+ "name": "idx_asset_edit_asset_id",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)",
+ "unique": false,
+ "columns": []
+ }
+ }
+ ],
+ "fixed_sql": [
+ {
+ "name": "user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NOT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"local_date_time\" TEXT NULL, \"thumb_hash\" TEXT NULL, \"deleted_at\" TEXT NULL, \"live_photo_video_id\" TEXT NULL, \"visibility\" INTEGER NOT NULL, \"stack_id\" TEXT NULL, \"library_id\" TEXT NULL, \"is_edited\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_edited\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "stack_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"stack_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"primary_asset_id\" TEXT NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"i_cloud_id\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"description\" TEXT NOT NULL DEFAULT '', \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"thumbnail_asset_id\" TEXT NULL REFERENCES remote_asset_entity (id) ON DELETE SET NULL, \"is_activity_enabled\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_activity_enabled\" IN (0, 1)), \"order\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_album_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_album_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"backup_selection\" INTEGER NOT NULL, \"is_ios_shared_album\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_ios_shared_album\" IN (0, 1)), \"linked_remote_album_id\" TEXT NULL REFERENCES remote_album_entity (id) ON DELETE SET NULL, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "local_album_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"local_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES local_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES local_album_entity (id) ON DELETE CASCADE, \"marker\" INTEGER NULL CHECK (\"marker\" IN (0, 1)), PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_album_asset_album_asset",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_local_asset_cloud_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_stack_primary_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_owner_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)"
+ }
+ ]
+ },
+ {
+ "name": "UQ_remote_assets_owner_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)"
+ }
+ ]
+ },
+ {
+ "name": "UQ_remote_assets_owner_library_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_stack_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_local_date_time_day",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME('%Y-%m-%d', local_date_time))"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_local_date_time_month",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME('%Y-%m', local_date_time))"
+ }
+ ]
+ },
+ {
+ "name": "auth_user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"auth_user_entity\" (\"id\" TEXT NOT NULL, \"name\" TEXT NOT NULL, \"email\" TEXT NOT NULL, \"is_admin\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_admin\" IN (0, 1)), \"has_profile_image\" INTEGER NOT NULL DEFAULT 0 CHECK (\"has_profile_image\" IN (0, 1)), \"profile_changed_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"avatar_color\" INTEGER NOT NULL, \"quota_size_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"quota_usage_in_bytes\" INTEGER NOT NULL DEFAULT 0, \"pin_code\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "user_metadata_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"user_metadata_entity\" (\"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"key\" INTEGER NOT NULL, \"value\" BLOB NOT NULL, PRIMARY KEY (\"user_id\", \"key\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "partner_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"partner_entity\" (\"shared_by_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"shared_with_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"in_timeline\" INTEGER NOT NULL DEFAULT 0 CHECK (\"in_timeline\" IN (0, 1)), PRIMARY KEY (\"shared_by_id\", \"shared_with_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_exif_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_exif_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"city\" TEXT NULL, \"state\" TEXT NULL, \"country\" TEXT NULL, \"date_time_original\" TEXT NULL, \"description\" TEXT NULL, \"height\" INTEGER NULL, \"width\" INTEGER NULL, \"exposure_time\" TEXT NULL, \"f_number\" REAL NULL, \"file_size\" INTEGER NULL, \"focal_length\" REAL NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, \"iso\" INTEGER NULL, \"make\" TEXT NULL, \"model\" TEXT NULL, \"lens\" TEXT NULL, \"orientation\" TEXT NULL, \"time_zone\" TEXT NULL, \"rating\" INTEGER NULL, \"projection_type\" TEXT NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_album_user_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_album_user_entity\" (\"album_id\" TEXT NOT NULL REFERENCES remote_album_entity (id) ON DELETE CASCADE, \"user_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"role\" INTEGER NOT NULL, PRIMARY KEY (\"album_id\", \"user_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "remote_asset_cloud_id_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"remote_asset_cloud_id_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"cloud_id\" TEXT NULL, \"created_at\" TEXT NULL, \"adjustment_time\" TEXT NULL, \"latitude\" REAL NULL, \"longitude\" REAL NULL, PRIMARY KEY (\"asset_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "memory_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"memory_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"deleted_at\" TEXT NULL, \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"type\" INTEGER NOT NULL, \"data\" TEXT NOT NULL, \"is_saved\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_saved\" IN (0, 1)), \"memory_at\" TEXT NOT NULL, \"seen_at\" TEXT NULL, \"show_at\" TEXT NULL, \"hide_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "memory_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"memory_asset_entity\" (\"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"memory_id\" TEXT NOT NULL REFERENCES memory_entity (id) ON DELETE CASCADE, PRIMARY KEY (\"asset_id\", \"memory_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "person_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"person_entity\" (\"id\" TEXT NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"owner_id\" TEXT NOT NULL REFERENCES user_entity (id) ON DELETE CASCADE, \"name\" TEXT NOT NULL, \"face_asset_id\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL CHECK (\"is_favorite\" IN (0, 1)), \"is_hidden\" INTEGER NOT NULL CHECK (\"is_hidden\" IN (0, 1)), \"color\" TEXT NULL, \"birth_date\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "asset_face_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"asset_face_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"person_id\" TEXT NULL REFERENCES person_entity (id) ON DELETE SET NULL, \"image_width\" INTEGER NOT NULL, \"image_height\" INTEGER NOT NULL, \"bounding_box_x1\" INTEGER NOT NULL, \"bounding_box_y1\" INTEGER NOT NULL, \"bounding_box_x2\" INTEGER NOT NULL, \"bounding_box_y2\" INTEGER NOT NULL, \"source_type\" TEXT NOT NULL, \"is_visible\" INTEGER NOT NULL DEFAULT 1 CHECK (\"is_visible\" IN (0, 1)), \"deleted_at\" TEXT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "store_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"store_entity\" (\"id\" INTEGER NOT NULL, \"string_value\" TEXT NULL, \"int_value\" INTEGER NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "trashed_local_asset_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"trashed_local_asset_entity\" (\"name\" TEXT NOT NULL, \"type\" INTEGER NOT NULL, \"created_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), \"width\" INTEGER NULL, \"height\" INTEGER NULL, \"duration_ms\" INTEGER NULL, \"id\" TEXT NOT NULL, \"album_id\" TEXT NOT NULL, \"checksum\" TEXT NULL, \"is_favorite\" INTEGER NOT NULL DEFAULT 0 CHECK (\"is_favorite\" IN (0, 1)), \"orientation\" INTEGER NOT NULL DEFAULT 0, \"source\" INTEGER NOT NULL, \"playback_style\" INTEGER NOT NULL DEFAULT 0, PRIMARY KEY (\"id\", \"album_id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "asset_edit_entity",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"asset_edit_entity\" (\"id\" TEXT NOT NULL, \"asset_id\" TEXT NOT NULL REFERENCES remote_asset_entity (id) ON DELETE CASCADE, \"action\" INTEGER NOT NULL, \"parameters\" BLOB NOT NULL, \"sequence\" INTEGER NOT NULL, PRIMARY KEY (\"id\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "metadata",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE TABLE IF NOT EXISTS \"metadata\" (\"key\" TEXT NOT NULL, \"value\" TEXT NOT NULL, \"updated_at\" TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP), PRIMARY KEY (\"key\")) WITHOUT ROWID, STRICT;"
+ }
+ ]
+ },
+ {
+ "name": "idx_partner_shared_with_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_lat_lng",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_album_asset_album_asset",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_remote_asset_cloud_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_person_owner_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_face_person_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_face_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_trashed_local_asset_checksum",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)"
+ }
+ ]
+ },
+ {
+ "name": "idx_trashed_local_asset_album",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)"
+ }
+ ]
+ },
+ {
+ "name": "idx_asset_edit_asset_id",
+ "sql": [
+ {
+ "dialect": "sqlite",
+ "sql": "CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)"
+ }
+ ]
+ }
+ ]
+}
\ No newline at end of file
diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist
index 3b030e4f86..0ca810438e 100644
--- a/mobile/ios/Runner/Info.plist
+++ b/mobile/ios/Runner/Info.plist
@@ -78,7 +78,7 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 2.7.5
+ 3.0.0
CFBundleSignature
????
CFBundleURLTypes
diff --git a/mobile/ios/fastlane/Appfile b/mobile/ios/fastlane/Appfile
index e233ba2dcc..77318e3603 100644
--- a/mobile/ios/fastlane/Appfile
+++ b/mobile/ios/fastlane/Appfile
@@ -1,5 +1,5 @@
app_identifier "app.alextran.immich" # The bundle identifier of your app
-apple_id "alex.tran1502@gmail.com" # Your Apple email address
+apple_id "altran@futo.org" # Your Apple email address
# For more information about the Appfile, see:
diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile
index 9c31ced00d..ff9fc4580f 100644
--- a/mobile/ios/fastlane/Fastfile
+++ b/mobile/ios/fastlane/Fastfile
@@ -17,10 +17,11 @@ default_platform(:ios)
platform :ios do
# Constants
- TEAM_ID = "2F67MQ8R79"
- CODE_SIGN_IDENTITY = "Apple Distribution: Hau Tran (#{TEAM_ID})"
+ TEAM_ID = "2W7AC6T8T5"
+ CODE_SIGN_IDENTITY = "Apple Distribution: FUTO Holdings, Inc. (#{TEAM_ID})"
BASE_BUNDLE_ID = "app.alextran.immich"
-
+ DEV_BUNDLE_ID = "tech.futo.immich.testflight"
+
# Helper method to get App Store Connect API key
def get_api_key
app_store_connect_api_key(
@@ -44,47 +45,45 @@ def get_version_from_pubspec
end
# Helper method to configure code signing for all targets
- def configure_code_signing(bundle_id_suffix: "", profile_name_main:, profile_name_share:, profile_name_widget:)
- bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
-
+ def configure_code_signing(base_bundle_id:, profile_name_main:, profile_name_share:, profile_name_widget:)
# Runner (main app)
update_code_signing_settings(
use_automatic_signing: false,
path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY,
- bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}",
+ bundle_identifier: base_bundle_id,
profile_name: profile_name_main,
targets: ["Runner"]
)
-
+
# ShareExtension
update_code_signing_settings(
use_automatic_signing: false,
path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY,
- bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.ShareExtension",
+ bundle_identifier: "#{base_bundle_id}.ShareExtension",
profile_name: profile_name_share,
targets: ["ShareExtension"]
)
-
+
# WidgetExtension
update_code_signing_settings(
use_automatic_signing: false,
path: "./Runner.xcodeproj",
team_id: ENV["FASTLANE_TEAM_ID"] || TEAM_ID,
code_sign_identity: CODE_SIGN_IDENTITY,
- bundle_identifier: "#{BASE_BUNDLE_ID}#{bundle_suffix}.Widget",
+ bundle_identifier: "#{base_bundle_id}.Widget",
profile_name: profile_name_widget,
targets: ["WidgetExtension"]
)
end
-
+
# Helper method to build and upload to TestFlight
def build_and_upload(
api_key:,
- bundle_id_suffix: "",
+ base_bundle_id:,
configuration: "Release",
distribute_external: true,
version_number: nil,
@@ -92,9 +91,8 @@ end
profile_name_share:,
profile_name_widget:
)
- bundle_suffix = bundle_id_suffix.empty? ? "" : ".#{bundle_id_suffix}"
- app_identifier = "#{BASE_BUNDLE_ID}#{bundle_suffix}"
-
+ app_identifier = base_bundle_id
+
# Set version number if provided
if version_number
increment_version_number(version_number: version_number)
@@ -138,31 +136,31 @@ end
desc "iOS Development Build to TestFlight (requires separate bundle ID)"
lane :gha_testflight_dev do
api_key = get_api_key
-
+
# Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain
# Capture profile names after each sigh call
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
+ sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME]
-
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
+
+ sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME]
-
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
+
+ sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
-
+
# Configure code signing for dev bundle IDs using the downloaded profile names
configure_code_signing(
- bundle_id_suffix: "development",
+ base_bundle_id: DEV_BUNDLE_ID,
profile_name_main: main_profile_name,
profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name
)
-
+
# Build and upload
build_and_upload(
api_key: api_key,
- bundle_id_suffix: "development",
+ base_bundle_id: DEV_BUNDLE_ID,
configuration: "Profile",
distribute_external: false,
profile_name_main: main_profile_name,
@@ -189,6 +187,7 @@ end
# Configure code signing for production bundle IDs
configure_code_signing(
+ base_bundle_id: BASE_BUNDLE_ID,
profile_name_main: main_profile_name,
profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name
@@ -197,6 +196,7 @@ end
# Build and upload with version number
build_and_upload(
api_key: api_key,
+ base_bundle_id: BASE_BUNDLE_ID,
version_number: get_version_from_pubspec,
distribute_external: false,
profile_name_main: main_profile_name,
@@ -243,30 +243,30 @@ end
desc "iOS Build Only (no TestFlight upload)"
lane :gha_build_only do
- # Use the same build process as production, just skip the upload
- # This ensures PR builds validate the same way as production builds
-
+ # Use the same build process as the dev TestFlight lane, just skip the upload
+ # This ensures PR builds validate the same way as dev TestFlight builds
+
api_key = get_api_key
-
+
# Download and install provisioning profiles from App Store Connect
# Certificate is imported by GHA workflow into build.keychain
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development", force: true)
+ sigh(api_key: api_key, app_identifier: DEV_BUNDLE_ID, force: true)
main_profile_name = lane_context[SharedValues::SIGH_NAME]
-
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.ShareExtension", force: true)
+
+ sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.ShareExtension", force: true)
share_profile_name = lane_context[SharedValues::SIGH_NAME]
-
- sigh(api_key: api_key, app_identifier: "#{BASE_BUNDLE_ID}.development.Widget", force: true)
+
+ sigh(api_key: api_key, app_identifier: "#{DEV_BUNDLE_ID}.Widget", force: true)
widget_profile_name = lane_context[SharedValues::SIGH_NAME]
-
+
# Configure code signing for dev bundle IDs
configure_code_signing(
- bundle_id_suffix: "development",
+ base_bundle_id: DEV_BUNDLE_ID,
profile_name_main: main_profile_name,
profile_name_share: share_profile_name,
profile_name_widget: widget_profile_name
)
-
+
# Build the app (same as gha_testflight_dev but without upload)
build_app(
scheme: "Runner",
@@ -277,9 +277,9 @@ end
xcargs: "-skipMacroValidation CODE_SIGN_IDENTITY='#{CODE_SIGN_IDENTITY}' CODE_SIGN_STYLE=Manual",
export_options: {
provisioningProfiles: {
- "#{BASE_BUNDLE_ID}.development" => main_profile_name,
- "#{BASE_BUNDLE_ID}.development.ShareExtension" => share_profile_name,
- "#{BASE_BUNDLE_ID}.development.Widget" => widget_profile_name
+ DEV_BUNDLE_ID => main_profile_name,
+ "#{DEV_BUNDLE_ID}.ShareExtension" => share_profile_name,
+ "#{DEV_BUNDLE_ID}.Widget" => widget_profile_name
},
signingStyle: "manual",
signingCertificate: CODE_SIGN_IDENTITY
diff --git a/mobile/lib/constants/colors.dart b/mobile/lib/constants/colors.dart
index e39480de32..655d2d9c09 100644
--- a/mobile/lib/constants/colors.dart
+++ b/mobile/lib/constants/colors.dart
@@ -2,9 +2,6 @@ import 'package:flutter/material.dart';
enum ImmichColorPreset { indigo, deepPurple, pink, red, orange, yellow, lime, green, cyan, slateGray }
-const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo;
-const String defaultColorPresetName = "indigo";
-
const Color immichBrandColorLight = Color(0xFF4150AF);
const Color immichBrandColorDark = Color(0xFFACCBFA);
const Color whiteOpacity75 = Color.fromRGBO(255, 255, 255, 0.75);
diff --git a/mobile/lib/domain/models/config/app_config.dart b/mobile/lib/domain/models/config/app_config.dart
new file mode 100644
index 0000000000..7179137af4
--- /dev/null
+++ b/mobile/lib/domain/models/config/app_config.dart
@@ -0,0 +1,22 @@
+import 'package:immich_mobile/domain/models/config/cleanup_config.dart';
+import 'package:immich_mobile/domain/models/config/theme_config.dart';
+
+class AppConfig {
+ final ThemeConfig theme;
+ final CleanupConfig cleanup;
+
+ const AppConfig({this.theme = const .new(), this.cleanup = const .new()});
+
+ AppConfig copyWith({ThemeConfig? theme, CleanupConfig? cleanup}) =>
+ .new(theme: theme ?? this.theme, cleanup: cleanup ?? this.cleanup);
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) || (other is AppConfig && other.theme == theme && other.cleanup == cleanup);
+
+ @override
+ int get hashCode => Object.hash(theme, cleanup);
+
+ @override
+ String toString() => 'AppConfig(theme: $theme, cleanup: $cleanup)';
+}
diff --git a/mobile/lib/domain/models/config/cleanup_config.dart b/mobile/lib/domain/models/config/cleanup_config.dart
new file mode 100644
index 0000000000..4b34814492
--- /dev/null
+++ b/mobile/lib/domain/models/config/cleanup_config.dart
@@ -0,0 +1,48 @@
+import 'package:immich_mobile/constants/enums.dart';
+
+class CleanupConfig {
+ final bool keepFavorites;
+ final AssetKeepType keepMediaType;
+ final List keepAlbumIds;
+ final int cutoffDaysAgo;
+ final bool defaultsInitialized;
+
+ const CleanupConfig({
+ this.keepFavorites = true,
+ this.keepMediaType = AssetKeepType.none,
+ this.keepAlbumIds = const [],
+ this.cutoffDaysAgo = -1,
+ this.defaultsInitialized = false,
+ });
+
+ CleanupConfig copyWith({
+ bool? keepFavorites,
+ AssetKeepType? keepMediaType,
+ List? keepAlbumIds,
+ int? cutoffDaysAgo,
+ bool? defaultsInitialized,
+ }) => .new(
+ keepFavorites: keepFavorites ?? this.keepFavorites,
+ keepMediaType: keepMediaType ?? this.keepMediaType,
+ keepAlbumIds: keepAlbumIds ?? this.keepAlbumIds,
+ cutoffDaysAgo: cutoffDaysAgo ?? this.cutoffDaysAgo,
+ defaultsInitialized: defaultsInitialized ?? this.defaultsInitialized,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is CleanupConfig &&
+ other.keepFavorites == keepFavorites &&
+ other.keepMediaType == keepMediaType &&
+ other.keepAlbumIds == keepAlbumIds &&
+ other.cutoffDaysAgo == cutoffDaysAgo &&
+ other.defaultsInitialized == defaultsInitialized);
+
+ @override
+ int get hashCode => Object.hash(keepFavorites, keepMediaType, keepAlbumIds, cutoffDaysAgo, defaultsInitialized);
+
+ @override
+ String toString() =>
+ 'CleanupConfig(keepFavorites: $keepFavorites, keepMediaType: $keepMediaType, keepAlbumIds: $keepAlbumIds, cutoffDaysAgo: $cutoffDaysAgo, defaultsInitialized: $defaultsInitialized)';
+}
diff --git a/mobile/lib/domain/models/config/system_config.dart b/mobile/lib/domain/models/config/system_config.dart
new file mode 100644
index 0000000000..cbad77695d
--- /dev/null
+++ b/mobile/lib/domain/models/config/system_config.dart
@@ -0,0 +1,18 @@
+import 'package:immich_mobile/domain/models/log.model.dart';
+
+class SystemConfig {
+ final LogLevel logLevel;
+
+ const SystemConfig({this.logLevel = .info});
+
+ SystemConfig copyWith({LogLevel? logLevel}) => SystemConfig(logLevel: logLevel ?? this.logLevel);
+
+ @override
+ bool operator ==(Object other) => identical(this, other) || (other is SystemConfig && other.logLevel == logLevel);
+
+ @override
+ int get hashCode => logLevel.hashCode;
+
+ @override
+ String toString() => 'SystemConfig(logLevel: $logLevel)';
+}
diff --git a/mobile/lib/domain/models/config/theme_config.dart b/mobile/lib/domain/models/config/theme_config.dart
new file mode 100644
index 0000000000..fa955c5d46
--- /dev/null
+++ b/mobile/lib/domain/models/config/theme_config.dart
@@ -0,0 +1,44 @@
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/constants/colors.dart';
+
+class ThemeConfig {
+ final ThemeMode mode;
+ final ImmichColorPreset primaryColor;
+ final bool dynamicTheme;
+ final bool colorfulInterface;
+
+ const ThemeConfig({
+ this.mode = .system,
+ this.primaryColor = .indigo,
+ this.dynamicTheme = false,
+ this.colorfulInterface = true,
+ });
+
+ ThemeConfig copyWith({
+ ThemeMode? mode,
+ ImmichColorPreset? primaryColor,
+ bool? dynamicTheme,
+ bool? colorfulInterface,
+ }) => .new(
+ mode: mode ?? this.mode,
+ primaryColor: primaryColor ?? this.primaryColor,
+ dynamicTheme: dynamicTheme ?? this.dynamicTheme,
+ colorfulInterface: colorfulInterface ?? this.colorfulInterface,
+ );
+
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is ThemeConfig &&
+ other.mode == mode &&
+ other.primaryColor == primaryColor &&
+ other.dynamicTheme == dynamicTheme &&
+ other.colorfulInterface == colorfulInterface);
+
+ @override
+ int get hashCode => Object.hash(mode, primaryColor, dynamicTheme, colorfulInterface);
+
+ @override
+ String toString() =>
+ 'ThemeConfig(mode: $mode, primaryColor: $primaryColor, dynamicTheme: $dynamicTheme, colorfulInterface: $colorfulInterface)';
+}
diff --git a/mobile/lib/domain/models/metadata_key.dart b/mobile/lib/domain/models/metadata_key.dart
new file mode 100644
index 0000000000..667822663a
--- /dev/null
+++ b/mobile/lib/domain/models/metadata_key.dart
@@ -0,0 +1,150 @@
+import 'dart:convert';
+
+import 'package:collection/collection.dart';
+import 'package:flutter/material.dart';
+import 'package:immich_mobile/constants/colors.dart';
+import 'package:immich_mobile/constants/enums.dart';
+import 'package:immich_mobile/domain/models/config/app_config.dart';
+import 'package:immich_mobile/domain/models/config/system_config.dart';
+import 'package:immich_mobile/domain/models/log.model.dart';
+
+enum MetadataDomain {
+ appConfig('config.app'),
+ systemConfig('config.system');
+
+ final String prefix;
+ const MetadataDomain(this.prefix);
+}
+
+enum MetadataKey {
+ // Theme
+ themePrimaryColor(.appConfig, 'theme.primaryColor', .indigo, _EnumCodec(ImmichColorPreset.values)),
+ themeMode(.appConfig, 'theme.mode', .system, _EnumCodec(ThemeMode.values)),
+ themeDynamic(.appConfig, 'theme.dynamic', false),
+ themeColorfulInterface(.appConfig, 'theme.colorfulInterface', true),
+
+ // Log
+ logLevel(.systemConfig, 'log.level', .info, _EnumCodec(LogLevel.values)),
+
+ // Cleanup
+ cleanupKeepFavorites(.appConfig, 'cleanup.keepFavorites', true),
+ cleanupKeepMediaType(
+ .appConfig,
+ 'cleanup.keepMediaType',
+ AssetKeepType.none,
+ _EnumCodec(AssetKeepType.values),
+ ),
+ cleanupKeepAlbumIds>(.appConfig, 'cleanup.keepAlbumIds', [], _ListCodec(_PrimitiveCodec.string)),
+ cleanupCutoffDaysAgo(.appConfig, 'cleanup.cutoffDaysAgo', -1),
+ cleanupDefaultsInitialized(.appConfig, 'cleanup.defaultsInitialized', false);
+
+ final MetadataDomain domain;
+ final String name;
+ final T defaultValue;
+ final _MetadataCodec? _codecOverride;
+
+ const MetadataKey(this.domain, this.name, this.defaultValue, [this._codecOverride]);
+
+ String get key => '${domain.prefix}.$name';
+
+ _MetadataCodec get _codec => _codecOverride ?? _MetadataCodec.forPrimitive(defaultValue);
+
+ String encode(T value) => _codec.encode(value);
+
+ T decode(String raw) => _codec.decode(raw) ?? defaultValue;
+
+ static Map> asKeyMap() => {for (var value in MetadataKey.values) value.key: value};
+}
+
+sealed class _MetadataCodec {
+ const _MetadataCodec();
+
+ String encode(T value);
+ T? decode(String raw);
+
+ static const Map> _primitives = {
+ int: _PrimitiveCodec.integer,
+ double: _PrimitiveCodec.real,
+ bool: _PrimitiveCodec.boolean,
+ String: _PrimitiveCodec.string,
+ DateTime: _DateTimeCodec(),
+ };
+
+ static _MetadataCodec forPrimitive(T sample) {
+ final codec = _primitives[sample.runtimeType];
+ if (codec == null) {
+ throw StateError(
+ 'No primitive codec for ${sample.runtimeType}. Provide an explicit codec when defining the MetadataKey.',
+ );
+ }
+ return codec as _MetadataCodec;
+ }
+}
+
+final class _EnumCodec extends _MetadataCodec {
+ final List values;
+
+ const _EnumCodec(this.values);
+
+ @override
+ String encode(T value) => value.name;
+
+ @override
+ T? decode(String raw) => values.firstWhereOrNull((v) => v.name == raw);
+}
+
+final class _DateTimeCodec extends _MetadataCodec {
+ const _DateTimeCodec();
+
+ @override
+ String encode(DateTime value) => value.toIso8601String();
+
+ @override
+ DateTime? decode(String raw) => DateTime.tryParse(raw);
+}
+
+final class _ListCodec extends _MetadataCodec> {
+ final _MetadataCodec _elementCodec;
+
+ const _ListCodec(this._elementCodec);
+
+ @override
+ String encode(List value) => jsonEncode(value.map(_elementCodec.encode).toList());
+
+ @override
+ List? decode(String raw) {
+ try {
+ final decoded = jsonDecode(raw);
+ if (decoded is! List) return null;
+ final result = [];
+ for (final item in decoded) {
+ if (item is! String) return null;
+ final element = _elementCodec.decode(item);
+ if (element == null) return null;
+ result.add(element);
+ }
+ return result;
+ } on FormatException {
+ return null;
+ }
+ }
+}
+
+final class _PrimitiveCodec extends _MetadataCodec {
+ final T? Function(String) _parse;
+
+ const _PrimitiveCodec._(this._parse);
+
+ @override
+ String encode(T value) => value.toString();
+
+ @override
+ T? decode(String raw) => _parse(raw);
+
+ static const integer = _PrimitiveCodec._(int.tryParse);
+ static const real = _PrimitiveCodec._(double.tryParse);
+ static const boolean = _PrimitiveCodec._(bool.tryParse);
+ static const string = _PrimitiveCodec._(_identity);
+
+ static String? _identity(String s) => s;
+}
diff --git a/mobile/lib/domain/models/store.model.dart b/mobile/lib/domain/models/store.model.dart
index 00545aa01a..8a7cd3a71e 100644
--- a/mobile/lib/domain/models/store.model.dart
+++ b/mobile/lib/domain/models/store.model.dart
@@ -22,7 +22,6 @@ enum StoreKey {
// user settings from [AppSettingsEnum] below:
loadPreview._(100),
loadOriginal._(101),
- themeMode._(102),
tilesPerRow._(103),
dynamicLayout._(104),
groupAssetsBy._(105),
@@ -35,7 +34,6 @@ enum StoreKey {
albumThumbnailCacheSize._(112),
selectedAlbumSortOrder._(113),
advancedTroubleshooting._(114),
- logLevel._(115),
preferRemoteImage._(116),
loopVideo._(117),
// map related settings
@@ -50,11 +48,6 @@ enum StoreKey {
enableHapticFeedback._(126),
customHeaders._(127),
- // theme settings
- primaryColor._(128),
- dynamicTheme._(129),
- colorfulInterface._(130),
-
syncAlbums._(131),
// Auto endpoint switching
@@ -88,13 +81,19 @@ enum StoreKey {
shouldResetSync._(1007),
// Free up space
- cleanupKeepFavorites._(1008),
- cleanupKeepMediaType._(1009),
- cleanupKeepAlbumIds._(1010),
- cleanupCutoffDaysAgo._(1011),
- cleanupDefaultsInitialized._(1012),
+ syncMigrationStatus._(1013),
- syncMigrationStatus._(1013);
+ // Legacy keys that have been migrated to the new metadata store
+ legacyPrimaryColor._(128),
+ legacyDynamicTheme._(129),
+ legacyColorfulInterface._(130),
+ legacyThemeMode._(102),
+ legacyCleanupKeepFavorites._(1008),
+ legacyCleanupKeepMediaType._(1009),
+ legacyCleanupKeepAlbumIds._(1010),
+ legacyCleanupCutoffDaysAgo._(1011),
+ legacyCleanupDefaultsInitialized._(1012),
+ legacyLogLevel._(115);
const StoreKey._(this.id);
final int id;
diff --git a/mobile/lib/domain/services/background_worker.service.dart b/mobile/lib/domain/services/background_worker.service.dart
index d4da3e31a4..a2e96f2313 100644
--- a/mobile/lib/domain/services/background_worker.service.dart
+++ b/mobile/lib/domain/services/background_worker.service.dart
@@ -176,15 +176,8 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
final backgroundSyncManager = _ref?.read(backgroundSyncProvider);
final nativeSyncApi = _ref?.read(nativeSyncApiProvider);
- await _drift.close();
- await _driftLogger.close();
-
- _ref?.dispose();
- _ref = null;
-
- _cancellationToken.complete();
_logger.info("Cleaning up background worker");
-
+ _cancellationToken.complete();
final cleanupFutures = [
nativeSyncApi?.cancelHashing(),
workerManagerPatch.dispose().catchError((_) async {
@@ -195,10 +188,15 @@ class BackgroundWorkerBgService extends BackgroundWorkerFlutterApi {
Store.dispose(),
backgroundSyncManager?.cancel(),
+ _drift.optimize(allTables: true),
];
await Future.wait(cleanupFutures.nonNulls);
- _logger.info("Background worker resources cleaned up");
+ await _drift.close();
+ await _driftLogger.close();
+
+ _ref?.dispose();
+ _ref = null;
} catch (error, stack) {
dPrint(() => 'Failed to cleanup background worker: $error with stack: $stack');
}
diff --git a/mobile/lib/domain/services/log.service.dart b/mobile/lib/domain/services/log.service.dart
index b58ee89535..1235d7ac76 100644
--- a/mobile/lib/domain/services/log.service.dart
+++ b/mobile/lib/domain/services/log.service.dart
@@ -2,20 +2,20 @@ import 'dart:async';
import 'package:immich_mobile/constants/constants.dart';
import 'package:immich_mobile/domain/models/log.model.dart';
-import 'package:immich_mobile/domain/models/store.model.dart';
+import 'package:immich_mobile/domain/models/metadata_key.dart';
import 'package:immich_mobile/infrastructure/repositories/log.repository.dart';
-import 'package:immich_mobile/infrastructure/repositories/store.repository.dart';
+import 'package:immich_mobile/infrastructure/repositories/metadata.repository.dart';
import 'package:immich_mobile/utils/debug_print.dart';
import 'package:logging/logging.dart';
/// Service responsible for handling application logging.
///
/// It listens to Dart's [Logger.root], buffers logs in memory (optionally),
-/// writes them to a persistent [ILogRepository], and manages log levels
-/// via [IStoreRepository]
+/// writes them to a persistent [LogRepository], and manages log levels via
+/// [MetadataRepository].
class LogService {
final LogRepository _logRepository;
- final DriftStoreRepository _storeRepository;
+ final MetadataRepository _metadataRepository;
final List _msgBuffer = [];
@@ -38,12 +38,12 @@ class LogService {
static Future init({
required LogRepository logRepository,
- required DriftStoreRepository storeRepository,
+ required MetadataRepository metadataRepository,
bool shouldBuffer = true,
}) async {
_instance ??= await create(
logRepository: logRepository,
- storeRepository: storeRepository,
+ metadataRepository: metadataRepository,
shouldBuffer: shouldBuffer,
);
return _instance!;
@@ -51,17 +51,17 @@ class LogService {
static Future create({
required LogRepository logRepository,
- required DriftStoreRepository storeRepository,
+ required MetadataRepository metadataRepository,
bool shouldBuffer = true,
}) async {
- final instance = LogService._(logRepository, storeRepository, shouldBuffer);
+ final instance = LogService._(logRepository, metadataRepository, shouldBuffer);
await logRepository.truncate(limit: kLogTruncateLimit);
- final level = await instance._storeRepository.tryGet(StoreKey.logLevel) ?? LogLevel.info.index;
- Logger.root.level = Level.LEVELS.elementAtOrNull(level) ?? Level.INFO;
+ final level = instance._metadataRepository.systemConfig.logLevel;
+ Logger.root.level = Level.LEVELS.elementAtOrNull(level.index) ?? Level.INFO;
return instance;
}
- LogService._(this._logRepository, this._storeRepository, this._shouldBuffer) {
+ LogService._(this._logRepository, this._metadataRepository, this._shouldBuffer) {
_logSubscription = Logger.root.onRecord.listen(_handleLogRecord);
}
@@ -91,7 +91,7 @@ class LogService {
}
Future setLogLevel(LogLevel level) async {
- await _storeRepository.upsert(StoreKey.logLevel, level.index);
+ await _metadataRepository.write(MetadataKey.logLevel, level);
Logger.root.level = level.toLevel();
}
diff --git a/mobile/lib/domain/services/sync_stream.service.dart b/mobile/lib/domain/services/sync_stream.service.dart
index b98ba24407..906e352b49 100644
--- a/mobile/lib/domain/services/sync_stream.service.dart
+++ b/mobile/lib/domain/services/sync_stream.service.dart
@@ -3,6 +3,7 @@
import 'dart:async';
import 'dart:convert';
+import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/store.model.dart';
import 'package:immich_mobile/domain/models/sync_event.model.dart';
import 'package:immich_mobile/entities/store.entity.dart';
@@ -192,17 +193,22 @@ class SyncStreamService {
final remoteSyncAssets = data.cast();
await _syncStreamRepository.updateAssetsV1(remoteSyncAssets);
if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
- final hasPermission = await _localFilesManager.hasManageMediaPermission();
- if (hasPermission) {
- await _handleRemoteTrashed(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.checksum));
- await _applyRemoteRestoreToLocal();
- } else {
- _logger.warning("sync Trashed Assets cannot proceed because MANAGE_MEDIA permission is missing");
- }
+ await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
+ }
+ return;
+ case SyncEntityType.assetV2:
+ final remoteSyncAssets = data.cast();
+ await _syncStreamRepository.updateAssetsV2(remoteSyncAssets);
+ if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
+ await _syncAssetTrashStatus(remoteSyncAssets.where((e) => e.deletedAt != null).map((e) => e.id).toList());
}
return;
case SyncEntityType.assetDeleteV1:
- return _syncStreamRepository.deleteAssetsV1(data.cast());
+ final remoteSyncAssets = data.cast();
+ if (CurrentPlatform.isAndroid && Store.get(StoreKey.manageLocalMediaAndroid, false)) {
+ await _syncAssetDeletion(remoteSyncAssets.map((e) => e.assetId).toList());
+ }
+ return _syncStreamRepository.deleteAssetsV1(remoteSyncAssets);
case SyncEntityType.assetExifV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast());
case SyncEntityType.assetEditV1:
@@ -215,8 +221,12 @@ class SyncStreamService {
return _syncStreamRepository.deleteAssetsMetadataV1(data.cast());
case SyncEntityType.partnerAssetV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner');
+ case SyncEntityType.partnerAssetV2:
+ return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner');
case SyncEntityType.partnerAssetBackfillV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'partner backfill');
+ case SyncEntityType.partnerAssetBackfillV2:
+ return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'partner backfill');
case SyncEntityType.partnerAssetDeleteV1:
return _syncStreamRepository.deleteAssetsV1(data.cast(), debugLabel: "partner");
case SyncEntityType.partnerAssetExifV1:
@@ -237,10 +247,16 @@ class SyncStreamService {
return _syncStreamRepository.deleteAlbumUsersV1(data.cast());
case SyncEntityType.albumAssetCreateV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset create');
+ case SyncEntityType.albumAssetCreateV2:
+ return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset create');
case SyncEntityType.albumAssetUpdateV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset update');
+ case SyncEntityType.albumAssetUpdateV2:
+ return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset update');
case SyncEntityType.albumAssetBackfillV1:
return _syncStreamRepository.updateAssetsV1(data.cast(), debugLabel: 'album asset backfill');
+ case SyncEntityType.albumAssetBackfillV2:
+ return _syncStreamRepository.updateAssetsV2(data.cast(), debugLabel: 'album asset backfill');
case SyncEntityType.albumAssetExifCreateV1:
return _syncStreamRepository.updateAssetsExifV1(data.cast(), debugLabel: 'album asset exif create');
case SyncEntityType.albumAssetExifUpdateV1:
@@ -342,6 +358,47 @@ class SyncStreamService {
}
}
+ Future handleWsAssetUploadReadyV2Batch(List batchData) async {
+ if (batchData.isEmpty) return;
+
+ _logger.info('Processing batch of ${batchData.length} AssetUploadReadyV2 events');
+
+ final List assets = [];
+ final List exifs = [];
+
+ try {
+ for (final data in batchData) {
+ if (data is! Map) {
+ continue;
+ }
+
+ final payload = data;
+ final assetData = payload['asset'];
+ final exifData = payload['exif'];
+
+ if (assetData == null || exifData == null) {
+ continue;
+ }
+
+ final asset = SyncAssetV2.fromJson(assetData);
+ final exif = SyncAssetExifV1.fromJson(exifData);
+
+ if (asset != null && exif != null) {
+ assets.add(asset);
+ exifs.add(exif);
+ }
+ }
+
+ if (assets.isNotEmpty && exifs.isNotEmpty) {
+ await _syncStreamRepository.updateAssetsV2(assets, debugLabel: 'websocket-batch');
+ await _syncStreamRepository.updateAssetsExifV1(exifs, debugLabel: 'websocket-batch');
+ _logger.info('Successfully processed ${assets.length} assets in batch');
+ }
+ } catch (error, stackTrace) {
+ _logger.severe("Error processing AssetUploadReadyV2 websocket batch events", error, stackTrace);
+ }
+ }
+
Future handleWsAssetEditReadyV1(dynamic data) async {
_logger.info('Processing AssetEditReadyV1 event');
@@ -382,28 +439,67 @@ class SyncStreamService {
}
}
- Future _handleRemoteTrashed(Iterable checksums) async {
- if (checksums.isEmpty) {
+ Future handleWsAssetEditReadyV2(dynamic data) async {
+ _logger.info('Processing AssetEditReadyV2 event');
+
+ try {
+ if (data is! Map) {
+ throw ArgumentError("Invalid data format for AssetEditReadyV2 event");
+ }
+
+ final payload = data;
+
+ if (payload['asset'] == null) {
+ throw ArgumentError("Missing 'asset' field in AssetEditReadyV2 event data");
+ }
+
+ final asset = SyncAssetV2.fromJson(payload['asset']);
+ if (asset == null) {
+ throw ArgumentError("Failed to parse 'asset' field in AssetEditReadyV2 event data");
+ }
+
+ final assetEdits = (payload['edit'] as List)
+ .map((e) => SyncAssetEditV1.fromJson(e))
+ .whereType()
+ .toList();
+
+ await _syncStreamRepository.updateAssetsV2([asset], debugLabel: 'websocket-edit');
+ await _syncStreamRepository.replaceAssetEditsV1(asset.id, assetEdits, debugLabel: 'websocket-edit');
+
+ _logger.info(
+ 'Successfully processed AssetEditReadyV2 event for asset ${asset.id} with ${assetEdits.length} edits',
+ );
+ } catch (error, stackTrace) {
+ _logger.severe("Error processing AssetEditReadyV2 websocket event", error, stackTrace);
+ }
+ }
+
+ Future _handleRemoteDeleted(Iterable remoteIds) async {
+ if (remoteIds.isEmpty) {
return Future.value();
} else {
- final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(checksums);
+ final localAssetsToTrash = await _localAssetRepository.getAssetsFromBackupAlbums(remoteIds);
if (localAssetsToTrash.isNotEmpty) {
- final mediaUrls = await Future.wait(
- localAssetsToTrash.values
- .expand((e) => e)
- .map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
- );
- _logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
- final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
- if (result) {
- await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
- }
+ await _trashLocalAssets(localAssetsToTrash);
} else {
- _logger.info("No assets found in backup-enabled albums for assets: $checksums");
+ _logger.info("No assets found in backup-enabled albums for remote assets: $remoteIds");
}
}
}
+ Future _trashLocalAssets(Map> localAssetsToTrash) async {
+ final mediaUrls = await Future.wait(
+ localAssetsToTrash.values
+ .expand((e) => e)
+ .map((localAsset) => _storageRepository.getAssetEntityForAsset(localAsset).then((e) => e?.getMediaUrl())),
+ );
+ _logger.info("Moving to trash ${mediaUrls.join(", ")} assets");
+ final result = await _localFilesManager.moveToTrash(mediaUrls.nonNulls.toList());
+ if (result) {
+ await _trashedLocalAssetRepository.trashLocalAsset(localAssetsToTrash);
+ }
+ }
+
Future _applyRemoteRestoreToLocal() async {
final assetsToRestore = await _trashedLocalAssetRepository.getToRestore();
if (assetsToRestore.isNotEmpty) {
@@ -413,4 +509,23 @@ class SyncStreamService {
_logger.info("No remote assets found for restoration");
}
}
+
+ Future _syncAssetTrashStatus(List remoteIds) async {
+ if (!(await _localFilesManager.hasManageMediaPermission())) {
+ _logger.warning("Syncing asset trash status cannot proceed because MANAGE_MEDIA permission is missing");
+ return;
+ }
+
+ await _handleRemoteDeleted(remoteIds);
+ await _applyRemoteRestoreToLocal();
+ }
+
+ Future _syncAssetDeletion(List remoteIds) async {
+ if (!(await _localFilesManager.hasManageMediaPermission())) {
+ _logger.warning("Syncing asset deletion cannot proceed because MANAGE_MEDIA permission is missing");
+ return;
+ }
+
+ await _handleRemoteDeleted(remoteIds);
+ }
}
diff --git a/mobile/lib/domain/utils/background_sync.dart b/mobile/lib/domain/utils/background_sync.dart
index 7c9b6ae061..030e77cd54 100644
--- a/mobile/lib/domain/utils/background_sync.dart
+++ b/mobile/lib/domain/utils/background_sync.dart
@@ -186,7 +186,7 @@ class BackgroundSyncManager {
});
}
- Future syncWebsocketBatch(List batchData) {
+ Future syncWebsocketBatchV1(List batchData) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
@@ -196,7 +196,17 @@ class BackgroundSyncManager {
});
}
- Future syncWebsocketEdit(dynamic data) {
+ Future syncWebsocketBatchV2(List batchData) {
+ if (_syncWebsocketTask != null) {
+ return _syncWebsocketTask!.future;
+ }
+ _syncWebsocketTask = _handleWsAssetUploadReadyV2Batch(batchData);
+ return _syncWebsocketTask!.whenComplete(() {
+ _syncWebsocketTask = null;
+ });
+ }
+
+ Future syncWebsocketEditV1(dynamic data) {
if (_syncWebsocketTask != null) {
return _syncWebsocketTask!.future;
}
@@ -206,6 +216,16 @@ class BackgroundSyncManager {
});
}
+ Future syncWebsocketEditV2(dynamic data) {
+ if (_syncWebsocketTask != null) {
+ return _syncWebsocketTask!.future;
+ }
+ _syncWebsocketTask = _handleWsAssetEditReadyV2(data);
+ return _syncWebsocketTask!.whenComplete(() {
+ _syncWebsocketTask = null;
+ });
+ }
+
Future syncLinkedAlbum() {
if (_linkedAlbumSyncTask != null) {
return _linkedAlbumSyncTask!.future;
@@ -242,7 +262,17 @@ Cancelable _handleWsAssetUploadReadyV1Batch(List batchData) => ru
debugLabel: 'websocket-batch',
);
+Cancelable _handleWsAssetUploadReadyV2Batch(List batchData) => runInIsolateGentle(
+ computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetUploadReadyV2Batch(batchData),
+ debugLabel: 'websocket-batch',
+);
+
Cancelable _handleWsAssetEditReadyV1(dynamic data) => runInIsolateGentle(
computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV1(data),
debugLabel: 'websocket-edit',
);
+
+Cancelable _handleWsAssetEditReadyV2(dynamic data) => runInIsolateGentle(
+ computation: (ref) => ref.read(syncStreamServiceProvider).handleWsAssetEditReadyV2(data),
+ debugLabel: 'websocket-edit',
+);
diff --git a/mobile/lib/extensions/asset_extensions.dart b/mobile/lib/extensions/asset_extensions.dart
index 6bcc11f18d..3a994f9cb8 100644
--- a/mobile/lib/extensions/asset_extensions.dart
+++ b/mobile/lib/extensions/asset_extensions.dart
@@ -1,6 +1,5 @@
import 'package:immich_mobile/domain/models/asset/base_asset.model.dart';
import 'package:immich_mobile/domain/models/exif.model.dart';
-import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/infrastructure/utils/exif.converter.dart';
import 'package:openapi/api.dart' as api;
@@ -14,7 +13,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
- durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
+ durationMs: duration,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
@@ -36,7 +35,7 @@ extension DTOToAsset on api.AssetResponseDto {
updatedAt: updatedAt,
ownerId: ownerId,
visibility: visibility.toAssetVisibility(),
- durationMs: duration?.toDuration()?.inMilliseconds ?? 0,
+ durationMs: duration,
height: height?.toInt(),
width: width?.toInt(),
isFavorite: isFavorite,
diff --git a/mobile/lib/infrastructure/entities/metadata.entity.dart b/mobile/lib/infrastructure/entities/metadata.entity.dart
new file mode 100644
index 0000000000..2908245040
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/metadata.entity.dart
@@ -0,0 +1,18 @@
+import 'package:drift/drift.dart';
+import 'package:immich_mobile/infrastructure/utils/drift_default.mixin.dart';
+
+class MetadataEntity extends Table with DriftDefaultsMixin {
+ const MetadataEntity();
+
+ TextColumn get key => text()();
+
+ TextColumn get value => text()();
+
+ DateTimeColumn get updatedAt => dateTime().withDefault(currentDateAndTime)();
+
+ @override
+ Set get primaryKey => {key};
+
+ @override
+ String get tableName => "metadata";
+}
diff --git a/mobile/lib/infrastructure/entities/metadata.entity.drift.dart b/mobile/lib/infrastructure/entities/metadata.entity.drift.dart
new file mode 100644
index 0000000000..80bf7bfc43
--- /dev/null
+++ b/mobile/lib/infrastructure/entities/metadata.entity.drift.dart
@@ -0,0 +1,429 @@
+// dart format width=80
+// ignore_for_file: type=lint
+import 'package:drift/drift.dart' as i0;
+import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
+ as i1;
+import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart'
+ as i2;
+import 'package:drift/src/runtime/query_builder/query_builder.dart' as i3;
+
+typedef $$MetadataEntityTableCreateCompanionBuilder =
+ i1.MetadataEntityCompanion Function({
+ required String key,
+ required String value,
+ i0.Value updatedAt,
+ });
+typedef $$MetadataEntityTableUpdateCompanionBuilder =
+ i1.MetadataEntityCompanion Function({
+ i0.Value key,
+ i0.Value value,
+ i0.Value updatedAt,
+ });
+
+class $$MetadataEntityTableFilterComposer
+ extends i0.Composer {
+ $$MetadataEntityTableFilterComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.ColumnFilters get key => $composableBuilder(
+ column: $table.key,
+ builder: (column) => i0.ColumnFilters(column),
+ );
+
+ i0.ColumnFilters get value => $composableBuilder(
+ column: $table.value,
+ builder: (column) => i0.ColumnFilters(column),
+ );
+
+ i0.ColumnFilters get updatedAt => $composableBuilder(
+ column: $table.updatedAt,
+ builder: (column) => i0.ColumnFilters(column),
+ );
+}
+
+class $$MetadataEntityTableOrderingComposer
+ extends i0.Composer {
+ $$MetadataEntityTableOrderingComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.ColumnOrderings get key => $composableBuilder(
+ column: $table.key,
+ builder: (column) => i0.ColumnOrderings(column),
+ );
+
+ i0.ColumnOrderings get value => $composableBuilder(
+ column: $table.value,
+ builder: (column) => i0.ColumnOrderings(column),
+ );
+
+ i0.ColumnOrderings get updatedAt => $composableBuilder(
+ column: $table.updatedAt,
+ builder: (column) => i0.ColumnOrderings(column),
+ );
+}
+
+class $$MetadataEntityTableAnnotationComposer
+ extends i0.Composer {
+ $$MetadataEntityTableAnnotationComposer({
+ required super.$db,
+ required super.$table,
+ super.joinBuilder,
+ super.$addJoinBuilderToRootComposer,
+ super.$removeJoinBuilderFromRootComposer,
+ });
+ i0.GeneratedColumn get key =>
+ $composableBuilder(column: $table.key, builder: (column) => column);
+
+ i0.GeneratedColumn get value =>
+ $composableBuilder(column: $table.value, builder: (column) => column);
+
+ i0.GeneratedColumn get updatedAt =>
+ $composableBuilder(column: $table.updatedAt, builder: (column) => column);
+}
+
+class $$MetadataEntityTableTableManager
+ extends
+ i0.RootTableManager<
+ i0.GeneratedDatabase,
+ i1.$MetadataEntityTable,
+ i1.MetadataEntityData,
+ i1.$$MetadataEntityTableFilterComposer,
+ i1.$$MetadataEntityTableOrderingComposer,
+ i1.$$MetadataEntityTableAnnotationComposer,
+ $$MetadataEntityTableCreateCompanionBuilder,
+ $$MetadataEntityTableUpdateCompanionBuilder,
+ (
+ i1.MetadataEntityData,
+ i0.BaseReferences<
+ i0.GeneratedDatabase,
+ i1.$MetadataEntityTable,
+ i1.MetadataEntityData
+ >,
+ ),
+ i1.MetadataEntityData,
+ i0.PrefetchHooks Function()
+ > {
+ $$MetadataEntityTableTableManager(
+ i0.GeneratedDatabase db,
+ i1.$MetadataEntityTable table,
+ ) : super(
+ i0.TableManagerState(
+ db: db,
+ table: table,
+ createFilteringComposer: () =>
+ i1.$$MetadataEntityTableFilterComposer($db: db, $table: table),
+ createOrderingComposer: () =>
+ i1.$$MetadataEntityTableOrderingComposer($db: db, $table: table),
+ createComputedFieldComposer: () => i1
+ .$$MetadataEntityTableAnnotationComposer($db: db, $table: table),
+ updateCompanionCallback:
+ ({
+ i0.Value key = const i0.Value.absent(),
+ i0.Value value = const i0.Value.absent(),
+ i0.Value updatedAt = const i0.Value.absent(),
+ }) => i1.MetadataEntityCompanion(
+ key: key,
+ value: value,
+ updatedAt: updatedAt,
+ ),
+ createCompanionCallback:
+ ({
+ required String key,
+ required String value,
+ i0.Value updatedAt = const i0.Value.absent(),
+ }) => i1.MetadataEntityCompanion.insert(
+ key: key,
+ value: value,
+ updatedAt: updatedAt,
+ ),
+ withReferenceMapper: (p0) => p0
+ .map((e) => (e.readTable(table), i0.BaseReferences(db, table, e)))
+ .toList(),
+ prefetchHooksCallback: null,
+ ),
+ );
+}
+
+typedef $$MetadataEntityTableProcessedTableManager =
+ i0.ProcessedTableManager<
+ i0.GeneratedDatabase,
+ i1.$MetadataEntityTable,
+ i1.MetadataEntityData,
+ i1.$$MetadataEntityTableFilterComposer,
+ i1.$$MetadataEntityTableOrderingComposer,
+ i1.$$MetadataEntityTableAnnotationComposer,
+ $$MetadataEntityTableCreateCompanionBuilder,
+ $$MetadataEntityTableUpdateCompanionBuilder,
+ (
+ i1.MetadataEntityData,
+ i0.BaseReferences<
+ i0.GeneratedDatabase,
+ i1.$MetadataEntityTable,
+ i1.MetadataEntityData
+ >,
+ ),
+ i1.MetadataEntityData,
+ i0.PrefetchHooks Function()
+ >;
+
+class $MetadataEntityTable extends i2.MetadataEntity
+ with i0.TableInfo<$MetadataEntityTable, i1.MetadataEntityData> {
+ @override
+ final i0.GeneratedDatabase attachedDatabase;
+ final String? _alias;
+ $MetadataEntityTable(this.attachedDatabase, [this._alias]);
+ static const i0.VerificationMeta _keyMeta = const i0.VerificationMeta('key');
+ @override
+ late final i0.GeneratedColumn key = i0.GeneratedColumn(
+ 'key',
+ aliasedName,
+ false,
+ type: i0.DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ static const i0.VerificationMeta _valueMeta = const i0.VerificationMeta(
+ 'value',
+ );
+ @override
+ late final i0.GeneratedColumn value = i0.GeneratedColumn(
+ 'value',
+ aliasedName,
+ false,
+ type: i0.DriftSqlType.string,
+ requiredDuringInsert: true,
+ );
+ static const i0.VerificationMeta _updatedAtMeta = const i0.VerificationMeta(
+ 'updatedAt',
+ );
+ @override
+ late final i0.GeneratedColumn updatedAt =
+ i0.GeneratedColumn(
+ 'updated_at',
+ aliasedName,
+ false,
+ type: i0.DriftSqlType.dateTime,
+ requiredDuringInsert: false,
+ defaultValue: i3.currentDateAndTime,
+ );
+ @override
+ List get $columns => [key, value, updatedAt];
+ @override
+ String get aliasedName => _alias ?? actualTableName;
+ @override
+ String get actualTableName => $name;
+ static const String $name = 'metadata';
+ @override
+ i0.VerificationContext validateIntegrity(
+ i0.Insertable instance, {
+ bool isInserting = false,
+ }) {
+ final context = i0.VerificationContext();
+ final data = instance.toColumns(true);
+ if (data.containsKey('key')) {
+ context.handle(
+ _keyMeta,
+ key.isAcceptableOrUnknown(data['key']!, _keyMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_keyMeta);
+ }
+ if (data.containsKey('value')) {
+ context.handle(
+ _valueMeta,
+ value.isAcceptableOrUnknown(data['value']!, _valueMeta),
+ );
+ } else if (isInserting) {
+ context.missing(_valueMeta);
+ }
+ if (data.containsKey('updated_at')) {
+ context.handle(
+ _updatedAtMeta,
+ updatedAt.isAcceptableOrUnknown(data['updated_at']!, _updatedAtMeta),
+ );
+ }
+ return context;
+ }
+
+ @override
+ Set get $primaryKey => {key};
+ @override
+ i1.MetadataEntityData map(Map data, {String? tablePrefix}) {
+ final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : '';
+ return i1.MetadataEntityData(
+ key: attachedDatabase.typeMapping.read(
+ i0.DriftSqlType.string,
+ data['${effectivePrefix}key'],
+ )!,
+ value: attachedDatabase.typeMapping.read(
+ i0.DriftSqlType.string,
+ data['${effectivePrefix}value'],
+ )!,
+ updatedAt: attachedDatabase.typeMapping.read(
+ i0.DriftSqlType.dateTime,
+ data['${effectivePrefix}updated_at'],
+ )!,
+ );
+ }
+
+ @override
+ $MetadataEntityTable createAlias(String alias) {
+ return $MetadataEntityTable(attachedDatabase, alias);
+ }
+
+ @override
+ bool get withoutRowId => true;
+ @override
+ bool get isStrict => true;
+}
+
+class MetadataEntityData extends i0.DataClass
+ implements i0.Insertable {
+ final String key;
+ final String value;
+ final DateTime updatedAt;
+ const MetadataEntityData({
+ required this.key,
+ required this.value,
+ required this.updatedAt,
+ });
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ map['key'] = i0.Variable(key);
+ map['value'] = i0.Variable(value);
+ map['updated_at'] = i0.Variable(updatedAt);
+ return map;
+ }
+
+ factory MetadataEntityData.fromJson(
+ Map json, {
+ i0.ValueSerializer? serializer,
+ }) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return MetadataEntityData(
+ key: serializer.fromJson(json['key']),
+ value: serializer.fromJson(json['value']),
+ updatedAt: serializer.fromJson(json['updatedAt']),
+ );
+ }
+ @override
+ Map toJson({i0.ValueSerializer? serializer}) {
+ serializer ??= i0.driftRuntimeOptions.defaultSerializer;
+ return {
+ 'key': serializer.toJson(key),
+ 'value': serializer.toJson(value),
+ 'updatedAt': serializer.toJson(updatedAt),
+ };
+ }
+
+ i1.MetadataEntityData copyWith({
+ String? key,
+ String? value,
+ DateTime? updatedAt,
+ }) => i1.MetadataEntityData(
+ key: key ?? this.key,
+ value: value ?? this.value,
+ updatedAt: updatedAt ?? this.updatedAt,
+ );
+ MetadataEntityData copyWithCompanion(i1.MetadataEntityCompanion data) {
+ return MetadataEntityData(
+ key: data.key.present ? data.key.value : this.key,
+ value: data.value.present ? data.value.value : this.value,
+ updatedAt: data.updatedAt.present ? data.updatedAt.value : this.updatedAt,
+ );
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('MetadataEntityData(')
+ ..write('key: $key, ')
+ ..write('value: $value, ')
+ ..write('updatedAt: $updatedAt')
+ ..write(')'))
+ .toString();
+ }
+
+ @override
+ int get hashCode => Object.hash(key, value, updatedAt);
+ @override
+ bool operator ==(Object other) =>
+ identical(this, other) ||
+ (other is i1.MetadataEntityData &&
+ other.key == this.key &&
+ other.value == this.value &&
+ other.updatedAt == this.updatedAt);
+}
+
+class MetadataEntityCompanion
+ extends i0.UpdateCompanion {
+ final i0.Value key;
+ final i0.Value value;
+ final i0.Value updatedAt;
+ const MetadataEntityCompanion({
+ this.key = const i0.Value.absent(),
+ this.value = const i0.Value.absent(),
+ this.updatedAt = const i0.Value.absent(),
+ });
+ MetadataEntityCompanion.insert({
+ required String key,
+ required String value,
+ this.updatedAt = const i0.Value.absent(),
+ }) : key = i0.Value(key),
+ value = i0.Value(value);
+ static i0.Insertable custom({
+ i0.Expression? key,
+ i0.Expression? value,
+ i0.Expression? updatedAt,
+ }) {
+ return i0.RawValuesInsertable({
+ if (key != null) 'key': key,
+ if (value != null) 'value': value,
+ if (updatedAt != null) 'updated_at': updatedAt,
+ });
+ }
+
+ i1.MetadataEntityCompanion copyWith({
+ i0.Value? key,
+ i0.Value? value,
+ i0.Value? updatedAt,
+ }) {
+ return i1.MetadataEntityCompanion(
+ key: key ?? this.key,
+ value: value ?? this.value,
+ updatedAt: updatedAt ?? this.updatedAt,
+ );
+ }
+
+ @override
+ Map toColumns(bool nullToAbsent) {
+ final map = {};
+ if (key.present) {
+ map['key'] = i0.Variable(key.value);
+ }
+ if (value.present) {
+ map['value'] = i0.Variable(value.value);
+ }
+ if (updatedAt.present) {
+ map['updated_at'] = i0.Variable(updatedAt.value);
+ }
+ return map;
+ }
+
+ @override
+ String toString() {
+ return (StringBuffer('MetadataEntityCompanion(')
+ ..write('key: $key, ')
+ ..write('value: $value, ')
+ ..write('updatedAt: $updatedAt')
+ ..write(')'))
+ .toString();
+ }
+}
diff --git a/mobile/lib/infrastructure/repositories/db.repository.dart b/mobile/lib/infrastructure/repositories/db.repository.dart
index bed62e1b1b..b291207e42 100644
--- a/mobile/lib/infrastructure/repositories/db.repository.dart
+++ b/mobile/lib/infrastructure/repositories/db.repository.dart
@@ -13,6 +13,7 @@ import 'package:immich_mobile/infrastructure/entities/local_asset.entity.dart';
import 'package:immich_mobile/infrastructure/entities/local_asset.entity.drift.dart';
import 'package:immich_mobile/infrastructure/entities/memory.entity.dart';
import 'package:immich_mobile/infrastructure/entities/memory_asset.entity.dart';
+import 'package:immich_mobile/infrastructure/entities/metadata.entity.dart';
import 'package:immich_mobile/infrastructure/entities/partner.entity.dart';
import 'package:immich_mobile/infrastructure/entities/person.entity.dart';
import 'package:immich_mobile/infrastructure/entities/remote_album.entity.dart';
@@ -29,6 +30,7 @@ import 'package:immich_mobile/infrastructure/entities/user.entity.dart';
import 'package:immich_mobile/infrastructure/entities/user_metadata.entity.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.drift.dart';
import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.dart';
+import 'package:logging/logging.dart';
@DriftDatabase(
tables: [
@@ -53,6 +55,7 @@ import 'package:immich_mobile/infrastructure/repositories/db.repository.steps.da
StoreEntity,
TrashedLocalAssetEntity,
AssetEditEntity,
+ MetadataEntity,
],
include: {'package:immich_mobile/infrastructure/entities/merged_asset.drift'},
)
@@ -83,8 +86,19 @@ class Drift extends $Drift {
});
}
+ Future optimize({bool allTables = false}) async {
+ try {
+ if (allTables) {
+ await customStatement('PRAGMA optimize=0x10002');
+ }
+ await customStatement('PRAGMA optimize');
+ } catch (error) {
+ Logger('Drift').fine('Failed to optimize database', error);
+ }
+ }
+
@override
- int get schemaVersion => 24;
+ int get schemaVersion => 25;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -250,6 +264,9 @@ class Drift extends $Drift {
await customStatement('DROP INDEX IF EXISTS idx_remote_album_owner_id');
await m.alterTable(TableMigration(v24.remoteAlbumEntity));
},
+ from24To25: (m, v25) async {
+ await m.createTable(v25.metadata);
+ },
),
);
@@ -260,6 +277,7 @@ class Drift extends $Drift {
}
await customStatement('PRAGMA foreign_keys = ON;');
+ await optimize();
},
beforeOpen: (details) async {
await customStatement('PRAGMA foreign_keys = ON');
diff --git a/mobile/lib/infrastructure/repositories/db.repository.drift.dart b/mobile/lib/infrastructure/repositories/db.repository.drift.dart
index 4e199c51c1..01f44372fb 100644
--- a/mobile/lib/infrastructure/repositories/db.repository.drift.dart
+++ b/mobile/lib/infrastructure/repositories/db.repository.drift.dart
@@ -43,9 +43,11 @@ import 'package:immich_mobile/infrastructure/entities/trashed_local_asset.entity
as i20;
import 'package:immich_mobile/infrastructure/entities/asset_edit.entity.drift.dart'
as i21;
-import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
+import 'package:immich_mobile/infrastructure/entities/metadata.entity.drift.dart'
as i22;
-import 'package:drift/internal/modular.dart' as i23;
+import 'package:immich_mobile/infrastructure/entities/merged_asset.drift.dart'
+ as i23;
+import 'package:drift/internal/modular.dart' as i24;
abstract class $Drift extends i0.GeneratedDatabase {
$Drift(i0.QueryExecutor e) : super(e);
@@ -89,9 +91,12 @@ abstract class $Drift extends i0.GeneratedDatabase {
.$TrashedLocalAssetEntityTable(this);
late final i21.$AssetEditEntityTable assetEditEntity = i21
.$AssetEditEntityTable(this);
- i22.MergedAssetDrift get mergedAssetDrift => i23.ReadDatabaseContainer(
+ late final i22.$MetadataEntityTable metadataEntity = i22.$MetadataEntityTable(
this,
- ).accessor(i22.MergedAssetDrift.new);
+ );
+ i23.MergedAssetDrift get mergedAssetDrift => i24.ReadDatabaseContainer(
+ this,
+ ).accessor(i23.MergedAssetDrift.new);
@override
Iterable> get allTables =>
allSchemaEntities.whereType>();
@@ -129,6 +134,7 @@ abstract class $Drift extends i0.GeneratedDatabase {
storeEntity,
trashedLocalAssetEntity,
assetEditEntity,
+ metadataEntity,
i10.idxPartnerSharedWithId,
i11.idxLatLng,
i12.idxRemoteAlbumAssetAlbumAsset,
@@ -389,4 +395,6 @@ class $DriftManager {
);
i21.$$AssetEditEntityTableTableManager get assetEditEntity =>
i21.$$AssetEditEntityTableTableManager(_db, _db.assetEditEntity);
+ i22.$$MetadataEntityTableTableManager get metadataEntity =>
+ i22.$$MetadataEntityTableTableManager(_db, _db.metadataEntity);
}
diff --git a/mobile/lib/infrastructure/repositories/db.repository.steps.dart b/mobile/lib/infrastructure/repositories/db.repository.steps.dart
index 82bb81628c..4237cc41e3 100644
--- a/mobile/lib/infrastructure/repositories/db.repository.steps.dart
+++ b/mobile/lib/infrastructure/repositories/db.repository.steps.dart
@@ -12375,6 +12375,574 @@ class Shape48 extends i0.VersionedTable {
columnsByName['order']! as i1.GeneratedColumn;
}
+final class Schema25 extends i0.VersionedSchema {
+ Schema25({required super.database}) : super(version: 25);
+ @override
+ late final List entities = [
+ userEntity,
+ remoteAssetEntity,
+ stackEntity,
+ localAssetEntity,
+ remoteAlbumEntity,
+ localAlbumEntity,
+ localAlbumAssetEntity,
+ idxLocalAlbumAssetAlbumAsset,
+ idxLocalAssetChecksum,
+ idxLocalAssetCloudId,
+ idxStackPrimaryAssetId,
+ idxRemoteAssetOwnerChecksum,
+ uQRemoteAssetsOwnerChecksum,
+ uQRemoteAssetsOwnerLibraryChecksum,
+ idxRemoteAssetChecksum,
+ idxRemoteAssetStackId,
+ idxRemoteAssetLocalDateTimeDay,
+ idxRemoteAssetLocalDateTimeMonth,
+ authUserEntity,
+ userMetadataEntity,
+ partnerEntity,
+ remoteExifEntity,
+ remoteAlbumAssetEntity,
+ remoteAlbumUserEntity,
+ remoteAssetCloudIdEntity,
+ memoryEntity,
+ memoryAssetEntity,
+ personEntity,
+ assetFaceEntity,
+ storeEntity,
+ trashedLocalAssetEntity,
+ assetEditEntity,
+ metadata,
+ idxPartnerSharedWithId,
+ idxLatLng,
+ idxRemoteAlbumAssetAlbumAsset,
+ idxRemoteAssetCloudId,
+ idxPersonOwnerId,
+ idxAssetFacePersonId,
+ idxAssetFaceAssetId,
+ idxTrashedLocalAssetChecksum,
+ idxTrashedLocalAssetAlbum,
+ idxAssetEditAssetId,
+ ];
+ late final Shape33 userEntity = Shape33(
+ source: i0.VersionedTable(
+ entityName: 'user_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_108,
+ _column_109,
+ _column_110,
+ _column_111,
+ _column_112,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape34 remoteAssetEntity = Shape34(
+ source: i0.VersionedTable(
+ entityName: 'remote_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_108,
+ _column_113,
+ _column_114,
+ _column_115,
+ _column_116,
+ _column_117,
+ _column_118,
+ _column_107,
+ _column_119,
+ _column_120,
+ _column_121,
+ _column_122,
+ _column_123,
+ _column_124,
+ _column_125,
+ _column_126,
+ _column_127,
+ _column_128,
+ _column_129,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape35 stackEntity = Shape35(
+ source: i0.VersionedTable(
+ entityName: 'stack_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_114,
+ _column_115,
+ _column_121,
+ _column_130,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape36 localAssetEntity = Shape36(
+ source: i0.VersionedTable(
+ entityName: 'local_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_108,
+ _column_113,
+ _column_114,
+ _column_115,
+ _column_116,
+ _column_117,
+ _column_118,
+ _column_107,
+ _column_131,
+ _column_120,
+ _column_132,
+ _column_133,
+ _column_134,
+ _column_135,
+ _column_136,
+ _column_137,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape48 remoteAlbumEntity = Shape48(
+ source: i0.VersionedTable(
+ entityName: 'remote_album_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_108,
+ _column_138,
+ _column_114,
+ _column_115,
+ _column_139,
+ _column_140,
+ _column_141,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape38 localAlbumEntity = Shape38(
+ source: i0.VersionedTable(
+ entityName: 'local_album_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_108,
+ _column_115,
+ _column_142,
+ _column_143,
+ _column_144,
+ _column_145,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape39 localAlbumAssetEntity = Shape39(
+ source: i0.VersionedTable(
+ entityName: 'local_album_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
+ columns: [_column_146, _column_147, _column_145],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ final i1.Index idxLocalAlbumAssetAlbumAsset = i1.Index(
+ 'idx_local_album_asset_album_asset',
+ 'CREATE INDEX IF NOT EXISTS idx_local_album_asset_album_asset ON local_album_asset_entity (album_id, asset_id)',
+ );
+ final i1.Index idxLocalAssetChecksum = i1.Index(
+ 'idx_local_asset_checksum',
+ 'CREATE INDEX IF NOT EXISTS idx_local_asset_checksum ON local_asset_entity (checksum)',
+ );
+ final i1.Index idxLocalAssetCloudId = i1.Index(
+ 'idx_local_asset_cloud_id',
+ 'CREATE INDEX IF NOT EXISTS idx_local_asset_cloud_id ON local_asset_entity (i_cloud_id)',
+ );
+ final i1.Index idxStackPrimaryAssetId = i1.Index(
+ 'idx_stack_primary_asset_id',
+ 'CREATE INDEX IF NOT EXISTS idx_stack_primary_asset_id ON stack_entity (primary_asset_id)',
+ );
+ final i1.Index idxRemoteAssetOwnerChecksum = i1.Index(
+ 'idx_remote_asset_owner_checksum',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_owner_checksum ON remote_asset_entity (owner_id, checksum)',
+ );
+ final i1.Index uQRemoteAssetsOwnerChecksum = i1.Index(
+ 'UQ_remote_assets_owner_checksum',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_checksum ON remote_asset_entity (owner_id, checksum) WHERE(library_id IS NULL)',
+ );
+ final i1.Index uQRemoteAssetsOwnerLibraryChecksum = i1.Index(
+ 'UQ_remote_assets_owner_library_checksum',
+ 'CREATE UNIQUE INDEX IF NOT EXISTS UQ_remote_assets_owner_library_checksum ON remote_asset_entity (owner_id, library_id, checksum) WHERE(library_id IS NOT NULL)',
+ );
+ final i1.Index idxRemoteAssetChecksum = i1.Index(
+ 'idx_remote_asset_checksum',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_checksum ON remote_asset_entity (checksum)',
+ );
+ final i1.Index idxRemoteAssetStackId = i1.Index(
+ 'idx_remote_asset_stack_id',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_stack_id ON remote_asset_entity (stack_id)',
+ );
+ final i1.Index idxRemoteAssetLocalDateTimeDay = i1.Index(
+ 'idx_remote_asset_local_date_time_day',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_day ON remote_asset_entity (STRFTIME(\'%Y-%m-%d\', local_date_time))',
+ );
+ final i1.Index idxRemoteAssetLocalDateTimeMonth = i1.Index(
+ 'idx_remote_asset_local_date_time_month',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_local_date_time_month ON remote_asset_entity (STRFTIME(\'%Y-%m\', local_date_time))',
+ );
+ late final Shape40 authUserEntity = Shape40(
+ source: i0.VersionedTable(
+ entityName: 'auth_user_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_108,
+ _column_109,
+ _column_148,
+ _column_110,
+ _column_111,
+ _column_149,
+ _column_150,
+ _column_151,
+ _column_152,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape4 userMetadataEntity = Shape4(
+ source: i0.VersionedTable(
+ entityName: 'user_metadata_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(user_id, "key")'],
+ columns: [_column_153, _column_154, _column_155],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape41 partnerEntity = Shape41(
+ source: i0.VersionedTable(
+ entityName: 'partner_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(shared_by_id, shared_with_id)'],
+ columns: [_column_156, _column_157, _column_158],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape42 remoteExifEntity = Shape42(
+ source: i0.VersionedTable(
+ entityName: 'remote_exif_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(asset_id)'],
+ columns: [
+ _column_159,
+ _column_160,
+ _column_161,
+ _column_162,
+ _column_163,
+ _column_164,
+ _column_117,
+ _column_116,
+ _column_165,
+ _column_166,
+ _column_167,
+ _column_168,
+ _column_135,
+ _column_136,
+ _column_169,
+ _column_170,
+ _column_171,
+ _column_172,
+ _column_173,
+ _column_174,
+ _column_175,
+ _column_176,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape7 remoteAlbumAssetEntity = Shape7(
+ source: i0.VersionedTable(
+ entityName: 'remote_album_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(asset_id, album_id)'],
+ columns: [_column_159, _column_177],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape10 remoteAlbumUserEntity = Shape10(
+ source: i0.VersionedTable(
+ entityName: 'remote_album_user_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(album_id, user_id)'],
+ columns: [_column_177, _column_153, _column_178],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape43 remoteAssetCloudIdEntity = Shape43(
+ source: i0.VersionedTable(
+ entityName: 'remote_asset_cloud_id_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(asset_id)'],
+ columns: [
+ _column_159,
+ _column_179,
+ _column_180,
+ _column_134,
+ _column_135,
+ _column_136,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape44 memoryEntity = Shape44(
+ source: i0.VersionedTable(
+ entityName: 'memory_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_114,
+ _column_115,
+ _column_124,
+ _column_121,
+ _column_113,
+ _column_181,
+ _column_182,
+ _column_183,
+ _column_184,
+ _column_185,
+ _column_186,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape12 memoryAssetEntity = Shape12(
+ source: i0.VersionedTable(
+ entityName: 'memory_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(asset_id, memory_id)'],
+ columns: [_column_159, _column_187],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape45 personEntity = Shape45(
+ source: i0.VersionedTable(
+ entityName: 'person_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_114,
+ _column_115,
+ _column_121,
+ _column_108,
+ _column_188,
+ _column_189,
+ _column_190,
+ _column_191,
+ _column_192,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape46 assetFaceEntity = Shape46(
+ source: i0.VersionedTable(
+ entityName: 'asset_face_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_159,
+ _column_193,
+ _column_194,
+ _column_195,
+ _column_196,
+ _column_197,
+ _column_198,
+ _column_199,
+ _column_200,
+ _column_201,
+ _column_124,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape18 storeEntity = Shape18(
+ source: i0.VersionedTable(
+ entityName: 'store_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [_column_202, _column_203, _column_204],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape47 trashedLocalAssetEntity = Shape47(
+ source: i0.VersionedTable(
+ entityName: 'trashed_local_asset_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id, album_id)'],
+ columns: [
+ _column_108,
+ _column_113,
+ _column_114,
+ _column_115,
+ _column_116,
+ _column_117,
+ _column_118,
+ _column_107,
+ _column_205,
+ _column_131,
+ _column_120,
+ _column_132,
+ _column_206,
+ _column_137,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape32 assetEditEntity = Shape32(
+ source: i0.VersionedTable(
+ entityName: 'asset_edit_entity',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY(id)'],
+ columns: [
+ _column_107,
+ _column_159,
+ _column_207,
+ _column_208,
+ _column_209,
+ ],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ late final Shape49 metadata = Shape49(
+ source: i0.VersionedTable(
+ entityName: 'metadata',
+ withoutRowId: true,
+ isStrict: true,
+ tableConstraints: ['PRIMARY KEY("key")'],
+ columns: [_column_210, _column_211, _column_115],
+ attachedDatabase: database,
+ ),
+ alias: null,
+ );
+ final i1.Index idxPartnerSharedWithId = i1.Index(
+ 'idx_partner_shared_with_id',
+ 'CREATE INDEX IF NOT EXISTS idx_partner_shared_with_id ON partner_entity (shared_with_id)',
+ );
+ final i1.Index idxLatLng = i1.Index(
+ 'idx_lat_lng',
+ 'CREATE INDEX IF NOT EXISTS idx_lat_lng ON remote_exif_entity (latitude, longitude)',
+ );
+ final i1.Index idxRemoteAlbumAssetAlbumAsset = i1.Index(
+ 'idx_remote_album_asset_album_asset',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_album_asset_album_asset ON remote_album_asset_entity (album_id, asset_id)',
+ );
+ final i1.Index idxRemoteAssetCloudId = i1.Index(
+ 'idx_remote_asset_cloud_id',
+ 'CREATE INDEX IF NOT EXISTS idx_remote_asset_cloud_id ON remote_asset_cloud_id_entity (cloud_id)',
+ );
+ final i1.Index idxPersonOwnerId = i1.Index(
+ 'idx_person_owner_id',
+ 'CREATE INDEX IF NOT EXISTS idx_person_owner_id ON person_entity (owner_id)',
+ );
+ final i1.Index idxAssetFacePersonId = i1.Index(
+ 'idx_asset_face_person_id',
+ 'CREATE INDEX IF NOT EXISTS idx_asset_face_person_id ON asset_face_entity (person_id)',
+ );
+ final i1.Index idxAssetFaceAssetId = i1.Index(
+ 'idx_asset_face_asset_id',
+ 'CREATE INDEX IF NOT EXISTS idx_asset_face_asset_id ON asset_face_entity (asset_id)',
+ );
+ final i1.Index idxTrashedLocalAssetChecksum = i1.Index(
+ 'idx_trashed_local_asset_checksum',
+ 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_checksum ON trashed_local_asset_entity (checksum)',
+ );
+ final i1.Index idxTrashedLocalAssetAlbum = i1.Index(
+ 'idx_trashed_local_asset_album',
+ 'CREATE INDEX IF NOT EXISTS idx_trashed_local_asset_album ON trashed_local_asset_entity (album_id)',
+ );
+ final i1.Index idxAssetEditAssetId = i1.Index(
+ 'idx_asset_edit_asset_id',
+ 'CREATE INDEX IF NOT EXISTS idx_asset_edit_asset_id ON asset_edit_entity (asset_id)',
+ );
+}
+
+class Shape49 extends i0.VersionedTable {
+ Shape49({required super.source, required super.alias}) : super.aliased();
+ i1.GeneratedColumn get key =>
+ columnsByName['key']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get value =>
+ columnsByName['value']! as i1.GeneratedColumn;
+ i1.GeneratedColumn get updatedAt =>
+ columnsByName['updated_at']! as i1.GeneratedColumn;
+}
+
+i1.GeneratedColumn _column_210(String aliasedName) =>
+ i1.GeneratedColumn(
+ 'key',
+ aliasedName,
+ false,
+ type: i1.DriftSqlType.string,
+ $customConstraints: 'NOT NULL',
+ );
+i1.GeneratedColumn _column_211(String aliasedName) =>
+ i1.GeneratedColumn(
+ 'value',
+ aliasedName,
+ false,
+ type: i1.DriftSqlType.string,
+ $customConstraints: 'NOT NULL',
+ );
i0.MigrationStepWithVersion migrationSteps({
required Future Function(i1.Migrator m, Schema2 schema) from1To2,
required Future Function(i1.Migrator m, Schema3 schema) from2To3,
@@ -12399,6 +12967,7 @@ i0.MigrationStepWithVersion migrationSteps({
required Future Function(i1.Migrator m, Schema22 schema) from21To22,
required Future Function(i1.Migrator m, Schema23 schema) from22To23,
required Future Function(i1.Migrator m, Schema24 schema) from23To24,
+ required Future Function(i1.Migrator m, Schema25 schema) from24To25,
}) {
return (currentVersion, database) async {
switch (currentVersion) {
@@ -12517,6 +13086,11 @@ i0.MigrationStepWithVersion migrationSteps({
final migrator = i1.Migrator(database, schema);
await from23To24(migrator, schema);
return 24;
+ case 24:
+ final schema = Schema25(database: database);
+ final migrator = i1.Migrator(database, schema);
+ await from24To25(migrator, schema);
+ return 25;
default:
throw ArgumentError.value('Unknown migration from $currentVersion');
}
@@ -12547,6 +13121,7 @@ i1.OnUpgrade stepByStep({
required Future Function(i1.Migrator m, Schema22 schema) from21To22,
required Future Function(i1.Migrator m, Schema23 schema) from22To23,
required Future Function(i1.Migrator m, Schema24 schema) from23To24,
+ required Future Function(i1.Migrator m, Schema25 schema) from24To25,
}) => i0.VersionedSchema.stepByStepHelper(
step: migrationSteps(
from1To2: from1To2,
@@ -12572,5 +13147,6 @@ i1.OnUpgrade stepByStep({
from21To22: from21To22,
from22To23: from22To23,
from23To24: from23To24,
+ from24To25: from24To25,
),
);
diff --git a/mobile/lib/infrastructure/repositories/local_asset.repository.dart b/mobile/lib/infrastructure/repositories/local_asset.repository.dart
index 6f6ef20aeb..c34d2c4697 100644
--- a/mobile/lib/infrastructure/repositories/local_asset.repository.dart
+++ b/mobile/lib/infrastructure/repositories/local_asset.repository.dart
@@ -109,31 +109,40 @@ class DriftLocalAssetRepository extends DriftDatabaseRepository {
return query.map((localAlbum) => localAlbum.toDto()).get();
}
- Future