Compare commits

..

10 Commits

Author SHA1 Message Date
martabal
e36b7240c7 fix: merge main 2024-05-28 00:42:52 +02:00
martabal
fec3a04123 merge main 2024-05-28 00:04:45 +02:00
martabal
d919d2c975 chore: regenarate api 2024-05-26 22:15:28 +02:00
martabal
7ac9c1bb78 merge main 2024-05-26 22:14:36 +02:00
martabal
0f6e665d99 pr feedback 2024-05-12 17:25:49 +02:00
martin
7ced61e67d Update web/src/lib/components/faces-page/unassigned-faces-side-panel.svelte
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-11 22:44:48 +02:00
martin
ac26d2f45f Update web/src/lib/components/faces-page/unassigned-faces-side-panel.svelte
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-11 22:44:39 +02:00
martin
782d02a5e2 Update web/src/lib/components/faces-page/person-side-panel.svelte
Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com>
2024-05-11 22:44:31 +02:00
martabal
22c42dd8ff pr feedback 2024-05-11 22:43:56 +02:00
martabal
7e9dcaacff feat: unassign faces 2024-05-11 19:52:23 +02:00
477 changed files with 8998 additions and 12555 deletions

View File

@@ -45,7 +45,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
flutter-version: '3.22.0'
cache: true
- name: Create the Keystore

View File

@@ -35,7 +35,7 @@ jobs:
steps:
- name: Clean temporary images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0
uses: stumpylog/image-cleaner-action/ephemeral@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"
@@ -64,7 +64,7 @@ jobs:
steps:
- name: Clean untagged images
if: "${{ env.TOKEN != '' }}"
uses: stumpylog/image-cleaner-action/untagged@v0.7.0
uses: stumpylog/image-cleaner-action/untagged@v0.6.0
with:
token: "${{ env.TOKEN }}"
owner: "immich-app"

View File

@@ -1,43 +0,0 @@
name: Docs build
on:
push:
branches: [main]
paths:
- "docs/**"
pull_request:
branches: [main]
paths:
- "docs/**"
release:
types: [published]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Check formatting
run: npm run format
- name: Run build
run: npm run build
- name: Upload build output
uses: actions/upload-artifact@v4
with:
name: docs-build-output
path: docs/build/
retention-days: 1

View File

@@ -1,189 +0,0 @@
name: Docs deploy
on:
workflow_run:
workflows: ["Docs build"]
types:
- completed
jobs:
checks:
runs-on: ubuntu-latest
outputs:
parameters: ${{ steps.parameters.outputs.result }}
steps:
- if: ${{ github.event.workflow_run.conclusion == 'failure' }}
run: echo 'The triggering workflow failed' && exit 1
- name: Determine deploy parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const eventType = context.payload.workflow_run.event;
const isFork = context.payload.workflow_run.repository.fork;
let parameters;
console.log({eventType, isFork});
if (eventType == "push") {
const branch = context.payload.workflow_run.head_branch;
console.log({branch});
const shouldDeploy = !isFork && branch == "main";
parameters = {
event: "branch",
name: "main",
shouldDeploy
};
} else if (eventType == "pull_request") {
let pull_number = context.payload.workflow_run.pull_requests[0]?.number;
if(!pull_number) {
const response = await github.rest.search.issuesAndPullRequests({q: 'repo:${{ github.repository }} is:pr sha:${{ github.event.workflow_run.head_sha }}',per_page: 1,})
const items = response.data.items
if (items.length < 1) {
throw new Error("No pull request found for the commit")
}
const pullRequestNumber = items[0].number
console.info("Pull request number is", pullRequestNumber)
pull_number = pullRequestNumber
}
const {data: pr} = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number
});
console.log({pull_number});
parameters = {
event: "pr",
name: `pr-${pull_number}`,
pr_number: pull_number,
shouldDeploy: true
};
} else if (eventType == "release") {
parameters = {
event: "release",
name: context.payload.workflow_run.head_branch,
shouldDeploy: !isFork
};
}
console.log(parameters);
return parameters;
deploy:
runs-on: ubuntu-latest
needs: checks
if: ${{ fromJson(needs.checks.outputs.parameters).shouldDeploy }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Load parameters
id: parameters
uses: actions/github-script@v7
with:
script: |
const json = `${{ needs.checks.outputs.parameters }}`;
const parameters = JSON.parse(json);
core.setOutput("event", parameters.event);
core.setOutput("name", parameters.name);
core.setOutput("shouldDeploy", parameters.shouldDeploy);
- run: |
echo "Starting docs deployment for ${{ steps.parameters.outputs.event }} ${{ steps.parameters.outputs.name }}"
- name: Download artifact
uses: actions/github-script@v7
with:
script: |
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
owner: context.repo.owner,
repo: context.repo.repo,
run_id: context.payload.workflow_run.id,
});
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
return artifact.name == "docs-build-output"
})[0];
let download = await github.rest.actions.downloadArtifact({
owner: context.repo.owner,
repo: context.repo.repo,
artifact_id: matchArtifact.id,
archive_format: 'zip',
});
let fs = require('fs');
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/docs-build-output.zip`, Buffer.from(download.data));
- name: Unzip artifact
run: unzip "${{ github.workspace }}/docs-build-output.zip" -d "${{ github.workspace }}/docs/build"
- name: Deploy Docs Subdomain
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "apply"
- name: Deploy Docs Subdomain Output
id: docs-output
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
TF_VAR_prefix_event_type: ${{ steps.parameters.outputs.event }}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "output -json"
- name: Output Cleaning
id: clean
run: |
TG_OUT=$(echo '${{ steps.docs-output.outputs.tg_action_output }}' | sed 's|%0A|\n|g ; s|%3C|<|g' | jq -c .)
echo "output=$TG_OUT" >> $GITHUB_OUTPUT
- name: Publish to Cloudflare Pages
uses: cloudflare/pages-action@v1
with:
apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN_PAGES_UPLOAD }}
accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
projectName: ${{ fromJson(steps.clean.outputs.output).pages_project_name.value }}
workingDirectory: "docs"
directory: "build"
branch: ${{ steps.parameters.outputs.name }}
wranglerVersion: '3'
- name: Deploy Docs Release Domain
if: ${{ steps.parameters.outputs.event == 'release' }}
env:
TF_VAR_prefix_name: ${{ steps.parameters.outputs.name}}
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: '0.58.12'
tofu_version: '1.7.1'
tg_dir: 'deployment/modules/cloudflare/docs-release'
tg_command: 'apply'
- name: Comment
uses: actions-cool/maintain-one-comment@v3
if: ${{ steps.parameters.outputs.event == 'pr' }}
with:
number: ${{ fromJson(needs.checks.outputs.parameters).pr_number }}
body: |
📖 Documentation deployed to [${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }}](https://${{ fromJson(steps.clean.outputs.output).immich_app_branch_subdomain.value }})
emojis: 'rocket'
body-include: '<!-- Docs PR URL -->'

View File

@@ -1,32 +0,0 @@
name: Docs destroy
on:
pull_request_target:
types: [closed]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Destroy Docs Subdomain
env:
TF_VAR_prefix_name: "pr-${{ github.event.number }}"
TF_VAR_prefix_event_type: "pr"
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
TF_STATE_POSTGRES_CONN_STR: ${{ secrets.TF_STATE_POSTGRES_CONN_STR }}
uses: gruntwork-io/terragrunt-action@v2
with:
tg_version: "0.58.12"
tofu_version: "1.7.1"
tg_dir: "deployment/modules/cloudflare/docs"
tg_command: "destroy"
- name: Comment
uses: actions-cool/maintain-one-comment@v3
with:
number: ${{ github.event.number }}
delete: true
body-include: '<!-- Docs PR URL -->'

View File

@@ -23,7 +23,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
flutter-version: '3.22.0'
- name: Install dependencies
run: dart pub get

View File

@@ -10,6 +10,28 @@ concurrency:
cancel-in-progress: true
jobs:
doc-tests:
name: Docs
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./docs
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run npm install
run: npm ci
- name: Run formatter
run: npm run format
if: ${{ !cancelled() }}
- name: Run build
run: npm run build
if: ${{ !cancelled() }}
server-unit-tests:
name: Server
runs-on: ubuntu-latest
@@ -186,7 +208,7 @@ jobs:
uses: subosito/flutter-action@v2
with:
channel: 'stable'
flutter-version-file: ./mobile/pubspec.yaml
flutter-version: '3.22.0'
- name: Run tests
working-directory: ./mobile
run: flutter test -j 1

View File

@@ -1 +1 @@
20.14
20.13

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19@sha256:696ae41fb5880949a15ade7879a2deae93b3f0723f757bdb5b8a9e4a744ce27f as core
FROM node:20-alpine3.19@sha256:291e84d956f1aff38454bbd3da38941461ad569a185c20aa289f71f37ea08e23 as core
WORKDIR /usr/src/open-api/typescript-sdk
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./

106
cli/package-lock.json generated
View File

@@ -54,7 +54,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.12.12",
"typescript": "^5.3.3"
}
},
@@ -1138,9 +1138,9 @@
}
},
"node_modules/@types/node": {
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1154,17 +1154,17 @@
"dev": true
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1188,16 +1188,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1217,14 +1217,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1235,14 +1235,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1263,9 +1263,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1277,14 +1277,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1306,16 +1306,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1329,13 +1329,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1801,11 +1801,10 @@
"dev": true
},
"node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"version": "12.0.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.0.0.tgz",
"integrity": "sha512-MwVNWlYjDTtOjX5PiD7o5pK0UrFU/OYgcJfjjK4RaHZETNtjJqrZa9Y9ds88+A+f+d5lv+561eZ+yCKoS3gbAA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
}
@@ -4281,11 +4280,10 @@
}
},
"node_modules/vite": {
"version": "5.2.12",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.12.tgz",
"integrity": "sha512-/gC8GxzxMK5ntBwb48pR32GGhENnjtY30G4A0jemunsBkiEZFw60s8InGpN8gkhHEkjnRK1aSAxeQgwvFhUHAA==",
"version": "5.2.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz",
"integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "^0.20.1",
"postcss": "^8.4.38",

View File

@@ -62,6 +62,6 @@
"lodash-es": "^4.17.21"
},
"volta": {
"node": "20.14.0"
"node": "20.13.1"
}
}

View File

@@ -1,8 +1,7 @@
import {
Action,
AssetBulkUploadCheckResult,
AssetMediaResponseDto,
AssetMediaStatus,
AssetFileUploadResponseDto,
addAssetsToAlbum,
checkBulkUpload,
createAlbum,
@@ -168,7 +167,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
newAssets.push({ id: response.id, filepath });
if (response.status === AssetMediaStatus.Duplicate) {
if (response.duplicate) {
duplicateCount++;
duplicateSize += stats.size ?? 0;
} else {
@@ -193,7 +192,7 @@ const uploadFiles = async (files: string[], { dryRun, concurrency }: UploadOptio
return newAssets;
};
const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaResponseDto> => {
const uploadFile = async (input: string, stats: Stats): Promise<AssetFileUploadResponseDto> => {
const { baseUrl, headers } = defaults;
const assetPath = path.parse(input);
@@ -226,7 +225,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
formData.append('sidecarData', sidecarData);
}
const response = await fetch(`${baseUrl}/assets`, {
const response = await fetch(`${baseUrl}/asset/upload`, {
method: 'post',
redirect: 'error',
headers: headers as Record<string, string>,

37
cli/src/version.ts Normal file
View File

@@ -0,0 +1,37 @@
import { version } from '../package.json';
export interface ICliVersion {
major: number;
minor: number;
patch: number;
}
export class CliVersion implements ICliVersion {
constructor(
public readonly major: number,
public readonly minor: number,
public readonly patch: number,
) {}
toString() {
return `${this.major}.${this.minor}.${this.patch}`;
}
toJSON() {
const { major, minor, patch } = this;
return { major, minor, patch };
}
static fromString(version: string): CliVersion {
const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
const matchResult = version.match(regex);
if (matchResult) {
const [, major, minor, patch] = matchResult.map(Number);
return new CliVersion(major, minor, patch);
} else {
throw new Error(`Invalid version format: ${version}`);
}
}
}
export const cliVersion = CliVersion.fromString(version);

38
deployment/.gitignore vendored
View File

@@ -1,38 +0,0 @@
# OpenTofu
# Local .terraform directories
**/.terraform/*
# .tfstate files
*.tfstate
*.tfstate.*
# Crash log files
crash.log
crash.*.log
# Ignore override files as they are usually used to override resources locally and so
# are not checked in
override.tf
override.tf.json
*_override.tf
*_override.tf.json
# Include override files you do wish to add to version control using negated pattern
# !example_override.tf
# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
# Ignore CLI configuration files
.terraformrc
terraform.rc
# Terragrunt
# terragrunt cache directories
**/.terragrunt-cache/*
# Terragrunt debug output file (when using `--terragrunt-debug` option)
# See: https://terragrunt.gruntwork.io/docs/reference/cli-options/#terragrunt-debug
terragrunt-debug.tfvars.json

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,14 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_release_domain" {
account_id = var.cloudflare_account_id
project_name = data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name
domain = "immich.app"
}
resource "cloudflare_record" "immich_app_release_domain" {
name = "immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = data.terraform_remote_state.cloudflare_immich_app_docs.outputs.immich_app_branch_pages_hostname
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,27 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}
data "terraform_remote_state" "cloudflare_immich_app_docs" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_immich_app_docs_${var.prefix_name}"
}
}

View File

@@ -1,20 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_release"
}
}

View File

@@ -1,4 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}

View File

@@ -1,38 +0,0 @@
# This file is maintained automatically by "tofu init".
# Manual edits may be lost in future updates.
provider "registry.opentofu.org/cloudflare/cloudflare" {
version = "4.34.0"
constraints = "4.34.0"
hashes = [
"h1:+W0+Xe1AUh7yvHjDbgR9T7CY1UbBC3Y6U7Eo+ucLnJM=",
"h1:2+1lKObDDdFZRluvROF3RKtXD66CFT3PfnHOvR6CmfA=",
"h1:7vluN2wmw8D9nI11YwTgoGv3hGDXlkt8xqQ4L/JABeQ=",
"h1:B0Urm8ZKTJ8cXzSCtEpJ+o+LsD8MXaD6LU59qVbh50Q=",
"h1:FpGLCm5oF12FaRti3E4iQJlkVbdCC7toyGVuH8og7KY=",
"h1:FunTmrCMDy+rom7YskY0WiL5/Y164zFrrD9xnBxU5NY=",
"h1:GrxZhEb+5HzmHF/BvZBdGKBJy6Wyjme0+ABVDz/63to=",
"h1:J36dda2K42/oTfHuZ4jKkW5+nI6BTWFRUvo60P17NJg=",
"h1:Kq0Wyn+j6zoQeghMYixbnfnyP9ZSIEJbOCzMbaCiAQQ=",
"h1:TKxunXCiS/z105sN/kBNFwU6tIKD67JKJ3ZKjwzoCuI=",
"h1:TR0URKFQxsRO5/v7bKm5hkD/CTTjsG7aVGllL/Mf25c=",
"h1:V+3Qs0Reb6r+8p4XjE5ZFDWYrOIN0x5SwORz4wvHOJ4=",
"h1:mZB3Ui7V/lPQMQK53eBOjIHcrul74252dT06Kgn3J+s=",
"h1:wJwZrIXxoki8omXLJ7XA7B1KaSrtcLMJp090fRtFRAc=",
"zh:02aa46743c1585ada8faa7db23af68ea614053a506f88f05d1090ff5e0e68076",
"zh:1e1a545e83e6457a0e15357b23139bc288fb4fbd5e9a5ddfedc95a6a0216b08c",
"zh:29eef2621e0b1501f620e615bf73b1b90d5417d745e38af63634bc03250faf87",
"zh:3c20989d7e1e141882e6091384bf85fdc83f70f3d29e3e047c493a07de992095",
"zh:3d39619379ba29c7ffb15196f0ea72a04c84cfcdf4b39ac42ac4cf4c19f3eae2",
"zh:805f4a2774e9279c590b8214aabe6df9dcc22bb995df2530513f2f78c647ce75",
"zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f",
"zh:8af716f8655a57aa986861a8a7fa1d724594a284bd77c870eaea4db5f8b9732d",
"zh:a3d13c93b4e6ee6004782debaa9a17f990f2fe8ec8ba545c232818bb6064aba9",
"zh:bfa136acf82d3719473c0064446cc16d1b0303d98b06f55f503b7abeebceadb1",
"zh:ca6cf9254ae5436f2efbc01a0e3f7e4aa3c08b45182037b3eb3eb9539b2f7aec",
"zh:cba32d5de02674004e0a5955bd5222016d9991ca0553d4bd3bea517cd9def6ab",
"zh:d22c8cd527c6d0e84567f57be5911792e2fcd5969e3bba3747489f18bb16705b",
"zh:e4eeede9b3e72cdadd6cc252d4cbcf41baee6ecfd12bacd927e2dcbe733ab210",
"zh:facdaa787a69f86203cd3cc6922baea0b4a18bd9c36b0a8162e2e88ef6c90655",
]
}

View File

@@ -1,11 +0,0 @@
terraform {
backend "pg" {}
required_version = "~> 1.7"
required_providers {
cloudflare = {
source = "cloudflare/cloudflare"
version = "4.34.0"
}
}
}

View File

@@ -1,26 +0,0 @@
resource "cloudflare_pages_domain" "immich_app_branch_domain" {
account_id = var.cloudflare_account_id
project_name = local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_name : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_name
domain = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
}
resource "cloudflare_record" "immich_app_branch_subdomain" {
name = "${var.prefix_name}.${local.deploy_domain_prefix}.immich.app"
proxied = true
ttl = 1
type = "CNAME"
value = "${replace(var.prefix_name, "/\\/|\\./", "-")}.${local.is_release ? data.terraform_remote_state.cloudflare_account.outputs.immich_app_archive_pages_project_subdomain : data.terraform_remote_state.cloudflare_account.outputs.immich_app_preview_pages_project_subdomain}"
zone_id = data.terraform_remote_state.cloudflare_account.outputs.immich_app_zone_id
}
output "immich_app_branch_subdomain" {
value = cloudflare_record.immich_app_branch_subdomain.hostname
}
output "immich_app_branch_pages_hostname" {
value = cloudflare_record.immich_app_branch_subdomain.value
}
output "pages_project_name" {
value = cloudflare_pages_domain.immich_app_branch_domain.project_name
}

View File

@@ -1,7 +0,0 @@
locals {
domain_name = "immich.app"
preview_prefix = contains(["branch", "pr"], var.prefix_event_type) ? "preview" : ""
archive_prefix = contains(["release"], var.prefix_event_type) ? "archive" : ""
deploy_domain_prefix = coalesce(local.preview_prefix, local.archive_prefix)
is_release = contains(["release"], var.prefix_event_type)
}

View File

@@ -1,3 +0,0 @@
provider "cloudflare" {
api_token = data.terraform_remote_state.api_keys_state.outputs.terraform_key_cloudflare_docs
}

View File

@@ -1,17 +0,0 @@
data "terraform_remote_state" "api_keys_state" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_api_keys"
}
}
data "terraform_remote_state" "cloudflare_account" {
backend = "pg"
config = {
conn_str = var.tf_state_postgres_conn_str
schema_name = "prod_cloudflare_account"
}
}

View File

@@ -1,24 +0,0 @@
terraform {
source = "."
extra_arguments custom_vars {
commands = get_terraform_commands_that_need_vars()
}
}
include {
path = find_in_parent_folders("state.hcl")
}
locals {
prefix_name = get_env("TF_VAR_prefix_name")
}
remote_state {
backend = "pg"
config = {
conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
schema_name = "prod_cloudflare_immich_app_docs_${local.prefix_name}"
}
}

View File

@@ -1,5 +0,0 @@
variable "cloudflare_account_id" {}
variable "tf_state_postgres_conn_str" {}
variable "prefix_name" {}
variable "prefix_event_type" {}

View File

@@ -1,20 +0,0 @@
locals {
cloudflare_account_id = get_env("CLOUDFLARE_ACCOUNT_ID")
cloudflare_api_token = get_env("CLOUDFLARE_API_TOKEN")
tf_state_postgres_conn_str = get_env("TF_STATE_POSTGRES_CONN_STR")
}
remote_state {
backend = "pg"
config = {
conn_str = local.tf_state_postgres_conn_str
}
}
inputs = {
cloudflare_account_id = local.cloudflare_account_id
cloudflare_api_token = local.cloudflare_api_token
tf_state_postgres_conn_str = local.tf_state_postgres_conn_str
}

View File

@@ -5,9 +5,6 @@ UPLOAD_LOCATION=./library
# The location where your database files are stored
DB_DATA_LOCATION=./postgres
# To set a timezone, uncomment the next line and change Etc/UTC to a TZ identifier from this list: https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List
# TZ=Etc/UTC
# The Immich version to use. You can pin this to a specific version like "v1.71.0"
IMMICH_VERSION=release

View File

@@ -1 +1 @@
20.14
20.13

View File

@@ -67,7 +67,7 @@ Yes, with an [External Library](/docs/features/libraries.md).
### What happens to existing files after I choose a new [Storage Template](/docs/administration/storage-template.mdx)?
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs-workers/#jobs) page.
Template changes will only apply to _new_ assets. To retroactively apply the template to previously uploaded assets, run the Storage Migration Job, available on the [Jobs](/docs/administration/jobs.md) page.
### Why are only photos and not videos being uploaded to Immich?

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

View File

@@ -1,55 +0,0 @@
# Jobs and Workers
## Workers
### Architecture
The `immich-server` container contains multiple workers:
- `api`: responds to API requests for data and files for the web and mobile app.
- `microservices`: handles most other work, such as thumbnail generation and video encoding, in the form of _jobs_. Simply put, a job is a request to process data in the background.
## Split workers
If you prefer to throttle or distribute the workers, you can do this using the [environment variables](/docs/install/environment-variables) to specify which container should pick up which tasks.
For example, for a simple setup with one container for the Web/API and one for all other microservices, you can do the following:
Copy the entire `immich-server` block as a new service and make the following changes to the **copy**:
```diff
- immich-server:
- container_name: immich_server
...
- ports:
- - 2283:3001
+ immich-microservices:
+ container_name: immich_microservices
```
Once you have two copies of the immich-server service, make the following chnages to each one. This will allow one container to only serve the web UI and API, and the other one to handle all other tasks.
```diff
services:
immich-server:
...
+ environment:
+ IMMICH_WORKERS_INCLUDE: 'api'
immich-microservices:
...
+ environment:
+ IMMICH_WORKERS_EXCLUDE: 'api'
```
## Jobs
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -0,0 +1,13 @@
# Jobs
The `immich-server` responds to API requests for data and files for the web and mobile app. To do this quickly and reliably, it offloads most other work to `immich-microservices` in the form of _jobs_. Simply put, a job is a request to process data in the background. Jobs are picked up automatically by microservices containers.
When a new asset is uploaded it kicks off a series of jobs, which include metadata extraction, thumbnail generation, machine learning tasks, and storage template migration, if enabled. To view the status of a job navigate to the Administration -> Jobs page.
Additionally, some jobs run on a schedule, which is every night at midnight. This schedule, with the exception of [External Libraries](/docs/features/libraries) scanning, cannot be changed.
:::info
Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library.
:::
<img src={require('./img/admin-jobs.png').default} width="80%" title="Admin jobs" />

View File

@@ -117,27 +117,7 @@ Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to
Here's an example of OAuth configured for Authentik:
Configuration of Authorised redirect URIs (Authentik OAuth2/OpenID Provider)
<img src={require('./img/authentik-redirect-uris-example.webp').default} width='70%' title="Authentik authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
| Setting | Value |
| ---------------------------- | ---------------------------------------------------------------------------------- |
| Issuer URL | `https://example.immich.app/application/o/immich/.well-known/openid-configuration` |
| Client ID | AFCj2rM1f4rps**\*\*\*\***\***\*\*\*\***lCLEum6hH9... |
| Client Secret | 0v89FXkQOWO\***\*\*\*\*\***\*\*\***\*\*\*\*\***mprbvXD549HH6s1iw... |
| Scope | openid email profile |
| Signing Algorithm | RS256 |
| Storage Label Claim | preferred_username |
| Storage Quota Claim | immich_quota |
| Default Storage Quota (GiB) | 0 (0 for unlimited quota) |
| Button Text | Sign in with Authentik (optional) |
| Auto Register | Enabled (optional) |
| Auto Launch | Enabled (optional) |
| Mobile Redirect URI Override | Disable |
| Mobile Redirect URI | |
<img src={require('./img/oauth-settings.png').default} title="OAuth settings" />
</details>
@@ -146,13 +126,11 @@ Configuration of OAuth in Immich System Settings
### Google Example
Here's an example of OAuth configured for Google:
Configuration of Authorised redirect URIs (Google Console)
<img src={require('./img/google-redirect-uris-example.webp').default} width='50%' title="Google authorised redirect URIs" />
<img src={require('./img/google-example.webp').default} width='50%' title="Authorised redirect URIs" />
Configuration of OAuth in Immich System Settings
Configuration of OAuth in System Settings
| Setting | Value |
| ---------------------------- | ------------------------------------------------------------------------------------------------------ |

View File

@@ -77,6 +77,7 @@ immich-admin list-users
deletedAt: null,
updatedAt: 2023-09-21T15:42:28.129Z,
oauthId: '',
memoriesEnabled: true
}
]
```

View File

@@ -80,7 +80,7 @@ The Immich Microservices image uses the same `Dockerfile` as the Immich Server,
- Background jobs (file deletion, user deletion)
:::info
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs-workers/#jobs) page, which provides some remote queue management capabilities.
This list closely matches what is available on the [Administration > Jobs](/docs/administration/jobs.md) page, which provides some remote queue management capabilities.
:::
### Machine Learning

View File

@@ -44,16 +44,16 @@ If the import paths are edited in a way that an external file is no longer in an
### Troubleshooting
Sometimes, an external library will not scan correctly. This can happen if Immich can't access the files. Here are some things to check:
Sometimes, an external library will not scan correctly. This can happen if immich_server or immich_microservices can't access the files. Here are some things to check:
- In the docker-compose file, are the volumes mounted correctly?
- Are the volumes also mounted to any worker containers?
- Are the volumes identical between the `server` and `microservices` container?
- Are the import paths set correctly, and do they match the path set in docker-compose file?
- Make sure you don't use symlinks in your import libraries, and that you aren't linking across docker mounts.
- Are the permissions set correctly?
- Make sure you are using forward slashes (`/`) and not backward slashes.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_server bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the same in any microservices containers.
To validate that Immich can reach your external library, start a shell inside the container. Run `docker exec -it immich_microservices /bin/bash` to a bash shell. If your import path is `/data/import/photos`, check it with `ls /data/import/photos`. Do the same check for the `immich_server` container. If you cannot access this directory in both the `microservices` and `server` containers, Immich won't be able to import files.
### Exclusion Patterns
@@ -98,7 +98,7 @@ First, we need to plan how we want to organize the libraries. The christmas trip
### Mount Docker Volumes
The `immich-server` container will need access to the gallery. Modify your docker compose file as follows
`immich-server` and `immich-microservices` containers will need access to the gallery. Modify your docker compose file as follows
```diff title="docker-compose.yml"
immich-server:
@@ -107,6 +107,15 @@ The `immich-server` container will need access to the gallery. Modify your docke
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
immich-microservices:
volumes:
- ${UPLOAD_LOCATION}:/usr/src/app/upload
+ - /mnt/nas/christmas-trip:/mnt/media/christmas-trip:ro
+ - /home/user/old-pics:/mnt/media/old-pics:ro
+ - /mnt/media/videos:/mnt/media/videos:ro
+ - "C:/Users/user_name/Desktop/my media:/mnt/media/my-media:ro" # import path in Windows system.
```

View File

@@ -9,7 +9,7 @@ It is important to remember to update the backup settings after following the gu
In our `.env` file, we will define variables that will help us in the future when we want to move to a more advanced server in the future
```diff title=".env"
# You can find documentation for all the supported env variables [here](/docs/install/environment-variables)
# You can find documentation for all the supported env variables at https://immich.app/docs/install/environment-variables
# Custom location where your uploaded, thumbnails, and transcoded video files are stored
- UPLOAD_LOCATION=./library

View File

@@ -11,8 +11,9 @@ docker ps -a # see a list of running and stopped containers
```bash
docker exec -it <id or name> <command> # attach to a container with a command
docker exec -it immich_server bash
docker exec -it immich_machine_learning bash
docker exec -it immich_server sh
docker exec -it immich_microservices sh
docker exec -it immich_machine_learning sh
```
## Logs
@@ -21,6 +22,7 @@ docker exec -it immich_machine_learning bash
docker logs <id or name> # see the logs for a specific container (by id or name)
docker logs immich_server
docker logs immich_microservices
docker logs immich_machine_learning
```

View File

@@ -6,14 +6,20 @@ in a directory on the same machine.
# Mount the directory into the containers.
Edit `docker-compose.yml` to add two new mount points in the section `immich-server:` under `volumes:`
Edit `docker-compose.yml` to add two new mount points in the sections `immich-server:` and `immich-microservices:` under `volumes:`
```diff
immich-server:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
immich-microservices:
volumes:
+ - ${EXTERNAL_PATH}:/usr/src/app/external
```
Be sure to add exactly the same path to both services.
Edit `.env` to define `EXTERNAL_PATH`, substituting in the correct path for your computer:
```

View File

@@ -32,7 +32,7 @@ def upload(file):
}
response = requests.post(
f'{BASE_URL}/assets', headers=headers, data=data, files=files)
f'{BASE_URL}/asset/upload', headers=headers, data=data, files=files)
print(response.json())
# {'id': 'ef96f635-61c7-4639-9e60-61a11c4bbfba', 'duplicate': False}

View File

@@ -7,7 +7,7 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm
- Start the container by running `docker compose up -d`.
:::info
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning container, but facial recognition is done in the `microservices` worker.
Starting with version v1.93.0 face detection work and face recognize were split. From now on face detection is done in the immich_machine_learning service, but facial recognition is done in the immich_microservices service.
:::
:::note

View File

@@ -17,11 +17,11 @@ If this should not work, try running `docker compose up -d --force-recreate`.
## Docker Compose
| Variable | Description | Default | Containers |
| :----------------- | :------------------------------ | :-------: | :----------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, machine learning |
| `UPLOAD_LOCATION` | Host Path for uploads | | server |
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
| Variable | Description | Default | Services |
| :----------------- | :------------------------------ | :-------: | :-------------------------------------- |
| `IMMICH_VERSION` | Image tags | `release` | server, microservices, machine learning |
| `UPLOAD_LOCATION` | Host Path for uploads | | server, microservices |
| `DB_DATA_LOCATION` | Host Path for Postgres database | | database |
:::tip
These environment variables are used by the `docker-compose.yml` file and do **NOT** affect the containers directly.
@@ -38,16 +38,15 @@ Regardless of filesystem, it is not recommended to use a network share for your
## General
| Variable | Description | Default | Containers | Workers |
| :------------------------------ | :---------------------------------------------- | :----------------------: | :----------------------- | :----------------- |
| `TZ` | Timezone | | 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_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server | api, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices |
| `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | |
| Variable | Description | Default | Services |
| :------------------------------ | :------------------------------------------- | :----------------------: | :-------------------------------------- |
| `TZ` | Timezone | | microservices |
| `IMMICH_ENV` | Environment (production, development) | `production` | server, microservices, machine learning |
| `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, microservices, machine learning |
| `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`<sup>\*1</sup> | server, microservices |
| `IMMICH_CONFIG_FILE` | Path to config file | | server, microservices |
| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server |
| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | microservices |
\*1: With the default `WORKDIR` of `/usr/src/app`, this path will resolve to `/usr/src/app/upload`.
It only need to be set if the Immich deployment method is changing.
@@ -55,39 +54,28 @@ It only need to be set if the Immich deployment method is changing.
:::tip
`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.
:::
## Workers
| Variable | Description | Default | Containers |
| :----------------------- | :--------------------------------------------------------------------------------------------------- | :-----: | :--------- |
| `IMMICH_WORKERS_INCLUDE` | Only run these workers. | | server |
| `IMMICH_WORKERS_EXCLUDE` | Do not run these workers. Matches against default workers, or `IMMICH_WORKERS_INCLUDE` if specified. | | server |
:::info
Information on the current workers can be found [here](/docs/administration/jobs-workers).
`TZ` is only used by `exiftool`, which is present in the microservices container, as a fallback in case the timezone cannot be determined from the image metadata.
:::
## Ports
| Variable | Description | Default |
| :------------ | :------------- | :----------------------------------------: |
| `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `IMMICH_PORT` | Listening port | `3001` (server), `3003` (machine learning) |
| Variable | Description | Default |
| :------------ | :------------- | :------------------------------------: |
| `IMMICH_HOST` | Listening host | `0.0.0.0` |
| `IMMICH_PORT` | Listening port | 3001 (server), 3003 (machine learning) |
## Database
| Variable | Description | Default | Containers |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :----------------------------- |
| `DB_URL` | Database URL | | server |
| `DB_HOSTNAME` | Database Host | `database` | server |
| `DB_PORT` | Database Port | `5432` | server |
| `DB_USERNAME` | Database User | `postgres` | server, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database Password | `postgres` | server, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server |
| Variable | Description | Default | Services |
| :---------------------------------- | :----------------------------------------------------------------------- | :----------: | :-------------------------------------------- |
| `DB_URL` | Database URL | | server, microservices |
| `DB_HOSTNAME` | Database Host | `database` | server, microservices |
| `DB_PORT` | Database Port | `5432` | server, microservices |
| `DB_USERNAME` | Database User | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_PASSWORD` | Database Password | `postgres` | server, microservices, database<sup>\*1</sup> |
| `DB_DATABASE_NAME` | Database Name | `immich` | server, microservices, database<sup>\*1</sup> |
| `DB_VECTOR_EXTENSION`<sup>\*2</sup> | Database Vector Extension (one of [`pgvector`, `pgvecto.rs`]) | `pgvecto.rs` | server, microservices |
| `DB_SKIP_MIGRATIONS` | Whether to skip running migrations on startup (one of [`true`, `false`]) | `false` | server, microservices |
\*1: The values of `DB_USERNAME`, `DB_PASSWORD`, and `DB_DATABASE_NAME` are passed to the Postgres container as the variables `POSTGRES_USER`, `POSTGRES_PASSWORD`, and `POSTGRES_DB` in `docker-compose.yml`.
@@ -95,34 +83,30 @@ Information on the current workers can be found [here](/docs/administration/jobs
:::info
All `DB_` variables must be provided to all Immich workers, including `api` and `microservices`.
`DB_URL` must be in the format `postgresql://immichdbusername:immichdbpassword@postgreshost:postgresport/immichdatabasename`.
You can require SSL by adding `?sslmode=require` to the end of the `DB_URL` string, or require SSL and skip certificate verification by adding `?sslmode=require&sslmode=no-verify`.
When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSWORD` and `DB_DATABASE_NAME` database variables are ignored.
:::
## Redis
| Variable | Description | Default | Containers |
| :--------------- | :------------- | :-----: | :--------- |
| `REDIS_URL` | Redis URL | | server |
| `REDIS_SOCKET` | Redis Socket | | server |
| `REDIS_HOSTNAME` | Redis Host | `redis` | server |
| `REDIS_PORT` | Redis Port | `6379` | server |
| `REDIS_USERNAME` | Redis Username | | server |
| `REDIS_PASSWORD` | Redis Password | | server |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server |
| Variable | Description | Default | Services |
| :--------------- | :------------- | :-----: | :-------------------- |
| `REDIS_URL` | Redis URL | | server, microservices |
| `REDIS_HOSTNAME` | Redis Host | `redis` | server, microservices |
| `REDIS_PORT` | Redis Port | `6379` | server, microservices |
| `REDIS_DBINDEX` | Redis DB Index | `0` | server, microservices |
| `REDIS_USERNAME` | Redis Username | | server, microservices |
| `REDIS_PASSWORD` | Redis Password | | server, microservices |
| `REDIS_SOCKET` | Redis Socket | | server, microservices |
:::info
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
More info can be found in the upstream [ioredis][redis-api] documentation.
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
- When `REDIS_URL` is defined, the other redis (`REDIS_*`) variables are ignored.
- When `REDIS_SOCKET` is defined, the other redis (`REDIS_*`) variables are ignored.
:::
Redis (Sentinel) URL example JSON before encoding:
@@ -154,7 +138,7 @@ Redis (Sentinel) URL example JSON before encoding:
## Machine Learning
| Variable | Description | Default | Containers |
| Variable | Description | Default | Services |
| :----------------------------------------------- | :------------------------------------------------------------------- | :-----------------: | :--------------- |
| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning |
| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning |
@@ -179,13 +163,13 @@ Other machine learning parameters can be tuned from the admin UI.
## Prometheus
| Variable | Description | Default | Containers | Workers |
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :--------- | :----------------- |
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server | api, microservices |
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server | api, microservices |
| Variable | Description | Default | Services |
| :----------------------------- | :-------------------------------------------------------------------------------------------- | :-----: | :-------------------- |
| `IMMICH_METRICS`<sup>\*1</sup> | Toggle all metrics (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_API_METRICS` | Toggle metrics for endpoints and response times (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_HOST_METRICS` | Toggle metrics for CPU and memory utilization for host and process (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_IO_METRICS` | Toggle metrics for database queries, image processing, etc. (one of [`true`, `false`]) | | server, microservices |
| `IMMICH_JOB_METRICS` | Toggle metrics for jobs and queues (one of [`true`, `false`]) | | server, microservices |
\*1: Overridden for a metric group when its corresponding environmental variable is set.

View File

@@ -100,9 +100,9 @@ const config = {
label: 'Docs',
},
{
to: '/roadmap',
to: '/milestones',
position: 'right',
label: 'Roadmap',
label: 'Milestones',
},
{
to: '/docs/api',

View File

@@ -56,6 +56,6 @@
"node": ">=20"
},
"volta": {
"node": "20.14.0"
"node": "20.13.1"
}
}

View File

@@ -1,23 +1,28 @@
import useIsBrowser from '@docusaurus/useIsBrowser';
import { mdiCheckboxBlankCircle, mdiCheckboxMarkedCircle } from '@mdi/js';
import Icon from '@mdi/react';
import React from 'react';
import Icon from '@mdi/react';
import { mdiCheckboxMarkedCircleOutline } from '@mdi/js';
import useIsBrowser from '@docusaurus/useIsBrowser';
export type Item = {
export interface Item {
icon: string;
iconColor: string;
title: string;
description?: string;
link?: { url: string; text: string };
done?: false;
getDateLabel: (language: string) => string;
};
release?: string;
tag?: string;
date: Date;
dateType: DateType;
}
export enum DateType {
RELEASE = 'Release Date',
DATE = 'Date',
}
interface Props {
items: Item[];
}
export function Timeline({ items }: Props): JSX.Element {
export default function Timeline({ items }: Props): JSX.Element {
const isBrowser = useIsBrowser();
return (
@@ -25,15 +30,21 @@ export function Timeline({ items }: Props): JSX.Element {
{items.map((item, index) => {
const isFirst = index === 0;
const isLast = index === items.length - 1;
const done = item.done ?? true;
const dateLabel = item.getDateLabel(isBrowser ? navigator.language : 'en-US');
const timelineIcon = done ? mdiCheckboxMarkedCircle : mdiCheckboxBlankCircle;
const cardIcon = item.icon;
const classNames: string[] = [];
if (isFirst) {
classNames.push('');
}
if (isLast) {
classNames.push('rounded rounded-b-full');
}
return (
<li key={index} className={`flex min-h-24 w-[700px] max-w-[90vw] ${done ? '' : 'italic'}`}>
<li key={index} className="flex min-h-24 w-[700px] max-w-[90vw]">
<div className="md:flex justify-start w-36 mr-8 items-center dark:text-immich-dark-primary text-immich-primary hidden">
{dateLabel}
{isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}
</div>
<div className={`${isFirst && 'relative top-[50%]'} ${isLast && 'relative bottom-[50%]'}`}>
<div
@@ -43,32 +54,33 @@ export function Timeline({ items }: Props): JSX.Element {
></div>
</div>
<div className="z-10 flex items-center bg-immich-primary dark:bg-immich-dark-primary border-2 border-solid rounded-full dark:text-black text-white relative top-[50%] left-[-3px] translate-y-[-50%] translate-x-[-50%] w-8 h-8 shadow-lg ">
{<Icon path={timelineIcon} size={1.25} />}
<Icon path={mdiCheckboxMarkedCircleOutline} size={1.25} />
</div>
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-row w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="flex flex-col flex-grow justify-between gap-2">
<div className="flex gap-2 items-center">
{cardIcon === 'immich' ? (
<img src="img/immich-logo.svg" height="30" />
) : (
<Icon path={cardIcon} size={1} color={item.iconColor} />
)}
<p className="m-0 mt-1 text-lg items-start flex gap-2 place-items-center content-center">
<span>{item.title}</span>
</p>
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</div>
<div className="flex flex-col justify-between place-items-end">
<section className=" dark:bg-immich-dark-gray bg-immich-gray dark:border-0 border-gray-200 border border-solid rounded-2xl flex flex-col w-full gap-2 p-4 md:ml-4 my-2 hover:bg-immich-primary/10 dark:hover:bg-immich-dark-primary/10 transition-all">
<div className="m-0 text-lg flex w-full items-center justify-between gap-2">
<p className="m-0 items-start flex gap-2">
<Icon path={item.icon} size={1} />
<span>{item.title}</span>
</p>
<span className="dark:text-immich-dark-primary text-immich-primary">
{item.link && (
<a href={item.link.url} target="_blank" rel="noopener">
[{item.link.text}]
{item.tag ? (
<a
href={`https://github.com/immich-app/immich/releases/tag/${item.tag}`}
target="_blank"
rel="noopener"
>
[{item.release ?? item.tag}]{' '}
</a>
) : (
item.release && <span>[{item.release}]</span>
)}
</span>
<div className="md:hidden text-sm text-right">{dateLabel}</div>
</div>
<div className="md:hidden text-xs">
{`${item.dateType} - ${isBrowser ? item.date.toLocaleDateString(navigator.language) : ''}`}
</div>
<p className="m-0 text-sm text-gray-600 dark:text-gray-300">{item.description}</p>
</section>
</li>
);

View File

@@ -15,10 +15,6 @@ button {
font-family: 'Overpass', sans-serif;
}
img {
border-radius: 15px;
}
/* You can override the default Infima variables here. */
:root {
--ifm-color-primary: #4250af;
@@ -46,7 +42,7 @@ img {
}
div[class^='announcementBar_'] {
min-height: 2rem;
height: 2rem;
}
.navbar__brand .navbar__title {

File diff suppressed because it is too large Load Diff

View File

@@ -28,4 +28,3 @@
/docs/features/search /docs/features/smart-search 301
/docs/guides/api-album-sync /docs/community-projects 301
/docs/guides/remove-offline-files /docs/community-projects 301
/milestones /roadmap 301

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@@ -1 +1 @@
20.14
20.13

139
e2e/package-lock.json generated
View File

@@ -88,7 +88,7 @@
"@oazapfts/runtime": "^1.0.2"
},
"devDependencies": {
"@types/node": "^20.12.13",
"@types/node": "^20.11.0",
"typescript": "^5.3.3"
}
},
@@ -1230,9 +1230,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "20.12.13",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.13.tgz",
"integrity": "sha512-gBGeanV41c1L171rR7wjbMiEpEI/l5XFQdLLfhr/REwpgDy/4U8y89+i8kRiLzDyZdOkXh+cRaTetUnCYutoXA==",
"version": "20.12.12",
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.12.tgz",
"integrity": "sha512-eWLDGF/FOSPtAvEqeRAQ4C8LSA7M1I7i0ky1I8U7kD1J5ITyW3AsRhQrKVoWf5pFKZ2kILsEGJhsI9r93PYnOw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1344,17 +1344,17 @@
}
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.11.0.tgz",
"integrity": "sha512-P+qEahbgeHW4JQ/87FuItjBj8O3MYv5gELDzr8QaQ7fsll1gSMTYb6j87MYyxwf3DtD7uGFB9ShwgmCJB5KmaQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.9.0.tgz",
"integrity": "sha512-6e+X0X3sFe/G/54aC3jt0txuMTURqLyekmEHViqyA2VnxhLMpvA6nqmcjIy+Cr9tLDHPssA74BP5Mx9HQIxBEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/type-utils": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/type-utils": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"graphemer": "^1.4.0",
"ignore": "^5.3.1",
"natural-compare": "^1.4.0",
@@ -1378,16 +1378,16 @@
}
},
"node_modules/@typescript-eslint/parser": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.11.0.tgz",
"integrity": "sha512-yimw99teuaXVWsBcPO1Ais02kwJ1jmNA1KxE7ng0aT7ndr1pT1wqj0OJnsYVGKKlc4QJai86l/025L6z8CljOg==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.9.0.tgz",
"integrity": "sha512-qHMJfkL5qvgQB2aLvhUSXxbK7OLnDkwPzFalg458pxQgfxKDfT1ZDbHQM/I6mDIf/svlMkj21kzKuQ2ixJlatQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4"
},
"engines": {
@@ -1407,14 +1407,14 @@
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.11.0.tgz",
"integrity": "sha512-27tGdVEiutD4POirLZX4YzT180vevUURJl4wJGmm6TrQoiYwuxTIY98PBp6L2oN+JQxzE0URvYlzJaBHIekXAw==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.9.0.tgz",
"integrity": "sha512-ZwPK4DeCDxr3GJltRz5iZejPFAAr4Wk3+2WIBaj1L5PYK5RgxExu/Y68FFVclN0y6GGwH8q+KgKRCvaTmFBbgQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0"
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1425,14 +1425,14 @@
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.11.0.tgz",
"integrity": "sha512-WmppUEgYy+y1NTseNMJ6mCFxt03/7jTOy08bcg7bxJJdsM4nuhnchyBbE8vryveaJUf62noH7LodPSo5Z0WUCg==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.9.0.tgz",
"integrity": "sha512-6Qy8dfut0PFrFRAZsGzuLoM4hre4gjzWJB6sUvdunCYZsYemTkzZNwF1rnGea326PHPT3zn5Lmg32M/xfJfByA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/typescript-estree": "7.11.0",
"@typescript-eslint/utils": "7.11.0",
"@typescript-eslint/typescript-estree": "7.9.0",
"@typescript-eslint/utils": "7.9.0",
"debug": "^4.3.4",
"ts-api-utils": "^1.3.0"
},
@@ -1453,9 +1453,9 @@
}
},
"node_modules/@typescript-eslint/types": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.11.0.tgz",
"integrity": "sha512-MPEsDRZTyCiXkD4vd3zywDCifi7tatc4K37KqTprCvaXptP7Xlpdw0NR2hRJTetG5TxbWDB79Ys4kLmHliEo/w==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.9.0.tgz",
"integrity": "sha512-oZQD9HEWQanl9UfsbGVcZ2cGaR0YT5476xfWE0oE5kQa2sNK2frxOlkeacLOTh9po4AlUT5rtkGyYM5kew0z5w==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1467,14 +1467,14 @@
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.11.0.tgz",
"integrity": "sha512-cxkhZ2C/iyi3/6U9EPc5y+a6csqHItndvN/CzbNXTNrsC3/ASoYQZEt9uMaEp+xFNjasqQyszp5TumAVKKvJeQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.9.0.tgz",
"integrity": "sha512-zBCMCkrb2YjpKV3LA0ZJubtKCDxLttxfdGmwZvTqqWevUPN0FZvSI26FalGFFUZU/9YQK/A4xcQF9o/VVaCKAg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/visitor-keys": "7.11.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/visitor-keys": "7.9.0",
"debug": "^4.3.4",
"globby": "^11.1.0",
"is-glob": "^4.0.3",
@@ -1522,16 +1522,16 @@
}
},
"node_modules/@typescript-eslint/utils": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.11.0.tgz",
"integrity": "sha512-xlAWwPleNRHwF37AhrZurOxA1wyXowW4PqVXZVUNCLjB48CqdPJoJWkrpH2nij9Q3Lb7rtWindtoXwxjxlKKCA==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.9.0.tgz",
"integrity": "sha512-5KVRQCzZajmT4Ep+NEgjXCvjuypVvYHUW7RHlXzNPuak2oWpVoD1jf5xCP0dPAuNIchjC7uQyvbdaSTFaLqSdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.4.0",
"@typescript-eslint/scope-manager": "7.11.0",
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/typescript-estree": "7.11.0"
"@typescript-eslint/scope-manager": "7.9.0",
"@typescript-eslint/types": "7.9.0",
"@typescript-eslint/typescript-estree": "7.9.0"
},
"engines": {
"node": "^18.18.0 || >=20.0.0"
@@ -1545,13 +1545,13 @@
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "7.11.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.11.0.tgz",
"integrity": "sha512-7syYk4MzjxTEk0g/w3iqtgxnFQspDJfn6QKD36xMuuhTzjcxY7F8EmBLnALjVyaOF1/bVocu3bS/2/F7rXrveQ==",
"version": "7.9.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.9.0.tgz",
"integrity": "sha512-iESPx2TNLDNGQLyjKhUvIKprlP49XNEK+MvIf9nIO7ZZaZdbnfWKHnXAgufpxqfA0YryH8XToi4+CjBgVnFTSQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "7.11.0",
"@typescript-eslint/types": "7.9.0",
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
@@ -1831,13 +1831,13 @@
}
},
"node_modules/braces": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
"dev": true,
"license": "MIT",
"dependencies": {
"fill-range": "^7.1.1"
"fill-range": "^7.0.1"
},
"engines": {
"node": ">=8"
@@ -2700,11 +2700,10 @@
}
},
"node_modules/exiftool-vendored": {
"version": "26.1.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.1.0.tgz",
"integrity": "sha512-Bhy2Ia86Agt3+PbJJhWeVMqJNXl74XJ0Oygef5F5uCL13fTxlmF8dECHiChyx8bBc3sxIw+2Q3ehWunJh3bs6w==",
"version": "26.0.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-26.0.0.tgz",
"integrity": "sha512-2TRxx21ovD95VvdSzHb/sTYYcwhiizQIhhVAbrgua9KoL902QRieREGvaUtfBZNjsptdjonuyku2kUBJCPqsgw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@photostructure/tz-lookup": "^10.0.0",
"@types/luxon": "^3.4.2",
@@ -2713,27 +2712,25 @@
"luxon": "^3.4.4"
},
"optionalDependencies": {
"exiftool-vendored.exe": "12.85.0",
"exiftool-vendored.pl": "12.85.0"
"exiftool-vendored.exe": "12.84.0",
"exiftool-vendored.pl": "12.84.0"
}
},
"node_modules/exiftool-vendored.exe": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.85.0.tgz",
"integrity": "sha512-rWsKVp9oXsS79S3bfCNXKeEo4av0xcd7slk/TfPpCa5pojg8ZVXSVfPZMAAlhOuK63YXrKN/e3jRNReeGP+2Gw==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.exe/-/exiftool-vendored.exe-12.84.0.tgz",
"integrity": "sha512-9ocqJb0Pr9k0TownEMd75payF/XOQLF/swr/l0Ep49D+m609uIZsW09CtowhXmk1KrIFobS3+SkdXK04CSyUwQ==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/exiftool-vendored.pl": {
"version": "12.85.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.85.0.tgz",
"integrity": "sha512-AelZQCCfl0a0g7PYx90TqbNGlSu2zDbRfCTjGw6bBBYnJF0NUfUWVhTpa8XGe2lHx1KYikH8AkJaey3esAxMAg==",
"version": "12.84.0",
"resolved": "https://registry.npmjs.org/exiftool-vendored.pl/-/exiftool-vendored.pl-12.84.0.tgz",
"integrity": "sha512-TxvMRaVYtd24Vupn48zy24LOYItIIWEu4dgt/VlqLwxQItTpvJTV9YH04iZRvaNh9ZdPRgVKWMuuUDBBHv+lAg==",
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"!win32"
@@ -2821,9 +2818,9 @@
}
},
"node_modules/fill-range": {
"version": "7.1.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3633,13 +3630,13 @@
}
},
"node_modules/micromatch": {
"version": "4.0.7",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
"integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
"dev": true,
"license": "MIT",
"dependencies": {
"braces": "^3.0.3",
"braces": "^3.0.2",
"picomatch": "^2.3.1"
},
"engines": {

View File

@@ -47,6 +47,6 @@
"vitest": "^1.3.0"
},
"volta": {
"node": "20.14.0"
"node": "20.13.1"
}
}

View File

@@ -2,7 +2,7 @@ import {
ActivityCreateDto,
AlbumResponseDto,
AlbumUserRole,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
ReactionType,
createActivity as create,
@@ -17,7 +17,7 @@ import { beforeAll, beforeEach, describe, expect, it } from 'vitest';
describe('/activities', () => {
let admin: LoginResponseDto;
let nonOwner: LoginResponseDto;
let asset: AssetMediaResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
const createActivity = (dto: ActivityCreateDto, accessToken?: string) =>

View File

@@ -2,7 +2,7 @@ import {
addAssetsToAlbum,
AlbumResponseDto,
AlbumUserRole,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
AssetOrder,
deleteUserAdmin,
getAlbumInfo,
@@ -26,8 +26,8 @@ const user2NotShared = 'user2NotShared';
describe('/albums', () => {
let admin: LoginResponseDto;
let user1: LoginResponseDto;
let user1Asset1: AssetMediaResponseDto;
let user1Asset2: AssetMediaResponseDto;
let user1Asset1: AssetFileUploadResponseDto;
let user1Asset2: AssetFileUploadResponseDto;
let user1Albums: AlbumResponseDto[];
let user2: LoginResponseDto;
let user2Albums: AlbumResponseDto[];

View File

@@ -1,6 +1,5 @@
import {
AssetMediaResponseDto,
AssetMediaStatus,
AssetFileUploadResponseDto,
AssetResponseDto,
AssetTypeEnum,
LoginResponseDto,
@@ -68,10 +67,10 @@ describe('/asset', () => {
let statsUser: LoginResponseDto;
let stackUser: LoginResponseDto;
let user1Assets: AssetMediaResponseDto[];
let user2Assets: AssetMediaResponseDto[];
let stackAssets: AssetMediaResponseDto[];
let locationAsset: AssetMediaResponseDto;
let user1Assets: AssetFileUploadResponseDto[];
let user2Assets: AssetFileUploadResponseDto[];
let stackAssets: AssetFileUploadResponseDto[];
let locationAsset: AssetFileUploadResponseDto;
const setupTests = async () => {
await utils.resetDatabase();
@@ -122,7 +121,7 @@ describe('/asset', () => {
]);
for (const asset of [...user1Assets, ...user2Assets]) {
expect(asset.status).toBe(AssetMediaStatus.Created);
expect(asset.duplicate).toBe(false);
}
await Promise.all([
@@ -165,34 +164,16 @@ describe('/asset', () => {
utils.disconnectWebsocket(websocket);
});
describe('GET /assets/:id/original', () => {
describe('GET /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}/original`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download the file', async () => {
const response = await request(app)
.get(`/assets/${user1Assets[0].id}/original`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/png');
});
});
describe('GET /assets/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${uuidDto.notFound}`);
const { status, body } = await request(app).get(`/asset/${uuidDto.notFound}`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.get(`/assets/${uuidDto.invalid}`)
.get(`/asset/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -200,7 +181,7 @@ describe('/asset', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.get(`/assets/${user2Assets[0].id}`)
.get(`/asset/${user2Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
@@ -208,7 +189,7 @@ describe('/asset', () => {
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/assets/${user1Assets[0].id}`)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
@@ -220,29 +201,32 @@ describe('/asset', () => {
assetIds: [user1Assets[0].id],
});
const { status, body } = await request(app).get(`/assets/${user1Assets[0].id}?key=${sharedLink.key}`);
const { status, body } = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
});
it('should not send people data for shared links for un-authenticated users', async () => {
const { status, body } = await request(app)
.get(`/assets/${user1Assets[0].id}`)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toEqual(200);
expect(body).toMatchObject({
id: user1Assets[0].id,
isFavorite: false,
people: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '/my/awesome/thumbnail.jpg',
},
],
people: {
visiblePeople: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '/my/awesome/thumbnail.jpg',
},
],
numberOfFaces: 1,
},
});
const sharedLink = await utils.createSharedLink(user1.accessToken, {
@@ -250,15 +234,15 @@ describe('/asset', () => {
assetIds: [user1Assets[0].id],
});
const data = await request(app).get(`/assets/${user1Assets[0].id}?key=${sharedLink.key}`);
const data = await request(app).get(`/asset/${user1Assets[0].id}?key=${sharedLink.key}`);
expect(data.status).toBe(200);
expect(data.body).toMatchObject({ people: [] });
expect(data.body).not.toHaveProperty('people');
});
describe('partner assets', () => {
it('should get the asset info', async () => {
const { status, body } = await request(app)
.get(`/assets/${user1Assets[0].id}`)
.get(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ id: user1Assets[0].id });
@@ -268,7 +252,7 @@ describe('/asset', () => {
const asset = await utils.createAsset(user1.accessToken, { isArchived: true });
const { status } = await request(app)
.get(`/assets/${asset.id}`)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
@@ -278,16 +262,16 @@ describe('/asset', () => {
await utils.deleteAssets(user1.accessToken, [asset.id]);
const { status } = await request(app)
.get(`/assets/${asset.id}`)
.get(`/asset/${asset.id}`)
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(400);
});
});
});
describe('GET /assets/statistics', () => {
describe('GET /asset/statistics', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/statistics');
const { status, body } = await request(app).get('/asset/statistics');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -295,7 +279,7 @@ describe('/asset', () => {
it('should return stats of all assets', async () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.get('/asset/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`);
expect(body).toEqual({ images: 3, videos: 1, total: 4 });
@@ -304,7 +288,7 @@ describe('/asset', () => {
it('should return stats of all favored assets', async () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.get('/asset/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true });
@@ -314,7 +298,7 @@ describe('/asset', () => {
it('should return stats of all archived assets', async () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.get('/asset/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isArchived: true });
@@ -324,7 +308,7 @@ describe('/asset', () => {
it('should return stats of all favored and archived assets', async () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.get('/asset/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: true, isArchived: true });
@@ -334,7 +318,7 @@ describe('/asset', () => {
it('should return stats of all assets neither favored nor archived', async () => {
const { status, body } = await request(app)
.get('/assets/statistics')
.get('/asset/statistics')
.set('Authorization', `Bearer ${statsUser.accessToken}`)
.query({ isFavorite: false, isArchived: false });
@@ -343,7 +327,7 @@ describe('/asset', () => {
});
});
describe('GET /assets/random', () => {
describe('GET /asset/random', () => {
beforeAll(async () => {
await Promise.all([
utils.createAsset(user1.accessToken),
@@ -356,7 +340,7 @@ describe('/asset', () => {
});
it('should require authentication', async () => {
const { status, body } = await request(app).get('/assets/random');
const { status, body } = await request(app).get('/asset/random');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -364,7 +348,7 @@ describe('/asset', () => {
it.each(TEN_TIMES)('should return 1 random assets', async () => {
const { status, body } = await request(app)
.get('/assets/random')
.get('/asset/random')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -376,7 +360,7 @@ describe('/asset', () => {
it.each(TEN_TIMES)('should return 2 random assets', async () => {
const { status, body } = await request(app)
.get('/assets/random?count=2')
.get('/asset/random?count=2')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(200);
@@ -393,7 +377,7 @@ describe('/asset', () => {
'should return 1 asset if there are 10 assets in the database but user 2 only has 1',
async () => {
const { status, body } = await request(app)
.get('/assets/random')
.get('/asset/random')
.set('Authorization', `Bearer ${user2.accessToken}`);
expect(status).toBe(200);
@@ -403,23 +387,23 @@ describe('/asset', () => {
it('should return error', async () => {
const { status } = await request(app)
.get('/assets/random?count=ABC')
.get('/asset/random?count=ABC')
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
});
});
describe('PUT /assets/:id', () => {
describe('PUT /asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/assets/:${uuidDto.notFound}`);
const { status, body } = await request(app).put(`/asset/:${uuidDto.notFound}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put(`/assets/${uuidDto.invalid}`)
.put(`/asset/${uuidDto.invalid}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['id must be a UUID']));
@@ -427,7 +411,7 @@ describe('/asset', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put(`/assets/${user2Assets[0].id}`)
.put(`/asset/${user2Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.noPermission);
@@ -438,7 +422,7 @@ describe('/asset', () => {
expect(before.isFavorite).toBe(false);
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(body).toMatchObject({ id: user1Assets[0].id, isFavorite: true });
@@ -450,7 +434,7 @@ describe('/asset', () => {
expect(before.isArchived).toBe(false);
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isArchived: true });
expect(body).toMatchObject({ id: user1Assets[0].id, isArchived: true });
@@ -459,7 +443,7 @@ describe('/asset', () => {
it('should update date time original', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ dateTimeOriginal: '2023-11-19T18:11:00.000-07:00' });
@@ -486,7 +470,7 @@ describe('/asset', () => {
{ latitude: 12, longitude: 181 },
]) {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.send(test)
.set('Authorization', `Bearer ${user1.accessToken}`);
expect(status).toBe(400);
@@ -496,7 +480,7 @@ describe('/asset', () => {
it('should update gps data', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ latitude: 12, longitude: 12 });
@@ -509,7 +493,7 @@ describe('/asset', () => {
it('should set the description', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ description: 'Test asset description' });
expect(body).toMatchObject({
@@ -523,30 +507,33 @@ describe('/asset', () => {
it('should return tagged people', async () => {
const { status, body } = await request(app)
.put(`/assets/${user1Assets[0].id}`)
.put(`/asset/${user1Assets[0].id}`)
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ isFavorite: true });
expect(status).toEqual(200);
expect(body).toMatchObject({
id: user1Assets[0].id,
isFavorite: true,
people: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '/my/awesome/thumbnail.jpg',
},
],
people: {
visiblePeople: [
{
birthDate: null,
id: expect.any(String),
isHidden: false,
name: 'Test Person',
thumbnailPath: '/my/awesome/thumbnail.jpg',
},
],
numberOfFaces: 1,
},
});
});
});
describe('DELETE /assets', () => {
describe('DELETE /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.delete(`/asset`)
.send({ ids: [uuidDto.notFound] });
expect(status).toBe(401);
@@ -555,7 +542,7 @@ describe('/asset', () => {
it('should require a valid uuid', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.delete(`/asset`)
.send({ ids: [uuidDto.invalid] })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -565,7 +552,7 @@ describe('/asset', () => {
it('should throw an error when the id is not found', async () => {
const { status, body } = await request(app)
.delete(`/assets`)
.delete(`/asset`)
.send({ ids: [uuidDto.notFound] })
.set('Authorization', `Bearer ${admin.accessToken}`);
@@ -580,7 +567,7 @@ describe('/asset', () => {
expect(before.isTrashed).toBe(false);
const { status } = await request(app)
.delete('/assets')
.delete('/asset')
.send({ ids: [assetId] })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(204);
@@ -590,9 +577,9 @@ describe('/asset', () => {
});
});
describe('GET /assets/:id/thumbnail', () => {
describe('GET /asset/thumbnail/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/thumbnail`);
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -605,7 +592,7 @@ describe('/asset', () => {
});
const { status, body, type } = await request(app)
.get(`/assets/${locationAsset.id}/thumbnail?format=WEBP`)
.get(`/asset/thumbnail/${locationAsset.id}?format=WEBP`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -617,9 +604,9 @@ describe('/asset', () => {
expect(exifData).not.toHaveProperty('GPSLatitude');
});
it('should not include gps data for jpeg previews', async () => {
it('should not include gps data for jpeg thumbnails', async () => {
const { status, body, type } = await request(app)
.get(`/assets/${locationAsset.id}/thumbnail?size=preview`)
.get(`/asset/thumbnail/${locationAsset.id}?format=JPEG`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -632,9 +619,9 @@ describe('/asset', () => {
});
});
describe('GET /assets/:id/original', () => {
describe('GET /asset/file/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get(`/assets/${locationAsset.id}/original`);
const { status, body } = await request(app).get(`/asset/thumbnail/${locationAsset.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -642,7 +629,7 @@ describe('/asset', () => {
it('should download the original', async () => {
const { status, body, type } = await request(app)
.get(`/assets/${locationAsset.id}/original`)
.get(`/asset/file/${locationAsset.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
@@ -660,9 +647,98 @@ describe('/asset', () => {
});
});
describe('PUT /assets', () => {
describe('GET /asset/map-marker', () => {
beforeAll(async () => {
const files = [
'formats/avif/8bit-sRGB.avif',
'formats/jpg/el_torcal_rocks.jpg',
'formats/jxl/8bit-sRGB.jxl',
'formats/heic/IMG_2682.heic',
'formats/png/density_plot.png',
'formats/raw/Nikon/D80/glarus.nef',
'formats/raw/Nikon/D700/philadelphia.nef',
'formats/raw/Panasonic/DMC-GH4/4_3.rw2',
'formats/raw/Sony/ILCE-6300/12bit-compressed-(3_2).arw',
'formats/raw/Sony/ILCE-7M2/14bit-uncompressed-(3_2).arw',
];
utils.resetEvents();
const uploadFile = async (input: string) => {
const filepath = join(testAssetDir, input);
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
};
const uploads = files.map((f) => uploadFile(f));
await Promise.all(uploads);
}, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).put('/assets');
const { status, body } = await request(app).get('/asset/map-marker');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/asset/map-marker')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('PUT /asset', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/asset');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -670,7 +746,7 @@ describe('/asset', () => {
it('should require a valid parent id', async () => {
const { status, body } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] });
@@ -680,7 +756,7 @@ describe('/asset', () => {
it('should require access to the parent', async () => {
const { status, body } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] });
@@ -690,7 +766,7 @@ describe('/asset', () => {
it('should add stack children', async () => {
const { status } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] });
@@ -703,7 +779,7 @@ describe('/asset', () => {
it('should remove stack children', async () => {
const { status } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[1].id] });
@@ -721,7 +797,7 @@ describe('/asset', () => {
it('should remove all stack children', async () => {
const { status } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] });
@@ -739,7 +815,7 @@ describe('/asset', () => {
);
const { status } = await request(app)
.put('/assets')
.put('/asset')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] });
@@ -757,9 +833,9 @@ describe('/asset', () => {
});
});
describe('PUT /assets/stack/parent', () => {
describe('PUT /asset/stack/parent', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put('/assets/stack/parent');
const { status, body } = await request(app).put('/asset/stack/parent');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
@@ -767,7 +843,7 @@ describe('/asset', () => {
it('should require a valid id', async () => {
const { status, body } = await request(app)
.put('/assets/stack/parent')
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid });
@@ -777,7 +853,7 @@ describe('/asset', () => {
it('should require access', async () => {
const { status, body } = await request(app)
.put('/assets/stack/parent')
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${user1.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
@@ -787,7 +863,7 @@ describe('/asset', () => {
it('should make old parent child of new parent', async () => {
const { status } = await request(app)
.put('/assets/stack/parent')
.put('/asset/stack/parent')
.set('Authorization', `Bearer ${stackUser.accessToken}`)
.send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id });
@@ -806,11 +882,11 @@ describe('/asset', () => {
);
});
});
describe('POST /assets', () => {
describe('POST /asset/upload', () => {
beforeAll(setupTests, 30_000);
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/assets`);
const { status, body } = await request(app).post(`/asset/upload`);
expect(body).toEqual(errorDto.unauthorized);
expect(status).toBe(401);
});
@@ -826,7 +902,7 @@ describe('/asset', () => {
{ should: 'throw if `isArchived` is not a boolean', dto: { ...makeUploadDto(), isArchived: 'not-a-boolean' } },
])('should $should', async ({ dto }) => {
const { status, body } = await request(app)
.post('/assets')
.post('/asset/upload')
.set('Authorization', `Bearer ${user1.accessToken}`)
.attach('assetData', makeRandomImage(), 'example.png')
.field(dto);
@@ -1052,11 +1128,11 @@ describe('/asset', () => {
},
])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => {
const filepath = join(testAssetDir, input);
const { id, status } = await utils.createAsset(admin.accessToken, {
const { id, duplicate } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
expect(status).toBe(AssetMediaStatus.Created);
expect(duplicate).toBe(false);
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id });
@@ -1069,19 +1145,19 @@ describe('/asset', () => {
it('should handle a duplicate', async () => {
const filepath = 'formats/jpeg/el_torcal_rocks.jpeg';
const { status } = await utils.createAsset(admin.accessToken, {
const { duplicate } = await utils.createAsset(admin.accessToken, {
assetData: {
bytes: await readFile(join(testAssetDir, filepath)),
filename: basename(filepath),
},
});
expect(status).toBe(AssetMediaStatus.Duplicate);
expect(duplicate).toBe(true);
});
it('should update the used quota', async () => {
const { body, status } = await request(app)
.post('/assets')
.post('/asset/upload')
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
@@ -1089,7 +1165,7 @@ describe('/asset', () => {
.field('fileModifiedAt', new Date().toISOString())
.attach('assetData', makeRandomImage(), 'example.jpg');
expect(body).toEqual({ id: expect.any(String), status: AssetMediaStatus.Created });
expect(body).toEqual({ id: expect.any(String), duplicate: false });
expect(status).toBe(201);
const user = await getMyUser({ headers: asBearerAuth(quotaUser.accessToken) });
@@ -1099,7 +1175,7 @@ describe('/asset', () => {
it('should not upload an asset if it would exceed the quota', async () => {
const { body, status } = await request(app)
.post('/assets')
.post('/asset/upload')
.set('Authorization', `Bearer ${quotaUser.accessToken}`)
.field('deviceAssetId', 'example-image')
.field('deviceId', 'e2e')
@@ -1139,7 +1215,7 @@ describe('/asset', () => {
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: response.id });
expect(response.status).toBe(AssetMediaStatus.Created);
expect(response.duplicate).toBe(false);
const asset = await utils.getAssetInfo(admin.accessToken, response.id);
expect(asset.livePhotoVideoId).toBeDefined();

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto } from '@immich/sdk';
import { readFile, writeFile } from 'node:fs/promises';
import { errorDto } from 'src/responses';
import { app, tempDir, utils } from 'src/utils';
@@ -7,8 +7,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/download', () => {
let admin: LoginResponseDto;
let asset1: AssetMediaResponseDto;
let asset2: AssetMediaResponseDto;
let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
@@ -73,4 +73,22 @@ describe('/download', () => {
}
});
});
describe('POST /download/asset/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).post(`/download/asset/${asset1.id}`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should download file', async () => {
const response = await request(app)
.post(`/download/asset/${asset1.id}`)
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(response.status).toBe(200);
expect(response.headers['content-type']).toEqual('image/png');
});
});
});

View File

@@ -1,162 +0,0 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { readFile } from 'node:fs/promises';
import { basename, join } from 'node:path';
import { Socket } from 'socket.io-client';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, testAssetDir, utils } from 'src/utils';
import request from 'supertest';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
describe('/map', () => {
let websocket: Socket;
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let asset: AssetMediaResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup({ onboarding: false });
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
websocket = await utils.connectWebsocket(admin.accessToken);
asset = await utils.createAsset(admin.accessToken);
const files = ['formats/heic/IMG_2682.heic', 'metadata/gps-position/thompson-springs.jpg'];
utils.resetEvents();
const uploadFile = async (input: string) => {
const filepath = join(testAssetDir, input);
const { id } = await utils.createAsset(admin.accessToken, {
assetData: { bytes: await readFile(filepath), filename: basename(filepath) },
});
await utils.waitForWebsocketEvent({ event: 'assetUpload', id });
};
await Promise.all(files.map((f) => uploadFile(f)));
});
afterAll(() => {
utils.disconnectWebsocket(websocket);
});
describe('GET /map/markers', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/markers');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
// TODO archive one of these assets
it('should get map markers for all non-archived assets', async () => {
const { status, body } = await request(app)
.get('/map/markers')
.query({ isArchived: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toHaveLength(2);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
// TODO archive one of these assets
it('should get all map markers', async () => {
const { status, body } = await request(app)
.get('/map/markers')
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual([
{
city: 'Palisade',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(39.115),
lon: expect.closeTo(-108.400_968),
state: 'Colorado',
},
{
city: 'Ralston',
country: 'United States of America',
id: expect.any(String),
lat: expect.closeTo(41.2203),
lon: expect.closeTo(-96.071_625),
state: 'Nebraska',
},
]);
});
});
describe('GET /map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should allow shared link access', async () => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
const { status, body } = await request(app).get(`/map/style.json?key=${sharedLink.key}`).query({ theme: 'dark' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should throw an error if a theme is not light or dark', async () => {
for (const theme of ['dark1', true, 123, '', null, undefined]) {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});
it('should return the light style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'light' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
});
it('should return the dark style.json', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should not require admin authentication', async () => {
const { status, body } = await request(app)
.get('/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
});
});

View File

@@ -1,5 +1,5 @@
import {
AssetMediaResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
MemoryResponseDto,
MemoryType,
@@ -15,9 +15,9 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/memories', () => {
let admin: LoginResponseDto;
let user: LoginResponseDto;
let adminAsset: AssetMediaResponseDto;
let userAsset1: AssetMediaResponseDto;
let userAsset2: AssetMediaResponseDto;
let adminAsset: AssetFileUploadResponseDto;
let userAsset1: AssetFileUploadResponseDto;
let userAsset2: AssetFileUploadResponseDto;
let userMemory: MemoryResponseDto;
beforeAll(async () => {

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, deleteAssets, getMapMarkers, updateAsset } from '@immich/sdk';
import { DateTime } from 'luxon';
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';
@@ -13,25 +13,25 @@ describe('/search', () => {
let admin: LoginResponseDto;
let websocket: Socket;
let assetFalcon: AssetMediaResponseDto;
let assetDenali: AssetMediaResponseDto;
let assetCyclamen: AssetMediaResponseDto;
let assetNotocactus: AssetMediaResponseDto;
let assetSilver: AssetMediaResponseDto;
let assetDensity: AssetMediaResponseDto;
// let assetPhiladelphia: AssetMediaResponseDto;
// let assetOrychophragmus: AssetMediaResponseDto;
// let assetRidge: AssetMediaResponseDto;
// let assetPolemonium: AssetMediaResponseDto;
// let assetWood: AssetMediaResponseDto;
// let assetGlarus: AssetMediaResponseDto;
let assetHeic: AssetMediaResponseDto;
let assetRocks: AssetMediaResponseDto;
let assetOneJpg6: AssetMediaResponseDto;
let assetOneHeic6: AssetMediaResponseDto;
let assetOneJpg5: AssetMediaResponseDto;
let assetSprings: AssetMediaResponseDto;
let assetLast: AssetMediaResponseDto;
let assetFalcon: AssetFileUploadResponseDto;
let assetDenali: AssetFileUploadResponseDto;
let assetCyclamen: AssetFileUploadResponseDto;
let assetNotocactus: AssetFileUploadResponseDto;
let assetSilver: AssetFileUploadResponseDto;
let assetDensity: AssetFileUploadResponseDto;
// let assetPhiladelphia: AssetFileUploadResponseDto;
// let assetOrychophragmus: AssetFileUploadResponseDto;
// let assetRidge: AssetFileUploadResponseDto;
// let assetPolemonium: AssetFileUploadResponseDto;
// let assetWood: AssetFileUploadResponseDto;
// let assetGlarus: AssetFileUploadResponseDto;
let assetHeic: AssetFileUploadResponseDto;
let assetRocks: AssetFileUploadResponseDto;
let assetOneJpg6: AssetFileUploadResponseDto;
let assetOneHeic6: AssetFileUploadResponseDto;
let assetOneJpg5: AssetFileUploadResponseDto;
let assetSprings: AssetFileUploadResponseDto;
let assetLast: AssetFileUploadResponseDto;
let cities: string[];
let states: string[];
let countries: string[];
@@ -66,7 +66,7 @@ describe('/search', () => {
// last asset
{ filename: '/albums/nature/wood_anemones.jpg' },
];
const assets: AssetMediaResponseDto[] = [];
const assets: AssetFileUploadResponseDto[] = [];
for (const { filename, dto } of files) {
const bytes = await readFile(join(testAssetDir, filename));
assets.push(
@@ -134,7 +134,7 @@ describe('/search', () => {
// assetWood,
] = assets;
assetLast = assets.at(-1) as AssetMediaResponseDto;
assetLast = assets.at(-1) as AssetFileUploadResponseDto;
await deleteAssets({ assetBulkDeleteDto: { ids: [assetSilver.id] } }, { headers: asBearerAuth(admin.accessToken) });

View File

@@ -1,6 +1,6 @@
import {
AlbumResponseDto,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
SharedLinkResponseDto,
SharedLinkType,
@@ -15,8 +15,8 @@ import { beforeAll, describe, expect, it } from 'vitest';
describe('/shared-links', () => {
let admin: LoginResponseDto;
let asset1: AssetMediaResponseDto;
let asset2: AssetMediaResponseDto;
let asset1: AssetFileUploadResponseDto;
let asset2: AssetFileUploadResponseDto;
let user1: LoginResponseDto;
let user2: LoginResponseDto;
let album: AlbumResponseDto;

View File

@@ -1,4 +1,5 @@
import { LoginResponseDto, getConfig } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, getConfig } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
import request from 'supertest';
@@ -8,10 +9,74 @@ const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAu
describe('/system-config', () => {
let admin: LoginResponseDto;
let nonAdmin: LoginResponseDto;
let asset: AssetFileUploadResponseDto;
beforeAll(async () => {
await utils.resetDatabase();
admin = await utils.adminSetup();
nonAdmin = await utils.userSetup(admin.accessToken, createUserDto.user1);
asset = await utils.createAsset(admin.accessToken);
});
describe('GET /system-config/map/style.json', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).get('/system-config/map/style.json');
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should allow shared link access', async () => {
const sharedLink = await utils.createSharedLink(admin.accessToken, {
type: SharedLinkType.Individual,
assetIds: [asset.id],
});
const { status, body } = await request(app)
.get(`/system-config/map/style.json?key=${sharedLink.key}`)
.query({ theme: 'dark' });
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should throw an error if a theme is not light or dark', async () => {
for (const theme of ['dark1', true, 123, '', null, undefined]) {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(400);
expect(body).toEqual(errorDto.badRequest(['theme must be one of the following values: light, dark']));
}
});
it('should return the light style.json', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'light' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-light' }));
});
it('should return the dark style.json', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
it('should not require admin authentication', async () => {
const { status, body } = await request(app)
.get('/system-config/map/style.json')
.query({ theme: 'dark' })
.set('Authorization', `Bearer ${nonAdmin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual(expect.objectContaining({ id: 'immich-map-dark' }));
});
});
describe('PUT /system-config', () => {

View File

@@ -1,4 +1,4 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType, TimeBucketSize } from '@immich/sdk';
import { DateTime } from 'luxon';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -19,7 +19,7 @@ describe('/timeline', () => {
let user: LoginResponseDto;
let timeBucketUser: LoginResponseDto;
let userAssets: AssetMediaResponseDto[];
let userAssets: AssetFileUploadResponseDto[];
beforeAll(async () => {
await utils.resetDatabase();

View File

@@ -1,11 +1,4 @@
import {
LoginResponseDto,
deleteUserAdmin,
getMyUser,
getUserAdmin,
getUserPreferencesAdmin,
login,
} from '@immich/sdk';
import { LoginResponseDto, deleteUserAdmin, getMyUser, getUserAdmin, login } from '@immich/sdk';
import { Socket } from 'socket.io-client';
import { createUserDto, uuidDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
@@ -110,7 +103,15 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'quotaSizeInBytes', 'shouldChangePassword', 'notify']) {
for (const key of [
'password',
'email',
'name',
'quotaSizeInBytes',
'shouldChangePassword',
'memoriesEnabled',
'notify',
]) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
@@ -138,6 +139,23 @@ describe('/admin/users', () => {
});
expect(status).toBe(201);
});
it('should create a user without memories enabled', async () => {
const { status, body } = await request(app)
.post(`/admin/users`)
.send({
email: 'no-memories@immich.cloud',
password: 'Password123',
name: 'No Memories',
memoriesEnabled: false,
})
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(body).toMatchObject({
email: 'no-memories@immich.cloud',
memoriesEnabled: false,
});
expect(status).toBe(201);
});
});
describe('PUT /admin/users/:id', () => {
@@ -155,7 +173,7 @@ describe('/admin/users', () => {
expect(body).toEqual(errorDto.forbidden);
});
for (const key of ['password', 'email', 'name', 'shouldChangePassword']) {
for (const key of ['password', 'email', 'name', 'shouldChangePassword', 'memoriesEnabled']) {
it(`should not allow null ${key}`, async () => {
const { status, body } = await request(app)
.put(`/admin/users/${uuidDto.notFound}`)
@@ -203,6 +221,22 @@ describe('/admin/users', () => {
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update memories enabled', async () => {
const before = await getUserAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}`)
.send({ memoriesEnabled: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
updatedAt: expect.anything(),
memoriesEnabled: false,
});
expect(before.updatedAt).not.toEqual(body.updatedAt);
});
it('should update password', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${nonAdmin.userId}`)
@@ -220,51 +254,6 @@ describe('/admin/users', () => {
});
});
describe('PUT /admin/users/:id/preferences', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).put(`/admin/users/${userToDelete.userId}/preferences`);
expect(status).toBe(401);
expect(body).toEqual(errorDto.unauthorized);
});
it('should update memories enabled', async () => {
const before = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ memories: { enabled: false } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ memories: { enabled: false } });
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
it('should update the avatar color', async () => {
const { status, body } = await request(app)
.put(`/admin/users/${admin.userId}/preferences`)
.send({ avatar: { color: 'orange' } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
const after = await getUserPreferencesAdmin({ id: admin.userId }, { headers: asBearerAuth(admin.accessToken) });
expect(after).toEqual({
avatar: { color: 'orange' },
memories: { enabled: false },
emailNotifications: { enabled: true, albumInvite: true, albumUpdate: true },
});
});
});
describe('DELETE /admin/users/:id', () => {
it('should require authentication', async () => {
const { status, body } = await request(app).delete(`/admin/users/${userToDelete.userId}`);

View File

@@ -1,4 +1,4 @@
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyPreferences, getMyUser, login } from '@immich/sdk';
import { LoginResponseDto, SharedLinkType, deleteUserAdmin, getMyUser, login } from '@immich/sdk';
import { createUserDto } from 'src/fixtures';
import { errorDto } from 'src/responses';
import { app, asBearerAuth, utils } from 'src/utils';
@@ -69,6 +69,7 @@ describe('/users', () => {
expect(body).toMatchObject({
id: admin.userId,
email: 'admin@immich.cloud',
memoriesEnabled: true,
quotaUsageInBytes: 0,
});
});
@@ -81,7 +82,7 @@ describe('/users', () => {
expect(body).toEqual(errorDto.unauthorized);
});
for (const key of ['email', 'name']) {
for (const key of ['email', 'name', 'memoriesEnabled', 'avatarColor']) {
it(`should not allow null ${key}`, async () => {
const dto = { [key]: null };
const { status, body } = await request(app)
@@ -109,6 +110,24 @@ describe('/users', () => {
});
});
it('should update memories enabled', async () => {
const before = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
const { status, body } = await request(app)
.put(`/users/me`)
.send({ memoriesEnabled: false })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({
...before,
updatedAt: expect.anything(),
memoriesEnabled: false,
});
const after = await getMyUser({ headers: asBearerAuth(admin.accessToken) });
expect(after.memoriesEnabled).toBe(false);
});
/** @deprecated */
it('should allow a user to change their password (deprecated)', async () => {
const user = await getMyUser({ headers: asBearerAuth(nonAdmin.accessToken) });
@@ -157,24 +176,6 @@ describe('/users', () => {
});
});
describe('PUT /users/me/preferences', () => {
it('should update memories enabled', async () => {
const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(before).toMatchObject({ memories: { enabled: true } });
const { status, body } = await request(app)
.put(`/users/me/preferences`)
.send({ memories: { enabled: false } })
.set('Authorization', `Bearer ${admin.accessToken}`);
expect(status).toBe(200);
expect(body).toMatchObject({ memories: { enabled: false } });
const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) });
expect(after).toMatchObject({ memories: { enabled: false } });
});
});
describe('GET /users/:id', () => {
it('should require authentication', async () => {
const { status } = await request(app).get(`/users/${admin.userId}`);
@@ -193,6 +194,7 @@ describe('/users', () => {
expect(body).not.toMatchObject({
shouldChangePassword: expect.anything(),
memoriesEnabled: expect.anything(),
storageLabel: expect.anything(),
});
});

View File

@@ -1,3 +1,5 @@
import { UserAvatarColor } from '@immich/sdk';
export const uuidDto = {
invalid: 'invalid-uuid',
// valid uuid v4
@@ -68,6 +70,8 @@ export const userDto = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.Primary,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},
@@ -84,6 +88,8 @@ export const userDto = {
updatedAt: new Date('2021-01-01'),
tags: [],
assets: [],
memoriesEnabled: true,
avatarColor: UserAvatarColor.Primary,
quotaSizeInBytes: null,
quotaUsageInBytes: 0,
},

View File

@@ -68,6 +68,7 @@ export const signupResponseDto = {
updatedAt: expect.any(String),
deletedAt: null,
oauthId: '',
memoriesEnabled: true,
quotaUsageInBytes: 0,
quotaSizeInBytes: null,
status: 'active',

View File

@@ -1,9 +1,9 @@
import {
AllJobStatusResponseDto,
AssetMediaCreateDto,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
AssetResponseDto,
CreateAlbumDto,
CreateAssetDto,
CreateLibraryDto,
MetadataSearchDto,
PersonCreateDto,
@@ -292,7 +292,7 @@ export const utils = {
createAsset: async (
accessToken: string,
dto?: Partial<Omit<AssetMediaCreateDto, 'assetData'>> & { assetData?: AssetData },
dto?: Partial<Omit<CreateAssetDto, 'assetData'>> & { assetData?: AssetData },
) => {
const _dto = {
deviceAssetId: 'test-1',
@@ -310,7 +310,7 @@ export const utils = {
}
const builder = request(app)
.post(`/assets`)
.post(`/asset/upload`)
.attach('assetData', assetData, filename)
.set('Authorization', `Bearer ${accessToken}`);
@@ -320,7 +320,7 @@ export const utils = {
const { body } = await builder;
return body as AssetMediaResponseDto;
return body as AssetFileUploadResponseDto;
},
createImageFile: (path: string) => {

View File

@@ -1,10 +1,10 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Detail Panel', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();

View File

@@ -1,10 +1,10 @@
import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { AssetFileUploadResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk';
import { expect, test } from '@playwright/test';
import { utils } from 'src/utils';
test.describe('Asset Viewer Navbar', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
let asset: AssetFileUploadResponseDto;
test.beforeAll(async () => {
utils.initSdk();

View File

@@ -1,6 +1,6 @@
import {
AlbumResponseDto,
AssetMediaResponseDto,
AssetFileUploadResponseDto,
LoginResponseDto,
SharedLinkResponseDto,
SharedLinkType,
@@ -11,7 +11,7 @@ import { asBearerAuth, utils } from 'src/utils';
test.describe('Shared Links', () => {
let admin: LoginResponseDto;
let asset: AssetMediaResponseDto;
let asset: AssetFileUploadResponseDto;
let album: AlbumResponseDto;
let sharedLink: SharedLinkResponseDto;
let sharedLinkPassword: SharedLinkResponseDto;

View File

@@ -1,4 +1,4 @@
FROM mambaorg/micromamba:bookworm-slim@sha256:4688551ffd61358d5bebfd88e0aac12d5b4aed7a153c170dbc435da453476a13 as builder
FROM mambaorg/micromamba:bookworm-slim@sha256:d5b82811074b396275ef69aadbf31098257dd8836e231371e9cdb393128e571c as builder
ENV TRANSFORMERS_CACHE=/cache \
PYTHONDONTWRITEBYTECODE=1 \

View File

@@ -957,74 +957,74 @@ test = ["cffi (>=1.12.2)", "coverage (>=5.0)", "dnspython (>=1.16.0,<2.0)", "idn
[[package]]
name = "geventhttpclient"
version = "2.3.1"
version = "2.2.1"
description = "HTTP client library for gevent"
optional = false
python-versions = ">=3.9"
files = [
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da22ab7bf5af4ba3d07cffee6de448b42696e53e7ac1fe97ed289037733bf1c2"},
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2399e3d4e2fae8bbd91756189da6e9d84adf8f3eaace5eef0667874a705a29f8"},
{file = "geventhttpclient-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3e33e87d0d5b9f5782c4e6d3cb7e3592fea41af52713137d04776df7646d71b"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c071db313866c3d0510feb6c0f40ec086ccf7e4a845701b6316c82c06e8b9b29"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f36f0c6ef88a27e60af8369d9c2189fe372c6f2943182a7568e0f2ad33bb69f1"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4624843c03a5337282a42247d987c2531193e57255ee307b36eeb4f243a0c21"},
{file = "geventhttpclient-2.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d614573621ba827c417786057e1e20e9f96c4f6b3878c55b1b7b54e1026693bc"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5d51330a40ac9762879d0e296c279c1beae8cfa6484bb196ac829242c416b709"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:bc9f2162d4e8cb86bb5322d99bfd552088a3eacd540a841298f06bb8bc1f1f03"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:06e59d3397e63c65ecc7a7561a5289f0cf2e2c2252e29632741e792f57f5d124"},
{file = "geventhttpclient-2.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4436eef515b3e0c1d4a453ae32e047290e780a623c1eddb11026ae9d5fb03d42"},
{file = "geventhttpclient-2.3.1-cp310-cp310-win32.whl", hash = "sha256:5d1cf7d8a4f8e15cc8fd7d88ac4cdb058d6274203a42587e594cc9f0850ac862"},
{file = "geventhttpclient-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:4deaebc121036f7ea95430c2d0f80ab085b15280e6ab677a6360b70e57020e7f"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f0ae055b9ce1704f2ce72c0847df28f4e14dbb3eea79256cda6c909d82688ea3"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f087af2ac439495b5388841d6f3c4de8d2573ca9870593d78f7b554aa5cfa7f5"},
{file = "geventhttpclient-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:76c367d175810facfe56281e516c9a5a4a191eff76641faaa30aa33882ed4b2f"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a58376d0d461fe0322ff2ad362553b437daee1eeb92b4c0e3b1ffef9e77defbe"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f440cc704f8a9869848a109b2c401805c17c070539b2014e7b884ecfc8591e33"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f10c62994f9052f23948c19de930b2d1f063240462c8bd7077c2b3290e61f4fa"},
{file = "geventhttpclient-2.3.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:52c45d9f3dd9627844c12e9ca347258c7be585bed54046336220e25ea6eac155"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:77c1a2c6e3854bf87cd5588b95174640c8a881716bd07fa0d131d082270a6795"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ce649d4e25c2d56023471df0bf1e8e2ab67dfe4ff12ce3e8fe7e6fae30cd672a"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:265d9f31b4ac8f688eebef0bd4c814ffb37a16f769ad0c8c8b8c24a84db8eab5"},
{file = "geventhttpclient-2.3.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:2de436a9d61dae877e4e811fb3e2594e2a1df1b18f4280878f318aef48a562b9"},
{file = "geventhttpclient-2.3.1-cp311-cp311-win32.whl", hash = "sha256:83e22178b9480b0a95edf0053d4f30b717d0b696b3c262beabe6964d9c5224b1"},
{file = "geventhttpclient-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:97b072a282233384c1302a7dee88ad8bfedc916f06b1bc1da54f84980f1406a9"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e1c90abcc2735cd8dd2d2572a13da32f6625392dc04862decb5c6476a3ddee22"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5deb41c2f51247b4e568c14964f59d7b8e537eff51900564c88af3200004e678"},
{file = "geventhttpclient-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c6f1a56a66a90c4beae2f009b5e9d42db9a58ced165aa35441ace04d69cb7b37"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ee6e741849c29e3129b1ec3828ac3a5e5dcb043402f852ea92c52334fb8cabf"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0d0972096a63b1ddaa73fa3dab2c7a136e3ab8bf7999a2f85a5dee851fa77cdd"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:00675ba682fb7d19d659c14686fa8a52a65e3f301b56c2a4ee6333b380dd9467"},
{file = "geventhttpclient-2.3.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea77b67c186df90473416f4403839728f70ef6cf1689cec97b4f6bbde392a8a8"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ddcc3f0fdffd9a3801e1005b73026202cffed8199863fdef9315bea9a860a032"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c9f1ef4ec048563cc621a47ff01a4f10048ff8b676d7a4d75e5433ed8e703e56"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:a364b30bec7a0a00dbe256e2b6807e4dc866bead7ac84aaa51ca5e2c3d15c258"},
{file = "geventhttpclient-2.3.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:25d255383d3d6a6fbd643bb51ae1a7e4f6f7b0dbd5f3225b537d0bd0432eaf39"},
{file = "geventhttpclient-2.3.1-cp312-cp312-win32.whl", hash = "sha256:ad0b507e354d2f398186dcb12fe526d0594e7c9387b514fb843f7a14fdf1729a"},
{file = "geventhttpclient-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:7924e0883bc2b177cfe27aa65af6bb9dd57f3e26905c7675a2d1f3ef69df7cca"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fe912c6456faab196b952adcd63e9353a0d5c8deb31c8d733d38f4f0ab22e359"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8b599359779c2278018786c35d70664d441a7cd0d6baef2b2cd0d1685cf478ed"},
{file = "geventhttpclient-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:34107b506e2c40ec7784efa282469bf86888cacddced463dceeb58c201834897"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc34031905b2b31a80d88cd33d7e42b81812950e5304860ab6a65ee2803e2046"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50b54f67ba2087f4d9d2172065c5c5de0f0c7f865ac350116e5452de4be31444"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9ddeb431836c2ef7fd33c505a06180dc907b474e0e8537a43ff12e12c9bf0307"},
{file = "geventhttpclient-2.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4890713433ca19b081f70b5f7ad258a0979ec3354f9538b50b3ad7d0a86f88de"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b8ca7dcbe94cb563341087b00b6fbd0fdd70b2acc1b5d963f9ebbfbc1e5e2893"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:05a1bbdd43ae36bcc10b3dbfa0806aefc5033a91efecfddfe56159446a46ea71"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f82c454595a88a5e510ae0985711ef398386998b6f37d90fc30e9ff1a2001280"},
{file = "geventhttpclient-2.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b032a5cdb1721921f4cd36aad620af318263b462962cfb23d648cdb93aab232"},
{file = "geventhttpclient-2.3.1-cp39-cp39-win32.whl", hash = "sha256:ce2c7d18bac7ffdacc4a86cd490bea6136a7d1e1170f8624f2e3bbe3b189d5b8"},
{file = "geventhttpclient-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:6ca50dd9761971d3557b897108933b34fb4a11533d52f0f2753840c740a2861a"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c31431e38df45b3c79bf3c9427c796adb8263d622bc6fa25e2f6ba916c2aad93"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:855ab1e145575769b180b57accb0573a77cd6a7392f40a6ef7bc9a4926ebd77b"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a374aad77c01539e786d0c7829bec2eba034ccd45733c1bf9811ad18d2a8ecd"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66c1e97460608304f400485ac099736fff3566d3d8db2038533d466f8cf5de5a"},
{file = "geventhttpclient-2.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4f843f81ee44ba4c553a1b3f73115e0ad8f00044023c24db29f5b1df3da08465"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:321b73c73d73b85cfeff36b9b5ee04174ec8406fb3dadc129558a26ccb879360"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:829d03c2a140edbe74ad1fb4f850384f585f3e06fc47cfe647d065412b93926f"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:994c543f156db7bce3bae15491a0e041eeb3f1cf467e0d1db0c161a900a90bec"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4beff505306aa9da5cdfe2f206b403ec7c8d06a22d6b7248365772858c4ee8c"},
{file = "geventhttpclient-2.3.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fb0a9673074541ccda09a2423fa16f4528819ceb1ba19d252213f6aca7d4b44a"},
{file = "geventhttpclient-2.3.1.tar.gz", hash = "sha256:b40ddac8517c456818942c7812f555f84702105c82783238c9fcb8dc12675185"},
{file = "geventhttpclient-2.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:672c6b0239dc6651c02b54b5d3f67290af40fade700ee3ab48fc97f09c6a5dc6"},
{file = "geventhttpclient-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f11fda0645c406c250e01db97a3e2d2f804c7b50eb1432d1e00f37225bcc4598"},
{file = "geventhttpclient-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:34338eafa649a281d7f5453c3aaf88744137bbe099ad3ba157ae491cd88b96e0"},
{file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb1021556cc4329246a4493ad90ac8a55594c27c2b85093676dc937cf19d6de2"},
{file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:06c6cc714ce66f66e8f892575aecdbed2355afe4b39cb89d08eb8728b8523466"},
{file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df3788352d9ee10fa7c6cdfa45260e353e96466555e2a7d2ebcc394f607e0cce"},
{file = "geventhttpclient-2.2.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ebe1333f4f6b879f84576ac1aeacbe32a382716f05172f9aa38313bf1bbcf45"},
{file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:bcdb648301db9649d3a099d3f833919315ff34f26e47149f986b0ca2f5b0e186"},
{file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:81d6d5a6a0a93c0b7d395270d5d357bbcc4b4502ea2086e711869a65c0f9fc30"},
{file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:6af2fc621ea8c7aae6fa49c2204bd80050a0c56ea349011f3ebe2f36d8623ad4"},
{file = "geventhttpclient-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ff7bbc4b4b913631dbc6f23d3d3cbbf1d9b020181cbfa8a806e13ebb01e13219"},
{file = "geventhttpclient-2.2.1-cp310-cp310-win32.whl", hash = "sha256:cfa65f0c595ad2cf9f129f7cf18de076db4f72449fa8a6cc7f7cf554e5332832"},
{file = "geventhttpclient-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:f4e1ae7ad0bd7a00c679874652ea49a6352f91690c35ee0da45bf63114ad433b"},
{file = "geventhttpclient-2.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:438d3f8c2ba0a9a8b58d62f6ccd29bea468b41f71132f21eb9e8aff347e98c5d"},
{file = "geventhttpclient-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e22e108b64d20c8767b1e78ebe230d3f2af5805e80246d6aa2afd1dab4a6f19"},
{file = "geventhttpclient-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:164ec70971c915ea3716d4175d704c6cb0cb020a64eb6ea7f0a3277abd07f2fb"},
{file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83589b7708f40b1366616dab832fcefb3f486cf61c65dac9bf2fe3196850d34d"},
{file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d735d39b9c64fb79f01b36d47f38653f8988d441d6b7dbaedac3d4b45f0cd21"},
{file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41b56ae8a616fa237b45e1a7bc9c474441d7e69fb46a1fac4f6edc1d462454d9"},
{file = "geventhttpclient-2.2.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:034961b2fafcdf1f54895f37980aca5bafa8740dde39d2eacbacb4e0995b99a5"},
{file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:eec11a2e3501e0170f057f4e292a5715d57e3362fefa75f804302fc4bd916b38"},
{file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:7387571fa24608e40230bd60641bb811dd0565f77dd52b7b3249eecb9293d01a"},
{file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:f37e0f56ade9c308ef5f5359bcb9d69f8b6d6ee177f2e1965b5f75472dfb02f9"},
{file = "geventhttpclient-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bbcf295b8987114437215ed5b2980811a5d135ddcdc1258add64caee679de8c"},
{file = "geventhttpclient-2.2.1-cp311-cp311-win32.whl", hash = "sha256:44e206dea6c5d11287f4ad96dd807d4cd85f8aad1a243f7b0d87a90dc877bdcd"},
{file = "geventhttpclient-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:e5c55f3526bf3d9c47a6c4d789ad9cd224ed301740e15c1bdeb7bc067b38c7bf"},
{file = "geventhttpclient-2.2.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:86f0372314515cc49bd88a1d733db31f8d746f77790cd3e9fcb2bfadbf06bf01"},
{file = "geventhttpclient-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:2203442640dc0f2178be7b7a2ed285deffeda31c80045162a291292f1269cf8b"},
{file = "geventhttpclient-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:005e4798af49bd017c19c7272f87e05bfd72ba7ff876de5a3457026587c16c33"},
{file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4188f482cc7d970b7fe71e178199c853064c17c6bfa87a4f5f482bb2a2db3d2"},
{file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f805eab5012133aabab802fc1efc7a865226f534340ce2617439c3be4f10925f"},
{file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75f2fff7785887441c4f57aa6004a5edf545952db089f060655f77dacc2f8a9f"},
{file = "geventhttpclient-2.2.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c20f68942bea6789abe363a08abb8453017c6eda69bc69d9b6c52f166254375c"},
{file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d9ab6892e9b95a782a3af279f07e60ee4de98f94e0a9c78955c820a1e7bb821"},
{file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:69f71152c5ff9272c1d4ee653c0ba7357e2eada4c3af68ceaa3b866c0b7410e8"},
{file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:b173bc1d11ee2bef1d46f5159a23fa749f7c770b75127184aa855df976267a05"},
{file = "geventhttpclient-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7a771dfbaba83ba558d438e5e3ece49f04c683e3af510ad366f94502af7c5f4e"},
{file = "geventhttpclient-2.2.1-cp312-cp312-win32.whl", hash = "sha256:438ee39c11b83d737e6c8121467a0e72d2cabe8c5a3a8d432106a10c9c95df79"},
{file = "geventhttpclient-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:f125e37261e9cf1841cd3d81b196e051150d7fbbf74652aad40eafab08b19969"},
{file = "geventhttpclient-2.2.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:37030e799993c2576c30264b58e868e7de6bbd9ff6298dace713e7ba5c545346"},
{file = "geventhttpclient-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:19ab382d7f736fa87a0f417b3b2b67b4ce8a81fceda38d1e6344725907b9d405"},
{file = "geventhttpclient-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f0691aaeb87f3ad8337b3d862c2f74d8910a2762076adfd32940094eb10a267"},
{file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e51627d3690a8829199ac39197d081cb13bc866c8c7fe9d9c383517b4bbbbfb"},
{file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:01f4ebcd0cae416cab27092f65c6b5a8c6bc9d50e9447f6278c6261995fb6629"},
{file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9cb660559b292d7a1e3d22938d384cc3c534d356ca308f50d9c3801bfc404cb"},
{file = "geventhttpclient-2.2.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2eec345499bbdf9acccdbd08e9180ff93334bf339cb2b0250b57b6a74a742bd4"},
{file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e013cb4fcadbb5e9ef36cbd8774bc8b70ea09f9b4d2ec84b9f3e2b5a203e1bfa"},
{file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2144d1900db9f6b5d5560ecba2bba39922829d09dbebaa794ebb0ad9e4747618"},
{file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:8b30fdd201893a8ed7cfd98df23925623f0e731737e42050a5602d7ed038e55e"},
{file = "geventhttpclient-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ec607413b3ac1b62035c2bdf5e27d705c8d74a3ecd26851318380c66231909e2"},
{file = "geventhttpclient-2.2.1-cp39-cp39-win32.whl", hash = "sha256:a06342791b66e2c40b53e7d8ba0fad6b88704cc5e7dcf8d795bbe16e88f783c2"},
{file = "geventhttpclient-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:a77fc38028c6fb8d9f712f9589c20e8da275368daf81c3efb3019cc2056b18a4"},
{file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c078d03bc1edf2b484ef056312e132772cb9debd0cf0ac3f27144014b504228e"},
{file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45daaec4ab2b77861a0a81a8735bb82f2571b5035366323ffac9f80abd2973cd"},
{file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:89cd7dc244e8052d0de7ae345aa009739f1ae32bbd2a0668a422321824bcd8b9"},
{file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a4835f5486cdf84c64680bba49a59439a81fa9eb632e64c7e86956d074e56a7"},
{file = "geventhttpclient-2.2.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8685d152abecd58d9b546012b08a35d1ff0e37761039e817347960ef576fff68"},
{file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ff2f6b587e7834bebf8ced8be227372b11c24c5429615b9080e2d18401403329"},
{file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a4aa373c83d4724066e528d7526f46139e03299a474ff442cc50f3c802e6cc0f"},
{file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cd354a3f7fa6b1d6bd1c4875e8d35861cb5021fd475d5120e65462b85c546b8e"},
{file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d488c914aeae9c740c0a90203ebffa195fac0bfc974a284df4677f39fc0d4d9"},
{file = "geventhttpclient-2.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0102e761996967bb28689c068a73c009cda43fa80a54b26253198c734926d043"},
{file = "geventhttpclient-2.2.1.tar.gz", hash = "sha256:29f7e02683e3cd4f0032fba67364ff322e8504fddd170d9de5541bcfade85a50"},
]
[package.dependencies]
@@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"]
[[package]]
name = "huggingface-hub"
version = "0.23.2"
version = "0.23.0"
description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub"
optional = false
python-versions = ">=3.8.0"
files = [
{file = "huggingface_hub-0.23.2-py3-none-any.whl", hash = "sha256:48727a16e704d409c4bb5913613308499664f22a99743435dc3a13b23c485827"},
{file = "huggingface_hub-0.23.2.tar.gz", hash = "sha256:f6829b62d5fdecb452a76fdbec620cba4c1573655a8d710c1df71735fd9edbd2"},
{file = "huggingface_hub-0.23.0-py3-none-any.whl", hash = "sha256:075c30d48ee7db2bba779190dc526d2c11d422aed6f9044c5e2fdc2c432fdb91"},
{file = "huggingface_hub-0.23.0.tar.gz", hash = "sha256:7126dedd10a4c6fac796ced4d87a8cf004efc722a5125c2c09299017fa366fa9"},
]
[package.dependencies]
@@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
[[package]]
name = "locust"
version = "2.28.0"
description = "Developer-friendly load testing framework"
version = "2.27.0"
description = "Developer friendly load testing framework"
optional = false
python-versions = ">=3.9"
files = [
{file = "locust-2.28.0-py3-none-any.whl", hash = "sha256:766be879db030c0118e7d9fca712f3538c4e628bdebf59468fa1c6c2fab217d3"},
{file = "locust-2.28.0.tar.gz", hash = "sha256:260557eec866f7e34a767b6c916b5b278167562a280480aadb88f43d962fbdeb"},
{file = "locust-2.27.0-py3-none-any.whl", hash = "sha256:c4db5747eb9a3851216deae8147143d335db41978a9291ac32e113fa9ec8ad39"},
{file = "locust-2.27.0.tar.gz", hash = "sha256:0c6d3d2523976dafe734012c41b2f7d9ad7120cbcea76d47d80cec5d6d139905"},
]
[package.dependencies]
@@ -1545,7 +1545,7 @@ flask = ">=2.0.0"
Flask-Cors = ">=3.0.10"
Flask-Login = ">=0.6.3"
gevent = ">=22.10.2"
geventhttpclient = ">=2.3.1"
geventhttpclient = "2.2.1"
msgpack = ">=1.0.0"
psutil = ">=5.9.1"
pywin32 = {version = "*", markers = "platform_system == \"Windows\""}
@@ -1958,36 +1958,36 @@ reference = ["Pillow", "google-re2"]
[[package]]
name = "onnxruntime"
version = "1.18.0"
version = "1.17.3"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
{file = "onnxruntime-1.18.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:5a3b7993a5ecf4a90f35542a4757e29b2d653da3efe06cdd3164b91167bbe10d"},
{file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15b944623b2cdfe7f7945690bfb71c10a4531b51997c8320b84e7b0bb59af902"},
{file = "onnxruntime-1.18.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e61ce5005118064b1a0ed73ebe936bc773a102f067db34108ea6c64dd62a179"},
{file = "onnxruntime-1.18.0-cp310-cp310-win32.whl", hash = "sha256:a4fc8a2a526eb442317d280610936a9f73deece06c7d5a91e51570860802b93f"},
{file = "onnxruntime-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:71ed219b768cab004e5cd83e702590734f968679bf93aa488c1a7ffbe6e220c3"},
{file = "onnxruntime-1.18.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:3d24bd623872a72a7fe2f51c103e20fcca2acfa35d48f2accd6be1ec8633d960"},
{file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f15e41ca9b307a12550bfd2ec93f88905d9fba12bab7e578f05138ad0ae10d7b"},
{file = "onnxruntime-1.18.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f45ca2887f62a7b847d526965686b2923efa72538c89b7703c7b3fe970afd59"},
{file = "onnxruntime-1.18.0-cp311-cp311-win32.whl", hash = "sha256:9e24d9ecc8781323d9e2eeda019b4b24babc4d624e7d53f61b1fe1a929b0511a"},
{file = "onnxruntime-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:f8608398976ed18aef450d83777ff6f77d0b64eced1ed07a985e1a7db8ea3771"},
{file = "onnxruntime-1.18.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:f1d79941f15fc40b1ee67738b2ca26b23e0181bf0070b5fb2984f0988734698f"},
{file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99e8caf3a8565c853a22d323a3eebc2a81e3de7591981f085a4f74f7a60aab2d"},
{file = "onnxruntime-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:498d2b8380635f5e6ebc50ec1b45f181588927280f32390fb910301d234f97b8"},
{file = "onnxruntime-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ba7cc0ce2798a386c082aaa6289ff7e9bedc3dee622eef10e74830cff200a72e"},
{file = "onnxruntime-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:1fa175bd43f610465d5787ae06050c81f7ce09da2bf3e914eb282cb8eab363ef"},
{file = "onnxruntime-1.18.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:0284c579c20ec8b1b472dd190290a040cc68b6caec790edb960f065d15cf164a"},
{file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d47353d036d8c380558a5643ea5f7964d9d259d31c86865bad9162c3e916d1f6"},
{file = "onnxruntime-1.18.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:885509d2b9ba4b01f08f7fa28d31ee54b6477953451c7ccf124a84625f07c803"},
{file = "onnxruntime-1.18.0-cp38-cp38-win32.whl", hash = "sha256:8614733de3695656411d71fc2f39333170df5da6c7efd6072a59962c0bc7055c"},
{file = "onnxruntime-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:47af3f803752fce23ea790fd8d130a47b2b940629f03193f780818622e856e7a"},
{file = "onnxruntime-1.18.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:9153eb2b4d5bbab764d0aea17adadffcfc18d89b957ad191b1c3650b9930c59f"},
{file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c7fd86eca727c989bb8d9c5104f3c45f7ee45f445cc75579ebe55d6b99dfd7c"},
{file = "onnxruntime-1.18.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac67a4de9c1326c4d87bcbfb652c923039b8a2446bb28516219236bec3b494f5"},
{file = "onnxruntime-1.18.0-cp39-cp39-win32.whl", hash = "sha256:6ffb445816d06497df7a6dd424b20e0b2c39639e01e7fe210e247b82d15a23b9"},
{file = "onnxruntime-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:46de6031cb6745f33f7eca9e51ab73e8c66037fb7a3b6b4560887c5b55ab5d5d"},
{file = "onnxruntime-1.17.3-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:d86dde9c0bb435d709e51bd25991c9fe5b9a5b168df45ce119769edc4d198b15"},
{file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d87b68bf931ac527b2d3c094ead66bb4381bac4298b65f46c54fe4d1e255865"},
{file = "onnxruntime-1.17.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e950cf0333cf114a155f9142e71da344d2b08dfe202763a403ae81cc02ebd1"},
{file = "onnxruntime-1.17.3-cp310-cp310-win32.whl", hash = "sha256:0962a4d0f5acebf62e1f0bf69b6e0adf16649115d8de854c1460e79972324d68"},
{file = "onnxruntime-1.17.3-cp310-cp310-win_amd64.whl", hash = "sha256:468ccb8a0faa25c681a41787b1594bf4448b0252d3efc8b62fd8b2411754340f"},
{file = "onnxruntime-1.17.3-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e8cd90c1c17d13d47b89ab076471e07fb85467c01dcd87a8b8b5cdfbcb40aa51"},
{file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a058b39801baefe454eeb8acf3ada298c55a06a4896fafc224c02d79e9037f60"},
{file = "onnxruntime-1.17.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2f823d5eb4807007f3da7b27ca972263df6a1836e6f327384eb266274c53d05d"},
{file = "onnxruntime-1.17.3-cp311-cp311-win32.whl", hash = "sha256:b66b23f9109e78ff2791628627a26f65cd335dcc5fbd67ff60162733a2f7aded"},
{file = "onnxruntime-1.17.3-cp311-cp311-win_amd64.whl", hash = "sha256:570760ca53a74cdd751ee49f13de70d1384dcf73d9888b8deac0917023ccda6d"},
{file = "onnxruntime-1.17.3-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:77c318178d9c16e9beadd9a4070d8aaa9f57382c3f509b01709f0f010e583b99"},
{file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23da8469049b9759082e22c41a444f44a520a9c874b084711b6343672879f50b"},
{file = "onnxruntime-1.17.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2949730215af3f9289008b2e31e9bbef952012a77035b911c4977edea06f3f9e"},
{file = "onnxruntime-1.17.3-cp312-cp312-win32.whl", hash = "sha256:6c7555a49008f403fb3b19204671efb94187c5085976ae526cb625f6ede317bc"},
{file = "onnxruntime-1.17.3-cp312-cp312-win_amd64.whl", hash = "sha256:58672cf20293a1b8a277a5c6c55383359fcdf6119b2f14df6ce3b140f5001c39"},
{file = "onnxruntime-1.17.3-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:4395ba86e3c1e93c794a00619ef1aec597ab78f5a5039f3c6d2e9d0695c0a734"},
{file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdf354c04344ec38564fc22394e1fe08aa6d70d790df00159205a0055c4a4d3f"},
{file = "onnxruntime-1.17.3-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a94b600b7af50e922d44b95a57981e3e35103c6e3693241a03d3ca204740bbda"},
{file = "onnxruntime-1.17.3-cp38-cp38-win32.whl", hash = "sha256:5a335c76f9c002a8586c7f38bc20fe4b3725ced21f8ead835c3e4e507e42b2ab"},
{file = "onnxruntime-1.17.3-cp38-cp38-win_amd64.whl", hash = "sha256:8f56a86fbd0ddc8f22696ddeda0677b041381f4168a2ca06f712ef6ec6050d6d"},
{file = "onnxruntime-1.17.3-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:e0ae39f5452278cd349520c296e7de3e90d62dc5b0157c6868e2748d7f28b871"},
{file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ff2dc012bd930578aff5232afd2905bf16620815f36783a941aafabf94b3702"},
{file = "onnxruntime-1.17.3-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf6c37483782e4785019b56e26224a25e9b9a35b849d0169ce69189867a22bb1"},
{file = "onnxruntime-1.17.3-cp39-cp39-win32.whl", hash = "sha256:351bf5a1140dcc43bfb8d3d1a230928ee61fcd54b0ea664c8e9a889a8e3aa515"},
{file = "onnxruntime-1.17.3-cp39-cp39-win_amd64.whl", hash = "sha256:57a3de15778da8d6cc43fbf6cf038e1e746146300b5f0b1fbf01f6f795dc6440"},
]
[package.dependencies]
@@ -2000,21 +2000,21 @@ sympy = "*"
[[package]]
name = "onnxruntime-gpu"
version = "1.18.0"
version = "1.17.1"
description = "ONNX Runtime is a runtime accelerator for Machine Learning models"
optional = false
python-versions = "*"
files = [
{file = "onnxruntime_gpu-1.18.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:039be9a6b7f71c6739e97eec79f4bf240793a7c0c4108a09e0e1a27b4c33dbca"},
{file = "onnxruntime_gpu-1.18.0-cp310-cp310-win_amd64.whl", hash = "sha256:afd4bc090b9412ab695cb34c05f4f92f88dbb6bd52d9b38658ad0115c50ff653"},
{file = "onnxruntime_gpu-1.18.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:2280f94c6be2717f010a73c30a94c2721af853c6b7110e83afa52d03de6614a8"},
{file = "onnxruntime_gpu-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:9e3b4e9a0171e53a71001805b9b0e1a98cbad5a413d795c0e132b0f058b386d6"},
{file = "onnxruntime_gpu-1.18.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:546fbf2dcb7a2830ca69bde0c38665a88a9454e923ebb76bedf85eaed33a6f4a"},
{file = "onnxruntime_gpu-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:24146aa670c45734d9b8583cd78bd790363bc8695a3808d129ec913186064e4c"},
{file = "onnxruntime_gpu-1.18.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:75884c51c2aa47c349de3b5485df7f9573e1b89c607dd55984d3fe40615ef002"},
{file = "onnxruntime_gpu-1.18.0-cp38-cp38-win_amd64.whl", hash = "sha256:f3b00a7443252dbfbd18ff72bcc2f44066fad9128eaa29bff8b315a834241701"},
{file = "onnxruntime_gpu-1.18.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:14f53bd74ad21c61fee55eca988758e5eec4c39450040c8986ec3a960cb127a8"},
{file = "onnxruntime_gpu-1.18.0-cp39-cp39-win_amd64.whl", hash = "sha256:aea5c02b3a0ee6682214a61a2a0467773401b075afdcb41dc2ef595f41c2d185"},
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:e34ecb2b527ee1265135ae74cd99ea198ff344b8221929a920596a1e461e2bbb"},
{file = "onnxruntime_gpu-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:37786c0f225be90da0a66ca413fe125a925a0900263301cc4dbcad4ff0404673"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:3bde190a683ec84ecf61bd390f3c275d388efe72404633df374c52c557ce6d4d"},
{file = "onnxruntime_gpu-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:5206c84caa770efcc2ca819f71ec007a244ed748ca04e7ff76b86df1a096d2c8"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0396ec73de565a64509d96dff154f531f8da8023c191f771ceba47a3f4efc266"},
{file = "onnxruntime_gpu-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:8531d4a833c8e978c5ff1de7b3bcc4126bbe58ea71fae54ddce58fe8777cb136"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:7b831f9eafd626f3d44955420a4b1b84f9ffcb987712a0ab6a37d1ee9f2f7a45"},
{file = "onnxruntime_gpu-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:a389334d3797519d4b12077db32b8764f1ce54374d0f89235edc04efe8bc192c"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:27aeaa36385e459b3867577ed7f68c1756de79aa68f57141d4ae2a31c84f6a33"},
{file = "onnxruntime_gpu-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:8b46094ea348aff6c6494402ac4260e2d2aba0522ae13e1ae29d98a29384ed70"},
]
[package.dependencies]
@@ -2438,13 +2438,13 @@ files = [
[[package]]
name = "pytest"
version = "8.2.1"
version = "8.2.0"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.2.1-py3-none-any.whl", hash = "sha256:faccc5d332b8c3719f40283d0d44aa5cf101cec36f88cde9ed8f2bc0538612b1"},
{file = "pytest-8.2.1.tar.gz", hash = "sha256:5046e5b46d8e4cac199c373041f26be56fdb81eb4e67dc11d4e10811fc3408fd"},
{file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"},
{file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"},
]
[package.dependencies]
@@ -2460,13 +2460,13 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments
[[package]]
name = "pytest-asyncio"
version = "0.23.7"
version = "0.23.6"
description = "Pytest support for asyncio"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest_asyncio-0.23.7-py3-none-any.whl", hash = "sha256:009b48127fbe44518a547bddd25611551b0e43ccdbf1e67d12479f569832c20b"},
{file = "pytest_asyncio-0.23.7.tar.gz", hash = "sha256:5f5c72948f4c49e7db4f29f2521d4031f1c27f86e57b046126654083d4770268"},
{file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"},
{file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"},
]
[package.dependencies]
@@ -2799,28 +2799,28 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
[[package]]
name = "ruff"
version = "0.4.7"
version = "0.4.4"
description = "An extremely fast Python linter and code formatter, written in Rust."
optional = false
python-versions = ">=3.7"
files = [
{file = "ruff-0.4.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e089371c67892a73b6bb1525608e89a2aca1b77b5440acf7a71dda5dac958f9e"},
{file = "ruff-0.4.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:10f973d521d910e5f9c72ab27e409e839089f955be8a4c8826601a6323a89753"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:59c3d110970001dfa494bcd95478e62286c751126dfb15c3c46e7915fc49694f"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa9773c6c00f4958f73b317bc0fd125295110c3776089f6ef318f4b775f0abe4"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07fc80bbb61e42b3b23b10fda6a2a0f5a067f810180a3760c5ef1b456c21b9db"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:fa4dafe3fe66d90e2e2b63fa1591dd6e3f090ca2128daa0be33db894e6c18648"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a7c0083febdec17571455903b184a10026603a1de078428ba155e7ce9358c5f6"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad1b20e66a44057c326168437d680a2166c177c939346b19c0d6b08a62a37589"},
{file = "ruff-0.4.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cbf5d818553add7511c38b05532d94a407f499d1a76ebb0cad0374e32bc67202"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:50e9651578b629baec3d1513b2534de0ac7ed7753e1382272b8d609997e27e83"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:8874a9df7766cb956b218a0a239e0a5d23d9e843e4da1e113ae1d27ee420877a"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b9de9a6e49f7d529decd09381c0860c3f82fa0b0ea00ea78409b785d2308a567"},
{file = "ruff-0.4.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:13a1768b0691619822ae6d446132dbdfd568b700ecd3652b20d4e8bc1e498f78"},
{file = "ruff-0.4.7-py3-none-win32.whl", hash = "sha256:769e5a51df61e07e887b81e6f039e7ed3573316ab7dd9f635c5afaa310e4030e"},
{file = "ruff-0.4.7-py3-none-win_amd64.whl", hash = "sha256:9e3ab684ad403a9ed1226894c32c3ab9c2e0718440f6f50c7c5829932bc9e054"},
{file = "ruff-0.4.7-py3-none-win_arm64.whl", hash = "sha256:10f2204b9a613988e3484194c2c9e96a22079206b22b787605c255f130db5ed7"},
{file = "ruff-0.4.7.tar.gz", hash = "sha256:2331d2b051dc77a289a653fcc6a42cce357087c5975738157cd966590b18b5e1"},
{file = "ruff-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29d44ef5bb6a08e235c8249294fa8d431adc1426bfda99ed493119e6f9ea1bf6"},
{file = "ruff-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c4efe62b5bbb24178c950732ddd40712b878a9b96b1d02b0ff0b08a090cbd891"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c8e2f1e8fc12d07ab521a9005d68a969e167b589cbcaee354cb61e9d9de9c15"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:60ed88b636a463214905c002fa3eaab19795679ed55529f91e488db3fe8976ab"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b90fc5e170fc71c712cc4d9ab0e24ea505c6a9e4ebf346787a67e691dfb72e85"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:8e7e6ebc10ef16dcdc77fd5557ee60647512b400e4a60bdc4849468f076f6eef"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9ddb2c494fb79fc208cd15ffe08f32b7682519e067413dbaf5f4b01a6087bcd"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c51c928a14f9f0a871082603e25a1588059b7e08a920f2f9fa7157b5bf08cfe9"},
{file = "ruff-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5eb0a4bfd6400b7d07c09a7725e1a98c3b838be557fee229ac0f84d9aa49c36"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b1867ee9bf3acc21778dcb293db504692eda5f7a11a6e6cc40890182a9f9e595"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1aecced1269481ef2894cc495647392a34b0bf3e28ff53ed95a385b13aa45768"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9da73eb616b3241a307b837f32756dc20a0b07e2bcb694fec73699c93d04a69e"},
{file = "ruff-0.4.4-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:958b4ea5589706a81065e2a776237de2ecc3e763342e5cc8e02a4a4d8a5e6f95"},
{file = "ruff-0.4.4-py3-none-win32.whl", hash = "sha256:cb53473849f011bca6e754f2cdf47cafc9c4f4ff4570003a0dad0b9b6890e876"},
{file = "ruff-0.4.4-py3-none-win_amd64.whl", hash = "sha256:424e5b72597482543b684c11def82669cc6b395aa8cc69acc1858b5ef3e5daae"},
{file = "ruff-0.4.4-py3-none-win_arm64.whl", hash = "sha256:39df0537b47d3b597293edbb95baf54ff5b49589eb7ff41926d8243caa995ea6"},
{file = "ruff-0.4.4.tar.gz", hash = "sha256:f87ea42d5cdebdc6a69761a9d0bc83ae9b3b30d0ad78952005ba6568d6c022af"},
]
[[package]]
@@ -3234,13 +3234,13 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "uvicorn"
version = "0.30.1"
version = "0.29.0"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
files = [
{file = "uvicorn-0.30.1-py3-none-any.whl", hash = "sha256:cd17daa7f3b9d7a24de3617820e634d0933b69eed8e33a516071174427238c81"},
{file = "uvicorn-0.30.1.tar.gz", hash = "sha256:d46cd8e0fd80240baffbcd9ec1012a712938754afcf81bce56c024c1656aece8"},
{file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"},
{file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"},
]
[package.dependencies]

View File

@@ -1,3 +1,3 @@
{
"flutter": "3.22.1"
"flutter": "3.22.0"
}

View File

@@ -1,5 +1,5 @@
{
"dart.flutterSdkPath": ".fvm/versions/3.22.1",
"dart.flutterSdkPath": ".fvm/versions/3.22.0",
"search.exclude": {
"**/.fvm": true
},

View File

@@ -27,10 +27,8 @@ class User {
Id get isarId => fastHash(id);
User.fromUserDto(
UserAdminResponseDto dto,
UserPreferencesResponseDto? preferences,
) : id = dto.id,
User.fromUserDto(UserAdminResponseDto dto)
: id = dto.id,
updatedAt = dto.updatedAt,
email = dto.email,
name = dto.name,
@@ -38,7 +36,7 @@ class User {
isPartnerSharedWith = false,
profileImagePath = dto.profileImagePath,
isAdmin = dto.isAdmin,
memoryEnabled = preferences?.memories.enabled ?? false,
memoryEnabled = dto.memoriesEnabled ?? false,
avatarColor = dto.avatarColor.toAvatarColor(),
inTimeline = false,
quotaUsageInBytes = dto.quotaUsageInBytes ?? 0,

View File

@@ -30,6 +30,7 @@ import 'package:immich_mobile/widgets/photo_view/src/photo_view_computed_scale.d
import 'package:immich_mobile/widgets/photo_view/src/photo_view_scale_state.dart';
import 'package:immich_mobile/widgets/photo_view/src/utils/photo_view_hero_attributes.dart';
import 'package:isar/isar.dart';
import 'package:openapi/api.dart' show ThumbnailFormat;
@RoutePage()
// ignore: must_be_immutable
@@ -51,6 +52,9 @@ class GalleryViewerPage extends HookConsumerWidget {
final PageController controller;
static const jpeg = ThumbnailFormat.JPEG;
static const webp = ThumbnailFormat.WEBP;
@override
Widget build(BuildContext context, WidgetRef ref) {
final settings = ref.watch(appSettingsServiceProvider);

View File

@@ -22,8 +22,8 @@ Future<VideoPlayerController> videoPlayerController(
// Use a network URL for the video player controller
final serverEndpoint = Store.get(StoreKey.serverEndpoint);
final String videoUrl = asset.livePhotoVideoId != null
? '$serverEndpoint/assets/${asset.livePhotoVideoId}/video/playback'
: '$serverEndpoint/assets/${asset.remoteId}/video/playback';
? '$serverEndpoint/asset/file/${asset.livePhotoVideoId}'
: '$serverEndpoint/asset/file/${asset.remoteId}';
final url = Uri.parse(videoUrl);
final accessToken = Store.get(StoreKey.accessToken);

View File

@@ -138,7 +138,7 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Future<bool> changePassword(String newPassword) async {
try {
await _apiService.usersApi.updateMyUser(
await _apiService.userApi.updateMyUser(
UserUpdateMeDto(
password: newPassword,
),
@@ -177,10 +177,8 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
retResult = false;
} else {
UserAdminResponseDto? userResponseDto;
UserPreferencesResponseDto? userPreferences;
try {
userResponseDto = await _apiService.usersApi.getMyUser();
userPreferences = await _apiService.usersApi.getMyPreferences();
userResponseDto = await _apiService.userApi.getMyUser();
} on ApiException catch (error, stackTrace) {
_log.severe(
"Error getting user information from the server [API EXCEPTION]",
@@ -203,13 +201,13 @@ class AuthenticationNotifier extends StateNotifier<AuthenticationState> {
Store.put(StoreKey.deviceIdHash, fastHash(deviceId));
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponseDto, userPreferences),
User.fromUserDto(userResponseDto),
);
Store.put(StoreKey.serverUrl, serverUrl);
Store.put(StoreKey.accessToken, accessToken);
shouldChangePassword = userResponseDto.shouldChangePassword;
user = User.fromUserDto(userResponseDto, userPreferences);
user = User.fromUserDto(userResponseDto);
retResult = true;
} else {

View File

@@ -74,7 +74,7 @@ class ImmichRemoteImageProvider
if (_loadPreview) {
final preview = getThumbnailUrlForRemoteId(
key.assetId,
type: api.AssetMediaSize.thumbnail,
type: api.ThumbnailFormat.WEBP,
);
yield await ImageLoader.loadImageFromCache(
@@ -88,7 +88,7 @@ class ImmichRemoteImageProvider
// Load the higher resolution version of the image
final url = getThumbnailUrlForRemoteId(
key.assetId,
type: api.AssetMediaSize.preview,
type: api.ThumbnailFormat.JPEG,
);
final codec = await ImageLoader.loadImageFromCache(
url,

View File

@@ -61,7 +61,7 @@ class ImmichRemoteThumbnailProvider
// Load a preview to the chunk events
final preview = getThumbnailUrlForRemoteId(
key.assetId,
type: api.AssetMediaSize.thumbnail,
type: api.ThumbnailFormat.WEBP,
);
yield await ImageLoader.loadImageFromCache(

View File

@@ -46,7 +46,7 @@ class MapStateNotifier extends _$MapStateNotifier {
// Fetch and save light theme
final lightResponse = await ref
.read(apiServiceProvider)
.mapApi
.systemConfigApi
.getMapStyleWithHttpInfo(MapTheme.light);
if (lightResponse.statusCode >= HttpStatus.badRequest) {
@@ -74,7 +74,7 @@ class MapStateNotifier extends _$MapStateNotifier {
// Fetch and save dark theme
final darkResponse = await ref
.read(apiServiceProvider)
.mapApi
.systemConfigApi
.getMapStyleWithHttpInfo(MapTheme.dark);
if (darkResponse.statusCode >= HttpStatus.badRequest) {

View File

@@ -20,12 +20,11 @@ class CurrentUserProvider extends StateNotifier<User?> {
refresh() async {
try {
final user = await _apiService.usersApi.getMyUser();
final userPreferences = await _apiService.usersApi.getMyPreferences();
final user = await _apiService.userApi.getMyUser();
if (user != null) {
Store.put(
StoreKey.currentUser,
User.fromUserDto(user, userPreferences),
User.fromUserDto(user),
);
}
} catch (_) {}

View File

@@ -57,9 +57,7 @@ class TabNavigationObserver extends AutoRouterObserver {
// Update user info
try {
final userResponseDto =
await ref.read(apiServiceProvider).usersApi.getMyUser();
final userPreferences =
await ref.read(apiServiceProvider).usersApi.getMyPreferences();
await ref.read(apiServiceProvider).userApi.getMyUser();
if (userResponseDto == null) {
return;
@@ -67,7 +65,7 @@ class TabNavigationObserver extends AutoRouterObserver {
Store.put(
StoreKey.currentUser,
User.fromUserDto(userResponseDto, userPreferences),
User.fromUserDto(userResponseDto),
);
ref.read(serverInfoProvider.notifier).getServerVersion();
} catch (e) {

View File

@@ -19,7 +19,7 @@ class ActivityService with ErrorLoggerMixin {
}) async {
return logError(
() async {
final list = await _apiService.activitiesApi
final list = await _apiService.activityApi
.getActivities(albumId, assetId: assetId);
return list != null ? list.map(Activity.fromDto).toList() : [];
},
@@ -31,7 +31,7 @@ class ActivityService with ErrorLoggerMixin {
Future<int> getStatistics(String albumId, {String? assetId}) async {
return logError(
() async {
final dto = await _apiService.activitiesApi
final dto = await _apiService.activityApi
.getActivityStatistics(albumId, assetId: assetId);
return dto?.comments ?? 0;
},
@@ -43,7 +43,7 @@ class ActivityService with ErrorLoggerMixin {
Future<bool> removeActivity(String id) async {
return logError(
() async {
await _apiService.activitiesApi.deleteActivity(id);
await _apiService.activityApi.deleteActivity(id);
return true;
},
defaultValue: false,
@@ -59,7 +59,7 @@ class ActivityService with ErrorLoggerMixin {
}) async {
return guardError(
() async {
final dto = await _apiService.activitiesApi.createActivity(
final dto = await _apiService.activityApi.createActivity(
ActivityCreateDto(
albumId: albumId,
type: type == ActivityType.comment

View File

@@ -151,7 +151,7 @@ class AlbumService {
bool changes = false;
try {
await _userService.refreshUsers();
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumsApi
final List<AlbumResponseDto>? serverAlbums = await _apiService.albumApi
.getAllAlbums(shared: isShared ? true : null);
if (serverAlbums == null) {
return false;
@@ -161,7 +161,7 @@ class AlbumService {
isShared: isShared,
loadDetails: (dto) async => dto.assetCount == dto.assets.length
? dto
: (await _apiService.albumsApi.getAlbumInfo(dto.id)) ?? dto,
: (await _apiService.albumApi.getAlbumInfo(dto.id)) ?? dto,
);
} finally {
_remoteCompleter.complete(changes);
@@ -176,7 +176,7 @@ class AlbumService {
Iterable<User> sharedUsers = const [],
]) async {
try {
AlbumResponseDto? remote = await _apiService.albumsApi.createAlbum(
AlbumResponseDto? remote = await _apiService.albumApi.createAlbum(
CreateAlbumDto(
albumName: albumName,
assetIds: assets.map((asset) => asset.remoteId!).toList(),
@@ -231,7 +231,7 @@ class AlbumService {
Album album,
) async {
try {
var response = await _apiService.albumsApi.addAssetsToAlbum(
var response = await _apiService.albumApi.addAssetsToAlbum(
album.remoteId!,
BulkIdsDto(ids: assets.map((asset) => asset.remoteId!).toList()),
);
@@ -290,7 +290,7 @@ class AlbumService {
.map((userId) => AlbumUserAddDto(userId: userId))
.toList();
final result = await _apiService.albumsApi.addUsersToAlbum(
final result = await _apiService.albumApi.addUsersToAlbum(
album.remoteId!,
AddUsersDto(albumUsers: albumUsers),
);
@@ -312,7 +312,7 @@ class AlbumService {
Future<bool> setActivityEnabled(Album album, bool enabled) async {
try {
final result = await _apiService.albumsApi.updateAlbumInfo(
final result = await _apiService.albumApi.updateAlbumInfo(
album.remoteId!,
UpdateAlbumDto(isActivityEnabled: enabled),
);
@@ -331,7 +331,7 @@ class AlbumService {
try {
final userId = Store.get(StoreKey.currentUser).isarId;
if (album.owner.value?.isarId == userId) {
await _apiService.albumsApi.deleteAlbum(album.remoteId!);
await _apiService.albumApi.deleteAlbum(album.remoteId!);
}
if (album.shared) {
final foreignAssets =
@@ -362,7 +362,7 @@ class AlbumService {
Future<bool> leaveAlbum(Album album) async {
try {
await _apiService.albumsApi.removeUserFromAlbum(album.remoteId!, "me");
await _apiService.albumApi.removeUserFromAlbum(album.remoteId!, "me");
return true;
} catch (e) {
debugPrint("Error leaveAlbum ${e.toString()}");
@@ -375,7 +375,7 @@ class AlbumService {
Iterable<Asset> assets,
) async {
try {
final response = await _apiService.albumsApi.removeAssetFromAlbum(
final response = await _apiService.albumApi.removeAssetFromAlbum(
album.remoteId!,
BulkIdsDto(
ids: assets.map((asset) => asset.remoteId!).toList(),
@@ -401,7 +401,7 @@ class AlbumService {
User user,
) async {
try {
await _apiService.albumsApi.removeUserFromAlbum(
await _apiService.albumApi.removeUserFromAlbum(
album.remoteId!,
user.id,
);
@@ -426,7 +426,7 @@ class AlbumService {
String newAlbumTitle,
) async {
try {
await _apiService.albumsApi.updateAlbumInfo(
await _apiService.albumApi.updateAlbumInfo(
album.remoteId!,
UpdateAlbumDto(
albumName: newAlbumTitle,

View File

@@ -12,21 +12,20 @@ import 'package:http/http.dart';
class ApiService {
late ApiClient _apiClient;
late UsersApi usersApi;
late UserApi userApi;
late AuthenticationApi authenticationApi;
late OAuthApi oAuthApi;
late AlbumsApi albumsApi;
late AssetsApi assetsApi;
late AlbumApi albumApi;
late AssetApi assetApi;
late SearchApi searchApi;
late ServerInfoApi serverInfoApi;
late MapApi mapApi;
late PartnersApi partnersApi;
late PeopleApi peopleApi;
late PartnerApi partnerApi;
late PersonApi personApi;
late AuditApi auditApi;
late SharedLinksApi sharedLinksApi;
late SharedLinkApi sharedLinkApi;
late SyncApi syncApi;
late SystemConfigApi systemConfigApi;
late ActivitiesApi activitiesApi;
late ActivityApi activityApi;
late DownloadApi downloadApi;
late TrashApi trashApi;
@@ -44,21 +43,20 @@ class ApiService {
if (_accessToken != null) {
setAccessToken(_accessToken!);
}
usersApi = UsersApi(_apiClient);
userApi = UserApi(_apiClient);
authenticationApi = AuthenticationApi(_apiClient);
oAuthApi = OAuthApi(_apiClient);
albumsApi = AlbumsApi(_apiClient);
assetsApi = AssetsApi(_apiClient);
albumApi = AlbumApi(_apiClient);
assetApi = AssetApi(_apiClient);
serverInfoApi = ServerInfoApi(_apiClient);
searchApi = SearchApi(_apiClient);
mapApi = MapApi(_apiClient);
partnersApi = PartnersApi(_apiClient);
peopleApi = PeopleApi(_apiClient);
partnerApi = PartnerApi(_apiClient);
personApi = PersonApi(_apiClient);
auditApi = AuditApi(_apiClient);
sharedLinksApi = SharedLinksApi(_apiClient);
sharedLinkApi = SharedLinkApi(_apiClient);
syncApi = SyncApi(_apiClient);
systemConfigApi = SystemConfigApi(_apiClient);
activitiesApi = ActivitiesApi(_apiClient);
activityApi = ActivityApi(_apiClient);
downloadApi = DownloadApi(_apiClient);
trashApi = TrashApi(_apiClient);
}

View File

@@ -82,9 +82,9 @@ class AssetService {
) async {
try {
final AssetResponseDto? dto =
await _apiService.assetsApi.getAssetInfo(remoteId);
await _apiService.assetApi.getAssetInfo(remoteId);
return dto?.people;
return dto?.people?.visiblePeople;
} catch (error, stack) {
log.severe(
'Error while getting remote asset info: ${error.toString()}',
@@ -138,7 +138,7 @@ class AssetService {
payload.add(asset.remoteId!);
}
await _apiService.assetsApi.deleteAssets(
await _apiService.assetApi.deleteAssets(
AssetBulkDeleteDto(
ids: payload,
force: force,
@@ -158,7 +158,7 @@ class AssetService {
// fileSize is always filled on the server but not set on client
if (a.exifInfo?.fileSize == null) {
if (a.isRemote) {
final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!);
final dto = await _apiService.assetApi.getAssetInfo(a.remoteId!);
if (dto != null && dto.exifInfo != null) {
final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id);
if (newExif != a.exifInfo) {
@@ -180,7 +180,7 @@ class AssetService {
List<Asset> assets,
UpdateAssetDto updateAssetDto,
) async {
return await _apiService.assetsApi.updateAssets(
return await _apiService.assetApi.updateAssets(
AssetBulkUpdateDto(
ids: assets.map((e) => e.remoteId!).toList(),
dateTimeOriginal: updateAssetDto.dateTimeOriginal,

View File

@@ -17,7 +17,7 @@ class AssetDescriptionService {
String remoteAssetId,
int localExifId,
) async {
final result = await _api.assetsApi.updateAsset(
final result = await _api.assetApi.updateAsset(
remoteAssetId,
UpdateAssetDto(description: description),
);
@@ -36,7 +36,7 @@ class AssetDescriptionService {
Future<String> readLatest(String assetRemoteId, int localExifId) async {
final latestAssetFromServer =
await _api.assetsApi.getAssetInfo(assetRemoteId);
await _api.assetApi.getAssetInfo(assetRemoteId);
final localExifInfo = await _db.exifInfos.get(localExifId);
if (latestAssetFromServer != null && localExifInfo != null) {

View File

@@ -27,7 +27,7 @@ class AssetStackService {
.map((e) => e.remoteId!)
.toList();
await _api.assetsApi.updateAssets(
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId),
);
}
@@ -37,7 +37,7 @@ class AssetStackService {
.where((e) => e.isRemote)
.map((e) => e.remoteId!)
.toList();
await _api.assetsApi.updateAssets(
await _api.assetApi.updateAssets(
AssetBulkUpdateDto(ids: toRemove, removeParent: true),
);
}
@@ -53,7 +53,7 @@ class AssetStackService {
}
try {
await _api.assetsApi.updateStackParent(
await _api.assetApi.updateStackParent(
UpdateStackParentDto(
oldParentId: oldParent.remoteId!,
newParentId: newParent.remoteId!,

View File

@@ -2,26 +2,26 @@ import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'package:cancellation_token_http/http.dart' as http;
import 'package:collection/collection.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:immich_mobile/entities/backup_album.entity.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/models/backup/current_upload_asset.model.dart';
import 'package:immich_mobile/entities/duplicated_asset.entity.dart';
import 'package:immich_mobile/models/backup/error_upload_asset.model.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/app_settings.provider.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:immich_mobile/entities/store.entity.dart';
import 'package:immich_mobile/providers/api.provider.dart';
import 'package:immich_mobile/providers/db.provider.dart';
import 'package:immich_mobile/services/api.service.dart';
import 'package:immich_mobile/services/app_settings.service.dart';
import 'package:isar/isar.dart';
import 'package:logging/logging.dart';
import 'package:openapi/api.dart';
import 'package:path/path.dart' as p;
import 'package:permission_handler/permission_handler.dart';
import 'package:photo_manager/photo_manager.dart';
import 'package:cancellation_token_http/http.dart' as http;
import 'package:path/path.dart' as p;
final backupServiceProvider = Provider(
(ref) => BackupService(
@@ -44,7 +44,7 @@ class BackupService {
final String deviceId = Store.get(StoreKey.deviceId);
try {
return await _apiService.assetsApi.getAllUserAssetsByDeviceId(deviceId);
return await _apiService.assetApi.getAllUserAssetsByDeviceId(deviceId);
} catch (e) {
debugPrint('Error [getDeviceBackupAsset] ${e.toString()}');
return null;
@@ -178,7 +178,7 @@ class BackupService {
try {
final String deviceId = Store.get(StoreKey.deviceId);
final CheckExistingAssetsResponseDto? duplicates =
await _apiService.assetsApi.checkExistingAssets(
await _apiService.assetApi.checkExistingAssets(
CheckExistingAssetsDto(
deviceAssetIds: candidates.map((e) => e.id).toList(),
deviceId: deviceId,
@@ -270,12 +270,10 @@ class BackupService {
);
file = await entity.loadFile(progressHandler: pmProgressHandler);
if (entity.isLivePhoto) {
livePhotoFile = await entity.loadFile(
withSubtype: true,
progressHandler: pmProgressHandler,
);
}
livePhotoFile = await entity.loadFile(
withSubtype: true,
progressHandler: pmProgressHandler,
);
} else {
if (entity.type == AssetType.video) {
file = await entity.originFile;
@@ -290,15 +288,6 @@ class BackupService {
if (file != null) {
String originalFileName = await entity.titleAsync;
if (entity.isLivePhoto) {
if (livePhotoFile == null) {
_log.warning(
"Failed to obtain motion part of the livePhoto - $originalFileName",
);
}
}
var fileStream = file.openRead();
var assetRawUploadData = http.MultipartFile(
"assetData",
@@ -307,29 +296,50 @@ class BackupService {
filename: originalFileName,
);
var baseRequest = MultipartRequest(
var req = MultipartRequest(
'POST',
Uri.parse('$savedEndpoint/assets'),
Uri.parse('$savedEndpoint/asset/upload'),
onProgress: ((bytes, totalBytes) =>
uploadProgressCb(bytes, totalBytes)),
);
baseRequest.headers["x-immich-user-token"] =
Store.get(StoreKey.accessToken);
baseRequest.headers["Transfer-Encoding"] = "chunked";
req.headers["x-immich-user-token"] = Store.get(StoreKey.accessToken);
req.headers["Transfer-Encoding"] = "chunked";
baseRequest.fields['deviceAssetId'] = entity.id;
baseRequest.fields['deviceId'] = deviceId;
baseRequest.fields['fileCreatedAt'] =
req.fields['deviceAssetId'] = entity.id;
req.fields['deviceId'] = deviceId;
req.fields['fileCreatedAt'] =
entity.createDateTime.toUtc().toIso8601String();
baseRequest.fields['fileModifiedAt'] =
req.fields['fileModifiedAt'] =
entity.modifiedDateTime.toUtc().toIso8601String();
baseRequest.fields['isFavorite'] = entity.isFavorite.toString();
baseRequest.fields['duration'] = entity.videoDuration.toString();
req.fields['isFavorite'] = entity.isFavorite.toString();
req.fields['duration'] = entity.videoDuration.toString();
baseRequest.files.add(assetRawUploadData);
req.files.add(assetRawUploadData);
var fileSize = file.lengthSync();
if (entity.isLivePhoto) {
if (livePhotoFile != null) {
final livePhotoTitle = p.setExtension(
originalFileName,
p.extension(livePhotoFile.path),
);
final fileStream = livePhotoFile.openRead();
final livePhotoRawUploadData = http.MultipartFile(
"livePhotoData",
fileStream,
livePhotoFile.lengthSync(),
filename: livePhotoTitle,
);
req.files.add(livePhotoRawUploadData);
fileSize += livePhotoFile.lengthSync();
} else {
_log.warning(
"Failed to obtain motion part of the livePhoto - $originalFileName",
);
}
}
setCurrentUploadAssetCb(
CurrentUploadAsset(
id: entity.id,
@@ -343,29 +353,19 @@ class BackupService {
),
);
String? livePhotoVideoId;
if (entity.isLivePhoto && livePhotoFile != null) {
livePhotoVideoId = await uploadLivePhotoVideo(
originalFileName,
livePhotoFile,
baseRequest,
cancelToken,
);
}
var response =
await httpClient.send(req, cancellationToken: cancelToken);
if (livePhotoVideoId != null) {
baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId;
}
var response = await httpClient.send(
baseRequest,
cancellationToken: cancelToken,
);
var responseBody = jsonDecode(await response.stream.bytesToString());
if (![200, 201].contains(response.statusCode)) {
var error = responseBody;
if (response.statusCode == 200) {
// asset is a duplicate (already exists on the server)
duplicatedAssetIds.add(entity.id);
uploadSuccessCb(entity.id, deviceId, true);
} else if (response.statusCode == 201) {
// stored a new asset on the server
uploadSuccessCb(entity.id, deviceId, false);
} else {
var data = await response.stream.bytesToString();
var error = jsonDecode(data);
var errorMessage = error['message'] ?? error['error'];
debugPrint(
@@ -389,14 +389,6 @@ class BackupService {
}
continue;
}
var isDuplicate = false;
if (response.statusCode == 200) {
isDuplicate = true;
duplicatedAssetIds.add(entity.id);
}
uploadSuccessCb(entity.id, deviceId, isDuplicate);
}
} on http.CancelledException {
debugPrint("Backup was cancelled by the user");
@@ -423,54 +415,6 @@ class BackupService {
return !anyErrors;
}
Future<String?> uploadLivePhotoVideo(
String originalFileName,
File? livePhotoVideoFile,
MultipartRequest baseRequest,
http.CancellationToken cancelToken,
) async {
if (livePhotoVideoFile == null) {
return null;
}
final livePhotoTitle = p.setExtension(
originalFileName,
p.extension(livePhotoVideoFile.path),
);
final fileStream = livePhotoVideoFile.openRead();
final livePhotoRawUploadData = http.MultipartFile(
"assetData",
fileStream,
livePhotoVideoFile.lengthSync(),
filename: livePhotoTitle,
);
final livePhotoReq = MultipartRequest(
baseRequest.method,
baseRequest.url,
onProgress: baseRequest.onProgress,
)
..headers.addAll(baseRequest.headers)
..fields.addAll(baseRequest.fields);
livePhotoReq.files.add(livePhotoRawUploadData);
var response = await httpClient.send(
livePhotoReq,
cancellationToken: cancelToken,
);
var responseBody = jsonDecode(await response.stream.bytesToString());
if (![200, 201].contains(response.statusCode)) {
var error = responseBody;
debugPrint(
"Error(${error['statusCode']}) uploading livePhoto for assetId | $livePhotoTitle | ${error['error']}",
);
}
return responseBody.containsKey('id') ? responseBody['id'] : null;
}
String _getAssetType(AssetType assetType) {
switch (assetType) {
case AssetType.audio:

View File

@@ -136,7 +136,7 @@ class BackupVerificationService {
ExifInfo? exif = remote.exifInfo;
if (exif != null && exif.lat != null) return false;
if (exif == null || exif.fileSize == null) {
final dto = await apiService.assetsApi.getAssetInfo(remote.remoteId!);
final dto = await apiService.assetApi.getAssetInfo(remote.remoteId!);
if (dto != null && dto.exifInfo != null) {
exif = ExifInfo.fromDto(dto.exifInfo!);
}
@@ -165,8 +165,8 @@ class BackupVerificationService {
// (skip first few KBs containing metadata)
final Uint64List localImage =
_fakeDecodeImg(local, await file.readAsBytes());
final res = await apiService.assetsApi
.downloadAssetWithHttpInfo(remote.remoteId!);
final res = await apiService.downloadApi
.downloadFileWithHttpInfo(remote.remoteId!);
final Uint64List remoteImage = _fakeDecodeImg(remote, res.bodyBytes);
final eq = const ListEquality().equals(remoteImage, localImage);

View File

@@ -26,19 +26,19 @@ class ImageViewerService {
// Download LivePhotos image and motion part
if (asset.isImage && asset.livePhotoVideoId != null && Platform.isIOS) {
var imageResponse =
await _apiService.assetsApi.downloadAssetWithHttpInfo(
await _apiService.downloadApi.downloadFileWithHttpInfo(
asset.remoteId!,
);
var motionResponse =
await _apiService.assetsApi.downloadAssetWithHttpInfo(
var motionReponse =
await _apiService.downloadApi.downloadFileWithHttpInfo(
asset.livePhotoVideoId!,
);
if (imageResponse.statusCode != 200 ||
motionResponse.statusCode != 200) {
motionReponse.statusCode != 200) {
final failedResponse =
imageResponse.statusCode != 200 ? imageResponse : motionResponse;
imageResponse.statusCode != 200 ? imageResponse : motionReponse;
_log.severe(
"Motion asset download failed",
failedResponse.toLoggerString(),
@@ -51,7 +51,7 @@ class ImageViewerService {
final tempDir = await getTemporaryDirectory();
videoFile = await File('${tempDir.path}/livephoto.mov').create();
imageFile = await File('${tempDir.path}/livephoto.heic').create();
videoFile.writeAsBytesSync(motionResponse.bodyBytes);
videoFile.writeAsBytesSync(motionReponse.bodyBytes);
imageFile.writeAsBytesSync(imageResponse.bodyBytes);
entity = await PhotoManager.editor.darwin.saveLivePhoto(
@@ -73,8 +73,8 @@ class ImageViewerService {
return entity != null;
} else {
var res = await _apiService.assetsApi
.downloadAssetWithHttpInfo(asset.remoteId!);
var res = await _apiService.downloadApi
.downloadFileWithHttpInfo(asset.remoteId!);
if (res.statusCode != 200) {
_log.severe("Asset download failed", res.toLoggerString());

View File

@@ -19,7 +19,7 @@ class MapSerivce with ErrorLoggerMixin {
}) async {
return logError(
() async {
final markers = await _apiService.mapApi.getMapMarkers(
final markers = await _apiService.assetApi.getMapMarkers(
isFavorite: isFavorite,
isArchived: withArchived,
withPartners: withPartners,

Some files were not shown because too many files have changed in this diff Show More