mirror of
https://github.com/immich-app/immich.git
synced 2025-07-31 14:35:25 -04:00
Merge branch 'main' into fix-back-scroll
This commit is contained in:
commit
d583d3d639
2
.github/workflows/cli.yml
vendored
2
.github/workflows/cli.yml
vendored
@ -22,7 +22,7 @@ permissions:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
publish:
|
publish:
|
||||||
name: Publish
|
name: CLI Publish
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
defaults:
|
defaults:
|
||||||
run:
|
run:
|
||||||
|
13
.github/workflows/docker.yml
vendored
13
.github/workflows/docker.yml
vendored
@ -234,3 +234,16 @@ jobs:
|
|||||||
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
BUILD_IMAGE=${{ github.event_name == 'release' && github.ref_name || steps.metadata.outputs.tags }}
|
||||||
BUILD_SOURCE_REF=${{ github.ref_name }}
|
BUILD_SOURCE_REF=${{ github.ref_name }}
|
||||||
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
BUILD_SOURCE_COMMIT=${{ github.sha }}
|
||||||
|
|
||||||
|
success-check:
|
||||||
|
name: Docker Build & Push Success
|
||||||
|
needs: [build_and_push_ml, build_and_push_server]
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
if: always()
|
||||||
|
steps:
|
||||||
|
- name: Any jobs failed?
|
||||||
|
if: ${{ contains(needs.*.result, 'failure') }}
|
||||||
|
run: exit 1
|
||||||
|
- name: All jobs passed or skipped
|
||||||
|
if: ${{ !(contains(needs.*.result, 'failure')) }}
|
||||||
|
run: echo "All jobs passed or skipped" && echo "${{ toJSON(needs.*.result) }}"
|
||||||
|
1
.github/workflows/docs-build.yml
vendored
1
.github/workflows/docs-build.yml
vendored
@ -30,6 +30,7 @@ jobs:
|
|||||||
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ github.event_name == 'release' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
build:
|
build:
|
||||||
|
name: Docs Build
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
2
.github/workflows/docs-deploy.yml
vendored
2
.github/workflows/docs-deploy.yml
vendored
@ -7,6 +7,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
checks:
|
checks:
|
||||||
|
name: Docs Deploy Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
outputs:
|
outputs:
|
||||||
parameters: ${{ steps.parameters.outputs.result }}
|
parameters: ${{ steps.parameters.outputs.result }}
|
||||||
@ -91,6 +92,7 @@ jobs:
|
|||||||
return parameters;
|
return parameters;
|
||||||
|
|
||||||
deploy:
|
deploy:
|
||||||
|
name: Docs Deploy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: checks
|
needs: checks
|
||||||
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
if: ${{ fromJson(needs.checks.outputs.artifact).found && fromJson(needs.checks.outputs.parameters).shouldDeploy }}
|
||||||
|
1
.github/workflows/docs-destroy.yml
vendored
1
.github/workflows/docs-destroy.yml
vendored
@ -5,6 +5,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
name: Docs Destroy
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
|
12
.github/workflows/test.yml
vendored
12
.github/workflows/test.yml
vendored
@ -48,7 +48,7 @@ jobs:
|
|||||||
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
run: echo "should_force=${{ github.event_name == 'workflow_dispatch' }}" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
server-unit-tests:
|
server-unit-tests:
|
||||||
name: Server
|
name: Test & Lint Server
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_server == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -85,7 +85,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests:
|
cli-unit-tests:
|
||||||
name: CLI
|
name: Unit Test CLI
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -126,7 +126,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
cli-unit-tests-win:
|
cli-unit-tests-win:
|
||||||
name: CLI (Windows)
|
name: Unit Test CLI (Windows)
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_cli == 'true' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
@ -160,7 +160,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
web-unit-tests:
|
web-unit-tests:
|
||||||
name: Web
|
name: Test & Lint Web
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_web == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -327,7 +327,7 @@ jobs:
|
|||||||
if: ${{ !cancelled() }}
|
if: ${{ !cancelled() }}
|
||||||
|
|
||||||
mobile-unit-tests:
|
mobile-unit-tests:
|
||||||
name: Mobile
|
name: Unit Test Mobile
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_mobile == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@ -343,7 +343,7 @@ jobs:
|
|||||||
run: flutter test -j 1
|
run: flutter test -j 1
|
||||||
|
|
||||||
ml-unit-tests:
|
ml-unit-tests:
|
||||||
name: Machine Learning
|
name: Unit Test ML
|
||||||
needs: pre-job
|
needs: pre-job
|
||||||
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
if: ${{ needs.pre-job.outputs.should_run_ml == 'true' }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint
|
|||||||
cd /tmp/immich-mountpoint
|
cd /tmp/immich-mountpoint
|
||||||
```
|
```
|
||||||
|
|
||||||
You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
|
You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint`
|
||||||
|
@ -125,7 +125,7 @@ When `DB_URL` is defined, the `DB_HOSTNAME`, `DB_PORT`, `DB_USERNAME`, `DB_PASSW
|
|||||||
All `REDIS_` variables must be provided to all Immich workers, including `api` and `microservices`.
|
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.
|
`REDIS_URL` must start with `ioredis://` and then include a `base64` encoded JSON string for the configuration.
|
||||||
More info can be found in the upstream [ioredis][redis-api] documentation.
|
More info can be found in the upstream [ioredis] documentation.
|
||||||
|
|
||||||
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
|
When `REDIS_URL` or `REDIS_SOCKET` are defined, the `REDIS_HOSTNAME`, `REDIS_PORT`, `REDIS_USERNAME`, `REDIS_PASSWORD`, and `REDIS_DBINDEX` variables are ignored.
|
||||||
:::
|
:::
|
||||||
@ -226,4 +226,4 @@ to use use a Docker secret for the password in the Redis container.
|
|||||||
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
[docker-secrets-example]: https://github.com/docker-library/redis/issues/46#issuecomment-335326234
|
||||||
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
[docker-secrets-docs]: https://github.com/docker-library/docs/tree/master/postgres#docker-secrets
|
||||||
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
[docker-secrets]: https://docs.docker.com/engine/swarm/secrets/
|
||||||
[redis-api]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
|
[ioredis]: https://ioredis.readthedocs.io/en/latest/README/#connect-to-redis
|
||||||
|
7
docs/package-lock.json
generated
7
docs/package-lock.json
generated
@ -13698,9 +13698,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/prism-react-renderer": {
|
"node_modules/prism-react-renderer": {
|
||||||
"version": "2.3.1",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.0.tgz",
|
||||||
"integrity": "sha512-Rdf+HzBLR7KYjzpJ1rSoxT9ioO85nZngQEoFIhL07XhtJHlCU3SOz0GJ6+qvMyQe0Se+BV3qpe6Yd/NmQF5Juw==",
|
"integrity": "sha512-327BsVCD/unU4CNLZTWVHyUHKnsqcvj2qbPlQ8MiBE2eq2rgctjigPA1Gp9HLF83kZ20zNN6jgizHJeEsyFYOw==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prismjs": "^1.26.0",
|
"@types/prismjs": "^1.26.0",
|
||||||
"clsx": "^2.0.0"
|
"clsx": "^2.0.0"
|
||||||
|
@ -6,7 +6,9 @@ import {
|
|||||||
LoginResponseDto,
|
LoginResponseDto,
|
||||||
SharedLinkType,
|
SharedLinkType,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
|
getConfig,
|
||||||
getMyUser,
|
getMyUser,
|
||||||
|
updateConfig,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { exiftool } from 'exiftool-vendored';
|
import { exiftool } from 'exiftool-vendored';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
@ -43,6 +45,9 @@ const TEN_TIMES = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
|
|||||||
|
|
||||||
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
const locationAssetFilepath = `${testAssetDir}/metadata/gps-position/thompson-springs.jpg`;
|
||||||
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
const ratingAssetFilepath = `${testAssetDir}/metadata/rating/mongolels.jpg`;
|
||||||
|
const facesAssetFilepath = `${testAssetDir}/metadata/faces/portrait.jpg`;
|
||||||
|
|
||||||
|
const getSystemConfig = (accessToken: string) => getConfig({ headers: asBearerAuth(accessToken) });
|
||||||
|
|
||||||
const readTags = async (bytes: Buffer, filename: string) => {
|
const readTags = async (bytes: Buffer, filename: string) => {
|
||||||
const filepath = join(tempDir, filename);
|
const filepath = join(tempDir, filename);
|
||||||
@ -71,6 +76,7 @@ describe('/asset', () => {
|
|||||||
let user2Assets: AssetMediaResponseDto[];
|
let user2Assets: AssetMediaResponseDto[];
|
||||||
let locationAsset: AssetMediaResponseDto;
|
let locationAsset: AssetMediaResponseDto;
|
||||||
let ratingAsset: AssetMediaResponseDto;
|
let ratingAsset: AssetMediaResponseDto;
|
||||||
|
let facesAsset: AssetMediaResponseDto;
|
||||||
|
|
||||||
const setupTests = async () => {
|
const setupTests = async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
@ -224,6 +230,64 @@ describe('/asset', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should get the asset faces', async () => {
|
||||||
|
const config = await getSystemConfig(admin.accessToken);
|
||||||
|
config.metadata.faces.import = true;
|
||||||
|
await updateConfig({ systemConfigDto: config }, { headers: asBearerAuth(admin.accessToken) });
|
||||||
|
|
||||||
|
// asset faces
|
||||||
|
facesAsset = await utils.createAsset(admin.accessToken, {
|
||||||
|
assetData: {
|
||||||
|
filename: 'portrait.jpg',
|
||||||
|
bytes: await readFile(facesAssetFilepath),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await utils.waitForWebsocketEvent({ event: 'assetUpload', id: facesAsset.id });
|
||||||
|
|
||||||
|
const { status, body } = await request(app)
|
||||||
|
.get(`/assets/${facesAsset.id}`)
|
||||||
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
||||||
|
expect(status).toBe(200);
|
||||||
|
expect(body.id).toEqual(facesAsset.id);
|
||||||
|
expect(body.people).toMatchObject([
|
||||||
|
{
|
||||||
|
name: 'Marie Curie',
|
||||||
|
birthDate: null,
|
||||||
|
thumbnailPath: '',
|
||||||
|
isHidden: false,
|
||||||
|
faces: [
|
||||||
|
{
|
||||||
|
imageHeight: 700,
|
||||||
|
imageWidth: 840,
|
||||||
|
boundingBoxX1: 261,
|
||||||
|
boundingBoxX2: 356,
|
||||||
|
boundingBoxY1: 146,
|
||||||
|
boundingBoxY2: 284,
|
||||||
|
sourceType: 'exif',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Pierre Curie',
|
||||||
|
birthDate: null,
|
||||||
|
thumbnailPath: '',
|
||||||
|
isHidden: false,
|
||||||
|
faces: [
|
||||||
|
{
|
||||||
|
imageHeight: 700,
|
||||||
|
imageWidth: 840,
|
||||||
|
boundingBoxX1: 536,
|
||||||
|
boundingBoxX2: 618,
|
||||||
|
boundingBoxY1: 83,
|
||||||
|
boundingBoxY2: 252,
|
||||||
|
sourceType: 'exif',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
it('should work with a shared link', async () => {
|
it('should work with a shared link', async () => {
|
||||||
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
const sharedLink = await utils.createSharedLink(user1.accessToken, {
|
||||||
type: SharedLinkType.Individual,
|
type: SharedLinkType.Individual,
|
||||||
|
@ -102,6 +102,7 @@ describe('/server-info', () => {
|
|||||||
configFile: false,
|
configFile: false,
|
||||||
duplicateDetection: false,
|
duplicateDetection: false,
|
||||||
facialRecognition: false,
|
facialRecognition: false,
|
||||||
|
importFaces: false,
|
||||||
map: true,
|
map: true,
|
||||||
reverseGeocoding: true,
|
reverseGeocoding: true,
|
||||||
oauth: false,
|
oauth: false,
|
||||||
|
@ -110,6 +110,7 @@ describe('/server', () => {
|
|||||||
facialRecognition: false,
|
facialRecognition: false,
|
||||||
map: true,
|
map: true,
|
||||||
reverseGeocoding: true,
|
reverseGeocoding: true,
|
||||||
|
importFaces: false,
|
||||||
oauth: false,
|
oauth: false,
|
||||||
oauthAutoLaunch: false,
|
oauthAutoLaunch: false,
|
||||||
passwordLogin: true,
|
passwordLogin: true,
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 4e9731d3fc270fe25901f72a6b6f57277cdb8a30
|
Subproject commit 3e057d2f58750acdf7ff281a3938e34a86cfef4d
|
@ -1,6 +1,6 @@
|
|||||||
ARG DEVICE=cpu
|
ARG DEVICE=cpu
|
||||||
|
|
||||||
FROM python:3.11-bookworm@sha256:f7543d9969bdc112dd9819ca642e14433fdacfe857f170f6b803392fc7e451ad AS builder-cpu
|
FROM python:3.11-bookworm@sha256:20c1819af5af3acba0b2b66074a2615e398ceee6842adf03cd7ad5f8d0ee3daf AS builder-cpu
|
||||||
|
|
||||||
FROM builder-cpu AS builder-openvino
|
FROM builder-cpu AS builder-openvino
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv
|
|||||||
COPY poetry.lock pyproject.toml ./
|
COPY poetry.lock pyproject.toml ./
|
||||||
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev
|
||||||
|
|
||||||
FROM python:3.11-slim-bookworm@sha256:ad5dadd957a398226996bc4846e522c39f2a77340b531b28aaab85b2d361210b AS prod-cpu
|
FROM python:3.11-slim-bookworm@sha256:ed4e985674f478c90ce879e9aa224fbb772c84e39b4aed5155b9e2280f131039 AS prod-cpu
|
||||||
|
|
||||||
FROM prod-cpu AS prod-openvino
|
FROM prod-cpu AS prod-openvino
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
FROM mambaorg/micromamba:bookworm-slim@sha256:475730daef12ff9c0733e70092aeeefdf4c373a584c952dac3f7bdb739601990 AS builder
|
FROM mambaorg/micromamba:bookworm-slim@sha256:29174348bd09352e5f1b1f6756cf1d00021487b8340fae040e91e4f98e954ce5 AS builder
|
||||||
|
|
||||||
ENV TRANSFORMERS_CACHE=/cache \
|
ENV TRANSFORMERS_CACHE=/cache \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
123
machine-learning/poetry.lock
generated
123
machine-learning/poetry.lock
generated
@ -680,13 +680,13 @@ test = ["pytest (>=6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fastapi-slim"
|
name = "fastapi-slim"
|
||||||
version = "0.112.1"
|
version = "0.112.2"
|
||||||
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "fastapi_slim-0.112.1-py3-none-any.whl", hash = "sha256:cc227cf9402d0ba54a24f80eb205c33bcb25d3ea18d53fdac3fd76ea5af8e76d"},
|
{file = "fastapi_slim-0.112.2-py3-none-any.whl", hash = "sha256:c023f74768f187af142c2fe5ff9e4ca3c4c1940bbde7df008cb283532422a23f"},
|
||||||
{file = "fastapi_slim-0.112.1.tar.gz", hash = "sha256:876ebd24e72273986709db2d469b75dc18f04c3ab9140ffd78b29d7785d26687"},
|
{file = "fastapi_slim-0.112.2.tar.gz", hash = "sha256:75b8eb0c6ee05a20270da7a527ac7ad53b83414602f42b68f7027484dab3aedb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -695,8 +695,8 @@ starlette = ">=0.37.2,<0.39.0"
|
|||||||
typing-extensions = ">=4.8.0"
|
typing-extensions = ">=4.8.0"
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
@ -1212,13 +1212,13 @@ test = ["Cython (>=0.29.24,<0.30.0)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httpx"
|
name = "httpx"
|
||||||
version = "0.27.0"
|
version = "0.27.2"
|
||||||
description = "The next generation HTTP client."
|
description = "The next generation HTTP client."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"},
|
{file = "httpx-0.27.2-py3-none-any.whl", hash = "sha256:7bb2708e112d8fdd7829cd4243970f0c223274051cb35ee80c03301ee29a3df0"},
|
||||||
{file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"},
|
{file = "httpx-0.27.2.tar.gz", hash = "sha256:f7c2be1d2f3c3c3160d441802406b206c2b76f5947b11115e6df10c6c65e66c2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -1233,6 +1233,7 @@ brotli = ["brotli", "brotlicffi"]
|
|||||||
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
http2 = ["h2 (>=3,<5)"]
|
http2 = ["h2 (>=3,<5)"]
|
||||||
socks = ["socksio (==1.*)"]
|
socks = ["socksio (==1.*)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "huggingface-hub"
|
name = "huggingface-hub"
|
||||||
@ -1530,13 +1531,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "locust"
|
name = "locust"
|
||||||
version = "2.31.3"
|
version = "2.31.5"
|
||||||
description = "Developer-friendly load testing framework"
|
description = "Developer-friendly load testing framework"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "locust-2.31.3-py3-none-any.whl", hash = "sha256:03122e007519b371a5a553d578af502826755de83551d79ea8a412ea1c660115"},
|
{file = "locust-2.31.5-py3-none-any.whl", hash = "sha256:2904ff6307d54d3202c9ebd776f9170214f6dfbe4059504dad9e3ffaca03f600"},
|
||||||
{file = "locust-2.31.3.tar.gz", hash = "sha256:25f4603f24afa11ef1ee1f26b1c86a232eb9a1140be30b2a4642c12d7a7af8ae"},
|
{file = "locust-2.31.5.tar.gz", hash = "sha256:14b2fa6f95bf248668e6dc92d100a44f06c5dcb1c26f88a5442bcaaee18faceb"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -1794,38 +1795,38 @@ files = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "mypy"
|
name = "mypy"
|
||||||
version = "1.11.1"
|
version = "1.11.2"
|
||||||
description = "Optional static typing for Python"
|
description = "Optional static typing for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
files = [
|
files = [
|
||||||
{file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
|
{file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"},
|
||||||
{file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
|
{file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"},
|
||||||
{file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
|
{file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"},
|
||||||
{file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
|
{file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"},
|
||||||
{file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
|
{file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"},
|
||||||
{file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
|
{file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"},
|
||||||
{file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
|
{file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"},
|
||||||
{file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
|
{file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"},
|
||||||
{file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
|
{file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"},
|
||||||
{file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
|
{file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"},
|
||||||
{file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
|
{file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"},
|
||||||
{file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
|
{file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"},
|
||||||
{file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
|
{file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"},
|
||||||
{file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
|
{file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"},
|
||||||
{file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
|
{file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"},
|
||||||
{file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
|
{file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"},
|
||||||
{file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
|
{file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"},
|
||||||
{file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
|
{file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"},
|
||||||
{file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
|
{file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"},
|
||||||
{file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
|
{file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"},
|
||||||
{file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
|
{file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"},
|
||||||
{file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
|
{file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"},
|
||||||
{file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
|
{file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"},
|
||||||
{file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
|
{file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"},
|
||||||
{file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
|
{file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"},
|
||||||
{file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
|
{file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"},
|
||||||
{file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
|
{file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2815,13 +2816,13 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rich"
|
name = "rich"
|
||||||
version = "13.7.1"
|
version = "13.8.0"
|
||||||
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7.0"
|
python-versions = ">=3.7.0"
|
||||||
files = [
|
files = [
|
||||||
{file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
|
{file = "rich-13.8.0-py3-none-any.whl", hash = "sha256:2e85306a063b9492dffc86278197a60cbece75bcb766022f3436f567cae11bdc"},
|
||||||
{file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
|
{file = "rich-13.8.0.tar.gz", hash = "sha256:a5ac1f1cd448ade0d59cc3356f7db7a7ccda2c8cbae9c7a90c28ff463d3e91f4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@ -2833,29 +2834,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.6.2"
|
version = "0.6.3"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"},
|
{file = "ruff-0.6.3-py3-none-linux_armv6l.whl", hash = "sha256:97f58fda4e309382ad30ede7f30e2791d70dd29ea17f41970119f55bdb7a45c3"},
|
||||||
{file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"},
|
{file = "ruff-0.6.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b061e49b5cf3a297b4d1c27ac5587954ccb4ff601160d3d6b2f70b1622194dc"},
|
||||||
{file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"},
|
{file = "ruff-0.6.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:34e2824a13bb8c668c71c1760a6ac7d795ccbd8d38ff4a0d8471fdb15de910b1"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bddfbb8d63c460f4b4128b6a506e7052bad4d6f3ff607ebbb41b0aa19c2770d1"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ced3eeb44df75353e08ab3b6a9e113b5f3f996bea48d4f7c027bc528ba87b672"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47021dff5445d549be954eb275156dfd7c37222acc1e8014311badcb9b4ec8c1"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d7bd20dc07cebd68cc8bc7b3f5ada6d637f42d947c85264f94b0d1cd9d87384"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:500f166d03fc6d0e61c8e40a3ff853fa8a43d938f5d14c183c612df1b0d6c58a"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42844ff678f9b976366b262fa2d1d1a3fe76f6e145bd92c84e27d172e3c34500"},
|
||||||
{file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"},
|
{file = "ruff-0.6.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70452a10eb2d66549de8e75f89ae82462159855e983ddff91bc0bce6511d0470"},
|
||||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"},
|
{file = "ruff-0.6.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65a533235ed55f767d1fc62193a21cbf9e3329cf26d427b800fdeacfb77d296f"},
|
||||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"},
|
{file = "ruff-0.6.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d2e2c23cef30dc3cbe9cc5d04f2899e7f5e478c40d2e0a633513ad081f7361b5"},
|
||||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"},
|
{file = "ruff-0.6.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d8a136aa7d228975a6aee3dd8bea9b28e2b43e9444aa678fb62aeb1956ff2351"},
|
||||||
{file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"},
|
{file = "ruff-0.6.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f92fe93bc72e262b7b3f2bba9879897e2d58a989b4714ba6a5a7273e842ad2f8"},
|
||||||
{file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"},
|
{file = "ruff-0.6.3-py3-none-win32.whl", hash = "sha256:7a62d3b5b0d7f9143d94893f8ba43aa5a5c51a0ffc4a401aa97a81ed76930521"},
|
||||||
{file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"},
|
{file = "ruff-0.6.3-py3-none-win_amd64.whl", hash = "sha256:746af39356fee2b89aada06c7376e1aa274a23493d7016059c3a72e3b296befb"},
|
||||||
{file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"},
|
{file = "ruff-0.6.3-py3-none-win_arm64.whl", hash = "sha256:14a9528a8b70ccc7a847637c29e56fd1f9183a9db743bbc5b8e0c4ad60592a82"},
|
||||||
{file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"},
|
{file = "ruff-0.6.3.tar.gz", hash = "sha256:183b99e9edd1ef63be34a3b51fee0a9f4ab95add123dbf89a71f7b1f0c991983"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
@ -118,8 +118,10 @@ class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ct
|
|||||||
// called when the system has to stop this worker because constraints are
|
// called when the system has to stop this worker because constraints are
|
||||||
// no longer met or the system needs resources for more important tasks
|
// no longer met or the system needs resources for more important tasks
|
||||||
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
Handler(Looper.getMainLooper()).postAtFrontOfQueue {
|
||||||
|
if (::backgroundChannel.isInitialized) {
|
||||||
backgroundChannel.invokeMethod("systemStop", null)
|
backgroundChannel.invokeMethod("systemStop", null)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
waitOnSetForegroundAsync()
|
waitOnSetForegroundAsync()
|
||||||
// cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
|
// cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException)
|
||||||
// instead, wait for 5 seconds until forcefully stopping backup work
|
// instead, wait for 5 seconds until forcefully stopping backup work
|
||||||
|
@ -252,9 +252,10 @@
|
|||||||
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
"home_page_share_err_local": "Can not share local assets via link, skipping",
|
||||||
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
"home_page_upload_err_limit": "Can only upload a maximum of 30 assets at a time, skipping",
|
||||||
"image_saved_successfully": "Image saved",
|
"image_saved_successfully": "Image saved",
|
||||||
"image_viewer_page_state_provider_download_error": "Download Error",
|
"download_error": "Download Error",
|
||||||
"image_viewer_page_state_provider_download_started": "Download Started",
|
"download_started": "Download started",
|
||||||
"image_viewer_page_state_provider_download_success": "Download Success",
|
"download_sucess": "Download success",
|
||||||
|
"download_sucess_android": "The media has been downloaded to DCIM/Immich",
|
||||||
"image_viewer_page_state_provider_share_error": "Share Error",
|
"image_viewer_page_state_provider_share_error": "Share Error",
|
||||||
"invalid_date": "Invalid date",
|
"invalid_date": "Invalid date",
|
||||||
"invalid_date_format": "Invalid date format",
|
"invalid_date_format": "Invalid date format",
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:fluttertoast/fluttertoast.dart';
|
import 'package:fluttertoast/fluttertoast.dart';
|
||||||
@ -31,19 +33,21 @@ class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
|
|||||||
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'image_viewer_page_state_provider_download_started'.tr(),
|
msg: 'download_started'.tr(),
|
||||||
toastType: ToastType.info,
|
toastType: ToastType.info,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
|
|
||||||
bool isSuccess = await _imageViewerService.downloadAssetToDevice(asset);
|
bool isSuccess = await _imageViewerService.downloadAsset(asset);
|
||||||
|
|
||||||
if (isSuccess) {
|
if (isSuccess) {
|
||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.success);
|
||||||
|
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'image_viewer_page_state_provider_download_success'.tr(),
|
msg: Platform.isAndroid
|
||||||
|
? 'download_sucess_android'.tr()
|
||||||
|
: 'download_sucess'.tr(),
|
||||||
toastType: ToastType.success,
|
toastType: ToastType.success,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
@ -52,7 +56,7 @@ class ImageViewerStateNotifier extends StateNotifier<AssetViewerPageState> {
|
|||||||
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
state = state.copyWith(downloadAssetStatus: DownloadAssetStatus.error);
|
||||||
ImmichToast.show(
|
ImmichToast.show(
|
||||||
context: context,
|
context: context,
|
||||||
msg: 'image_viewer_page_state_provider_download_error'.tr(),
|
msg: 'download_error'.tr(),
|
||||||
toastType: ToastType.error,
|
toastType: ToastType.error,
|
||||||
gravity: ToastGravity.BOTTOM,
|
gravity: ToastGravity.BOTTOM,
|
||||||
);
|
);
|
||||||
|
@ -8,7 +8,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
|
|||||||
import 'package:immich_mobile/entities/asset.entity.dart';
|
import 'package:immich_mobile/entities/asset.entity.dart';
|
||||||
import 'package:immich_mobile/entities/etag.entity.dart';
|
import 'package:immich_mobile/entities/etag.entity.dart';
|
||||||
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
import 'package:immich_mobile/entities/exif_info.entity.dart';
|
||||||
import 'package:immich_mobile/entities/store.entity.dart';
|
|
||||||
import 'package:immich_mobile/entities/user.entity.dart';
|
import 'package:immich_mobile/entities/user.entity.dart';
|
||||||
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
import 'package:immich_mobile/models/backup/backup_candidate.model.dart';
|
||||||
import 'package:immich_mobile/providers/api.provider.dart';
|
import 'package:immich_mobile/providers/api.provider.dart';
|
||||||
@ -309,18 +308,6 @@ class AssetService {
|
|||||||
useTimeFilter: false,
|
useTimeFilter: false,
|
||||||
);
|
);
|
||||||
|
|
||||||
final duplicates = await _apiService.assetsApi.checkExistingAssets(
|
|
||||||
CheckExistingAssetsDto(
|
|
||||||
deviceAssetIds: candidates.map((c) => c.asset.id).toList(),
|
|
||||||
deviceId: Store.get(StoreKey.deviceId),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (duplicates != null) {
|
|
||||||
candidates
|
|
||||||
.removeWhere((c) => !duplicates.existingIds.contains(c.asset.id));
|
|
||||||
}
|
|
||||||
|
|
||||||
await refreshRemoteAssets();
|
await refreshRemoteAssets();
|
||||||
final remoteAssets = await _db.assets
|
final remoteAssets = await _db.assets
|
||||||
.where()
|
.where()
|
||||||
|
@ -484,7 +484,7 @@ class BackupService {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (shouldSyncAlbums && !isDuplicate) {
|
if (shouldSyncAlbums) {
|
||||||
await _albumService.syncUploadAlbums(
|
await _albumService.syncUploadAlbums(
|
||||||
candidate.albumNames,
|
candidate.albumNames,
|
||||||
[responseBody['id'] as String],
|
[responseBody['id'] as String],
|
||||||
|
@ -19,7 +19,7 @@ class ImageViewerService {
|
|||||||
|
|
||||||
ImageViewerService(this._apiService);
|
ImageViewerService(this._apiService);
|
||||||
|
|
||||||
Future<bool> downloadAssetToDevice(Asset asset) async {
|
Future<bool> downloadAsset(Asset asset) async {
|
||||||
File? imageFile;
|
File? imageFile;
|
||||||
File? videoFile;
|
File? videoFile;
|
||||||
try {
|
try {
|
||||||
@ -82,18 +82,23 @@ class ImageViewerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final AssetEntity? entity;
|
final AssetEntity? entity;
|
||||||
|
final relativePath = Platform.isAndroid ? 'DCIM/Immich' : null;
|
||||||
|
|
||||||
if (asset.isImage) {
|
if (asset.isImage) {
|
||||||
entity = await PhotoManager.editor.saveImage(
|
entity = await PhotoManager.editor.saveImage(
|
||||||
res.bodyBytes,
|
res.bodyBytes,
|
||||||
title: asset.fileName,
|
title: asset.fileName,
|
||||||
|
relativePath: relativePath,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
final tempDir = await getTemporaryDirectory();
|
final tempDir = await getTemporaryDirectory();
|
||||||
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
|
videoFile = await File('${tempDir.path}/${asset.fileName}').create();
|
||||||
videoFile.writeAsBytesSync(res.bodyBytes);
|
videoFile.writeAsBytesSync(res.bodyBytes);
|
||||||
entity = await PhotoManager.editor
|
entity = await PhotoManager.editor.saveVideo(
|
||||||
.saveVideo(videoFile, title: asset.fileName);
|
videoFile,
|
||||||
|
title: asset.fileName,
|
||||||
|
relativePath: relativePath,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return entity != null;
|
return entity != null;
|
||||||
}
|
}
|
||||||
|
@ -93,6 +93,10 @@ class GalleryAppBar extends ConsumerWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleDownloadAsset() {
|
||||||
|
ref.read(imageViewerStateProvider.notifier).downloadAsset(asset, context);
|
||||||
|
}
|
||||||
|
|
||||||
return IgnorePointer(
|
return IgnorePointer(
|
||||||
ignoring: !ref.watch(showControlsProvider),
|
ignoring: !ref.watch(showControlsProvider),
|
||||||
child: AnimatedOpacity(
|
child: AnimatedOpacity(
|
||||||
@ -109,13 +113,7 @@ class GalleryAppBar extends ConsumerWidget {
|
|||||||
onFavorite: toggleFavorite,
|
onFavorite: toggleFavorite,
|
||||||
onRestorePressed: () => handleRestore(asset),
|
onRestorePressed: () => handleRestore(asset),
|
||||||
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
|
onUploadPressed: asset.isLocal ? () => handleUpload(asset) : null,
|
||||||
onDownloadPressed: asset.isLocal
|
onDownloadPressed: asset.isLocal ? null : handleDownloadAsset,
|
||||||
? null
|
|
||||||
: () =>
|
|
||||||
ref.read(imageViewerStateProvider.notifier).downloadAsset(
|
|
||||||
asset,
|
|
||||||
context,
|
|
||||||
),
|
|
||||||
onToggleMotionVideo: onToggleMotionVideo,
|
onToggleMotionVideo: onToggleMotionVideo,
|
||||||
onAddToAlbumPressed: () => addToAlbum(asset),
|
onAddToAlbumPressed: () => addToAlbum(asset),
|
||||||
onActivitiesPressed: handleActivities,
|
onActivitiesPressed: handleActivities,
|
||||||
|
4
mobile/openapi/.gitignore
vendored
4
mobile/openapi/.gitignore
vendored
@ -3,7 +3,9 @@
|
|||||||
.dart_tool/
|
.dart_tool/
|
||||||
.packages
|
.packages
|
||||||
build/
|
build/
|
||||||
pubspec.lock # Except for application packages
|
|
||||||
|
# Except for application packages
|
||||||
|
pubspec.lock
|
||||||
|
|
||||||
doc/api/
|
doc/api/
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
7.5.0
|
7.8.0
|
||||||
|
5
mobile/openapi/README.md
generated
5
mobile/openapi/README.md
generated
@ -4,7 +4,7 @@ Immich API
|
|||||||
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project:
|
||||||
|
|
||||||
- API version: 1.113.1
|
- API version: 1.113.1
|
||||||
- Generator version: 7.5.0
|
- Generator version: 7.8.0
|
||||||
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
- Build package: org.openapitools.codegen.languages.DartClientCodegen
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
@ -407,11 +407,13 @@ Class | Method | HTTP request | Description
|
|||||||
- [SignUpDto](doc//SignUpDto.md)
|
- [SignUpDto](doc//SignUpDto.md)
|
||||||
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
- [SmartInfoResponseDto](doc//SmartInfoResponseDto.md)
|
||||||
- [SmartSearchDto](doc//SmartSearchDto.md)
|
- [SmartSearchDto](doc//SmartSearchDto.md)
|
||||||
|
- [SourceType](doc//SourceType.md)
|
||||||
- [StackCreateDto](doc//StackCreateDto.md)
|
- [StackCreateDto](doc//StackCreateDto.md)
|
||||||
- [StackResponseDto](doc//StackResponseDto.md)
|
- [StackResponseDto](doc//StackResponseDto.md)
|
||||||
- [StackUpdateDto](doc//StackUpdateDto.md)
|
- [StackUpdateDto](doc//StackUpdateDto.md)
|
||||||
- [SystemConfigDto](doc//SystemConfigDto.md)
|
- [SystemConfigDto](doc//SystemConfigDto.md)
|
||||||
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
- [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md)
|
||||||
|
- [SystemConfigFacesDto](doc//SystemConfigFacesDto.md)
|
||||||
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
- [SystemConfigImageDto](doc//SystemConfigImageDto.md)
|
||||||
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
- [SystemConfigJobDto](doc//SystemConfigJobDto.md)
|
||||||
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
- [SystemConfigLibraryDto](doc//SystemConfigLibraryDto.md)
|
||||||
@ -420,6 +422,7 @@ Class | Method | HTTP request | Description
|
|||||||
- [SystemConfigLoggingDto](doc//SystemConfigLoggingDto.md)
|
- [SystemConfigLoggingDto](doc//SystemConfigLoggingDto.md)
|
||||||
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
|
- [SystemConfigMachineLearningDto](doc//SystemConfigMachineLearningDto.md)
|
||||||
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
|
- [SystemConfigMapDto](doc//SystemConfigMapDto.md)
|
||||||
|
- [SystemConfigMetadataDto](doc//SystemConfigMetadataDto.md)
|
||||||
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
|
- [SystemConfigNewVersionCheckDto](doc//SystemConfigNewVersionCheckDto.md)
|
||||||
- [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md)
|
- [SystemConfigNotificationsDto](doc//SystemConfigNotificationsDto.md)
|
||||||
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
|
- [SystemConfigOAuthDto](doc//SystemConfigOAuthDto.md)
|
||||||
|
3
mobile/openapi/lib/api.dart
generated
3
mobile/openapi/lib/api.dart
generated
@ -221,11 +221,13 @@ part 'model/shared_link_type.dart';
|
|||||||
part 'model/sign_up_dto.dart';
|
part 'model/sign_up_dto.dart';
|
||||||
part 'model/smart_info_response_dto.dart';
|
part 'model/smart_info_response_dto.dart';
|
||||||
part 'model/smart_search_dto.dart';
|
part 'model/smart_search_dto.dart';
|
||||||
|
part 'model/source_type.dart';
|
||||||
part 'model/stack_create_dto.dart';
|
part 'model/stack_create_dto.dart';
|
||||||
part 'model/stack_response_dto.dart';
|
part 'model/stack_response_dto.dart';
|
||||||
part 'model/stack_update_dto.dart';
|
part 'model/stack_update_dto.dart';
|
||||||
part 'model/system_config_dto.dart';
|
part 'model/system_config_dto.dart';
|
||||||
part 'model/system_config_f_fmpeg_dto.dart';
|
part 'model/system_config_f_fmpeg_dto.dart';
|
||||||
|
part 'model/system_config_faces_dto.dart';
|
||||||
part 'model/system_config_image_dto.dart';
|
part 'model/system_config_image_dto.dart';
|
||||||
part 'model/system_config_job_dto.dart';
|
part 'model/system_config_job_dto.dart';
|
||||||
part 'model/system_config_library_dto.dart';
|
part 'model/system_config_library_dto.dart';
|
||||||
@ -234,6 +236,7 @@ part 'model/system_config_library_watch_dto.dart';
|
|||||||
part 'model/system_config_logging_dto.dart';
|
part 'model/system_config_logging_dto.dart';
|
||||||
part 'model/system_config_machine_learning_dto.dart';
|
part 'model/system_config_machine_learning_dto.dart';
|
||||||
part 'model/system_config_map_dto.dart';
|
part 'model/system_config_map_dto.dart';
|
||||||
|
part 'model/system_config_metadata_dto.dart';
|
||||||
part 'model/system_config_new_version_check_dto.dart';
|
part 'model/system_config_new_version_check_dto.dart';
|
||||||
part 'model/system_config_notifications_dto.dart';
|
part 'model/system_config_notifications_dto.dart';
|
||||||
part 'model/system_config_o_auth_dto.dart';
|
part 'model/system_config_o_auth_dto.dart';
|
||||||
|
6
mobile/openapi/lib/api_client.dart
generated
6
mobile/openapi/lib/api_client.dart
generated
@ -497,6 +497,8 @@ class ApiClient {
|
|||||||
return SmartInfoResponseDto.fromJson(value);
|
return SmartInfoResponseDto.fromJson(value);
|
||||||
case 'SmartSearchDto':
|
case 'SmartSearchDto':
|
||||||
return SmartSearchDto.fromJson(value);
|
return SmartSearchDto.fromJson(value);
|
||||||
|
case 'SourceType':
|
||||||
|
return SourceTypeTypeTransformer().decode(value);
|
||||||
case 'StackCreateDto':
|
case 'StackCreateDto':
|
||||||
return StackCreateDto.fromJson(value);
|
return StackCreateDto.fromJson(value);
|
||||||
case 'StackResponseDto':
|
case 'StackResponseDto':
|
||||||
@ -507,6 +509,8 @@ class ApiClient {
|
|||||||
return SystemConfigDto.fromJson(value);
|
return SystemConfigDto.fromJson(value);
|
||||||
case 'SystemConfigFFmpegDto':
|
case 'SystemConfigFFmpegDto':
|
||||||
return SystemConfigFFmpegDto.fromJson(value);
|
return SystemConfigFFmpegDto.fromJson(value);
|
||||||
|
case 'SystemConfigFacesDto':
|
||||||
|
return SystemConfigFacesDto.fromJson(value);
|
||||||
case 'SystemConfigImageDto':
|
case 'SystemConfigImageDto':
|
||||||
return SystemConfigImageDto.fromJson(value);
|
return SystemConfigImageDto.fromJson(value);
|
||||||
case 'SystemConfigJobDto':
|
case 'SystemConfigJobDto':
|
||||||
@ -523,6 +527,8 @@ class ApiClient {
|
|||||||
return SystemConfigMachineLearningDto.fromJson(value);
|
return SystemConfigMachineLearningDto.fromJson(value);
|
||||||
case 'SystemConfigMapDto':
|
case 'SystemConfigMapDto':
|
||||||
return SystemConfigMapDto.fromJson(value);
|
return SystemConfigMapDto.fromJson(value);
|
||||||
|
case 'SystemConfigMetadataDto':
|
||||||
|
return SystemConfigMetadataDto.fromJson(value);
|
||||||
case 'SystemConfigNewVersionCheckDto':
|
case 'SystemConfigNewVersionCheckDto':
|
||||||
return SystemConfigNewVersionCheckDto.fromJson(value);
|
return SystemConfigNewVersionCheckDto.fromJson(value);
|
||||||
case 'SystemConfigNotificationsDto':
|
case 'SystemConfigNotificationsDto':
|
||||||
|
3
mobile/openapi/lib/api_helper.dart
generated
3
mobile/openapi/lib/api_helper.dart
generated
@ -127,6 +127,9 @@ String parameterToString(dynamic value) {
|
|||||||
if (value is SharedLinkType) {
|
if (value is SharedLinkType) {
|
||||||
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
return SharedLinkTypeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
if (value is SourceType) {
|
||||||
|
return SourceTypeTypeTransformer().encode(value).toString();
|
||||||
|
}
|
||||||
if (value is TimeBucketSize) {
|
if (value is TimeBucketSize) {
|
||||||
return TimeBucketSizeTypeTransformer().encode(value).toString();
|
return TimeBucketSizeTypeTransformer().encode(value).toString();
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ class AssetFaceResponseDto {
|
|||||||
required this.imageHeight,
|
required this.imageHeight,
|
||||||
required this.imageWidth,
|
required this.imageWidth,
|
||||||
required this.person,
|
required this.person,
|
||||||
|
this.sourceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
int boundingBoxX1;
|
int boundingBoxX1;
|
||||||
@ -39,6 +40,14 @@ class AssetFaceResponseDto {
|
|||||||
|
|
||||||
PersonResponseDto? person;
|
PersonResponseDto? person;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
SourceType? sourceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetFaceResponseDto &&
|
||||||
other.boundingBoxX1 == boundingBoxX1 &&
|
other.boundingBoxX1 == boundingBoxX1 &&
|
||||||
@ -48,7 +57,8 @@ class AssetFaceResponseDto {
|
|||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.imageHeight == imageHeight &&
|
other.imageHeight == imageHeight &&
|
||||||
other.imageWidth == imageWidth &&
|
other.imageWidth == imageWidth &&
|
||||||
other.person == person;
|
other.person == person &&
|
||||||
|
other.sourceType == sourceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -60,10 +70,11 @@ class AssetFaceResponseDto {
|
|||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(imageHeight.hashCode) +
|
(imageHeight.hashCode) +
|
||||||
(imageWidth.hashCode) +
|
(imageWidth.hashCode) +
|
||||||
(person == null ? 0 : person!.hashCode);
|
(person == null ? 0 : person!.hashCode) +
|
||||||
|
(sourceType == null ? 0 : sourceType!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person]';
|
String toString() => 'AssetFaceResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, person=$person, sourceType=$sourceType]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -79,6 +90,11 @@ class AssetFaceResponseDto {
|
|||||||
} else {
|
} else {
|
||||||
// json[r'person'] = null;
|
// json[r'person'] = null;
|
||||||
}
|
}
|
||||||
|
if (this.sourceType != null) {
|
||||||
|
json[r'sourceType'] = this.sourceType;
|
||||||
|
} else {
|
||||||
|
// json[r'sourceType'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,6 +114,7 @@ class AssetFaceResponseDto {
|
|||||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||||
person: PersonResponseDto.fromJson(json[r'person']),
|
person: PersonResponseDto.fromJson(json[r'person']),
|
||||||
|
sourceType: SourceType.fromJson(json[r'sourceType']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -20,6 +20,7 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
required this.id,
|
required this.id,
|
||||||
required this.imageHeight,
|
required this.imageHeight,
|
||||||
required this.imageWidth,
|
required this.imageWidth,
|
||||||
|
this.sourceType,
|
||||||
});
|
});
|
||||||
|
|
||||||
int boundingBoxX1;
|
int boundingBoxX1;
|
||||||
@ -36,6 +37,14 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
|
|
||||||
int imageWidth;
|
int imageWidth;
|
||||||
|
|
||||||
|
///
|
||||||
|
/// Please note: This property should have been non-nullable! Since the specification file
|
||||||
|
/// does not include a default value (using the "default:" property), however, the generated
|
||||||
|
/// source code must fall back to having a nullable type.
|
||||||
|
/// Consider adding a "default:" property in the specification file to hide this note.
|
||||||
|
///
|
||||||
|
SourceType? sourceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
|
bool operator ==(Object other) => identical(this, other) || other is AssetFaceWithoutPersonResponseDto &&
|
||||||
other.boundingBoxX1 == boundingBoxX1 &&
|
other.boundingBoxX1 == boundingBoxX1 &&
|
||||||
@ -44,7 +53,8 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
other.boundingBoxY2 == boundingBoxY2 &&
|
other.boundingBoxY2 == boundingBoxY2 &&
|
||||||
other.id == id &&
|
other.id == id &&
|
||||||
other.imageHeight == imageHeight &&
|
other.imageHeight == imageHeight &&
|
||||||
other.imageWidth == imageWidth;
|
other.imageWidth == imageWidth &&
|
||||||
|
other.sourceType == sourceType;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
int get hashCode =>
|
int get hashCode =>
|
||||||
@ -55,10 +65,11 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
(boundingBoxY2.hashCode) +
|
(boundingBoxY2.hashCode) +
|
||||||
(id.hashCode) +
|
(id.hashCode) +
|
||||||
(imageHeight.hashCode) +
|
(imageHeight.hashCode) +
|
||||||
(imageWidth.hashCode);
|
(imageWidth.hashCode) +
|
||||||
|
(sourceType == null ? 0 : sourceType!.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth]';
|
String toString() => 'AssetFaceWithoutPersonResponseDto[boundingBoxX1=$boundingBoxX1, boundingBoxX2=$boundingBoxX2, boundingBoxY1=$boundingBoxY1, boundingBoxY2=$boundingBoxY2, id=$id, imageHeight=$imageHeight, imageWidth=$imageWidth, sourceType=$sourceType]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -69,6 +80,11 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
json[r'id'] = this.id;
|
json[r'id'] = this.id;
|
||||||
json[r'imageHeight'] = this.imageHeight;
|
json[r'imageHeight'] = this.imageHeight;
|
||||||
json[r'imageWidth'] = this.imageWidth;
|
json[r'imageWidth'] = this.imageWidth;
|
||||||
|
if (this.sourceType != null) {
|
||||||
|
json[r'sourceType'] = this.sourceType;
|
||||||
|
} else {
|
||||||
|
// json[r'sourceType'] = null;
|
||||||
|
}
|
||||||
return json;
|
return json;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,6 +103,7 @@ class AssetFaceWithoutPersonResponseDto {
|
|||||||
id: mapValueOfType<String>(json, r'id')!,
|
id: mapValueOfType<String>(json, r'id')!,
|
||||||
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
imageHeight: mapValueOfType<int>(json, r'imageHeight')!,
|
||||||
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
imageWidth: mapValueOfType<int>(json, r'imageWidth')!,
|
||||||
|
sourceType: SourceType.fromJson(json[r'sourceType']),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
10
mobile/openapi/lib/model/server_features_dto.dart
generated
10
mobile/openapi/lib/model/server_features_dto.dart
generated
@ -17,6 +17,7 @@ class ServerFeaturesDto {
|
|||||||
required this.duplicateDetection,
|
required this.duplicateDetection,
|
||||||
required this.email,
|
required this.email,
|
||||||
required this.facialRecognition,
|
required this.facialRecognition,
|
||||||
|
required this.importFaces,
|
||||||
required this.map,
|
required this.map,
|
||||||
required this.oauth,
|
required this.oauth,
|
||||||
required this.oauthAutoLaunch,
|
required this.oauthAutoLaunch,
|
||||||
@ -36,6 +37,8 @@ class ServerFeaturesDto {
|
|||||||
|
|
||||||
bool facialRecognition;
|
bool facialRecognition;
|
||||||
|
|
||||||
|
bool importFaces;
|
||||||
|
|
||||||
bool map;
|
bool map;
|
||||||
|
|
||||||
bool oauth;
|
bool oauth;
|
||||||
@ -60,6 +63,7 @@ class ServerFeaturesDto {
|
|||||||
other.duplicateDetection == duplicateDetection &&
|
other.duplicateDetection == duplicateDetection &&
|
||||||
other.email == email &&
|
other.email == email &&
|
||||||
other.facialRecognition == facialRecognition &&
|
other.facialRecognition == facialRecognition &&
|
||||||
|
other.importFaces == importFaces &&
|
||||||
other.map == map &&
|
other.map == map &&
|
||||||
other.oauth == oauth &&
|
other.oauth == oauth &&
|
||||||
other.oauthAutoLaunch == oauthAutoLaunch &&
|
other.oauthAutoLaunch == oauthAutoLaunch &&
|
||||||
@ -77,6 +81,7 @@ class ServerFeaturesDto {
|
|||||||
(duplicateDetection.hashCode) +
|
(duplicateDetection.hashCode) +
|
||||||
(email.hashCode) +
|
(email.hashCode) +
|
||||||
(facialRecognition.hashCode) +
|
(facialRecognition.hashCode) +
|
||||||
|
(importFaces.hashCode) +
|
||||||
(map.hashCode) +
|
(map.hashCode) +
|
||||||
(oauth.hashCode) +
|
(oauth.hashCode) +
|
||||||
(oauthAutoLaunch.hashCode) +
|
(oauthAutoLaunch.hashCode) +
|
||||||
@ -88,7 +93,7 @@ class ServerFeaturesDto {
|
|||||||
(trash.hashCode);
|
(trash.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
|
String toString() => 'ServerFeaturesDto[configFile=$configFile, duplicateDetection=$duplicateDetection, email=$email, facialRecognition=$facialRecognition, importFaces=$importFaces, map=$map, oauth=$oauth, oauthAutoLaunch=$oauthAutoLaunch, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, search=$search, sidecar=$sidecar, smartSearch=$smartSearch, trash=$trash]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -96,6 +101,7 @@ class ServerFeaturesDto {
|
|||||||
json[r'duplicateDetection'] = this.duplicateDetection;
|
json[r'duplicateDetection'] = this.duplicateDetection;
|
||||||
json[r'email'] = this.email;
|
json[r'email'] = this.email;
|
||||||
json[r'facialRecognition'] = this.facialRecognition;
|
json[r'facialRecognition'] = this.facialRecognition;
|
||||||
|
json[r'importFaces'] = this.importFaces;
|
||||||
json[r'map'] = this.map;
|
json[r'map'] = this.map;
|
||||||
json[r'oauth'] = this.oauth;
|
json[r'oauth'] = this.oauth;
|
||||||
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
|
json[r'oauthAutoLaunch'] = this.oauthAutoLaunch;
|
||||||
@ -120,6 +126,7 @@ class ServerFeaturesDto {
|
|||||||
duplicateDetection: mapValueOfType<bool>(json, r'duplicateDetection')!,
|
duplicateDetection: mapValueOfType<bool>(json, r'duplicateDetection')!,
|
||||||
email: mapValueOfType<bool>(json, r'email')!,
|
email: mapValueOfType<bool>(json, r'email')!,
|
||||||
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
|
facialRecognition: mapValueOfType<bool>(json, r'facialRecognition')!,
|
||||||
|
importFaces: mapValueOfType<bool>(json, r'importFaces')!,
|
||||||
map: mapValueOfType<bool>(json, r'map')!,
|
map: mapValueOfType<bool>(json, r'map')!,
|
||||||
oauth: mapValueOfType<bool>(json, r'oauth')!,
|
oauth: mapValueOfType<bool>(json, r'oauth')!,
|
||||||
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
|
oauthAutoLaunch: mapValueOfType<bool>(json, r'oauthAutoLaunch')!,
|
||||||
@ -180,6 +187,7 @@ class ServerFeaturesDto {
|
|||||||
'duplicateDetection',
|
'duplicateDetection',
|
||||||
'email',
|
'email',
|
||||||
'facialRecognition',
|
'facialRecognition',
|
||||||
|
'importFaces',
|
||||||
'map',
|
'map',
|
||||||
'oauth',
|
'oauth',
|
||||||
'oauthAutoLaunch',
|
'oauthAutoLaunch',
|
||||||
|
85
mobile/openapi/lib/model/source_type.dart
generated
Normal file
85
mobile/openapi/lib/model/source_type.dart
generated
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
|
||||||
|
class SourceType {
|
||||||
|
/// Instantiate a new enum with the provided [value].
|
||||||
|
const SourceType._(this.value);
|
||||||
|
|
||||||
|
/// The underlying value of this enum member.
|
||||||
|
final String value;
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => value;
|
||||||
|
|
||||||
|
String toJson() => value;
|
||||||
|
|
||||||
|
static const machineLearning = SourceType._(r'machine-learning');
|
||||||
|
static const exif = SourceType._(r'exif');
|
||||||
|
|
||||||
|
/// List of all possible values in this [enum][SourceType].
|
||||||
|
static const values = <SourceType>[
|
||||||
|
machineLearning,
|
||||||
|
exif,
|
||||||
|
];
|
||||||
|
|
||||||
|
static SourceType? fromJson(dynamic value) => SourceTypeTypeTransformer().decode(value);
|
||||||
|
|
||||||
|
static List<SourceType> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SourceType>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SourceType.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Transformation class that can [encode] an instance of [SourceType] to String,
|
||||||
|
/// and [decode] dynamic data back to [SourceType].
|
||||||
|
class SourceTypeTypeTransformer {
|
||||||
|
factory SourceTypeTypeTransformer() => _instance ??= const SourceTypeTypeTransformer._();
|
||||||
|
|
||||||
|
const SourceTypeTypeTransformer._();
|
||||||
|
|
||||||
|
String encode(SourceType data) => data.value;
|
||||||
|
|
||||||
|
/// Decodes a [dynamic value][data] to a SourceType.
|
||||||
|
///
|
||||||
|
/// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully,
|
||||||
|
/// then null is returned. However, if [allowNull] is false and the [dynamic value][data]
|
||||||
|
/// cannot be decoded successfully, then an [UnimplementedError] is thrown.
|
||||||
|
///
|
||||||
|
/// The [allowNull] is very handy when an API changes and a new enum value is added or removed,
|
||||||
|
/// and users are still using an old app with the old code.
|
||||||
|
SourceType? decode(dynamic data, {bool allowNull = true}) {
|
||||||
|
if (data != null) {
|
||||||
|
switch (data) {
|
||||||
|
case r'machine-learning': return SourceType.machineLearning;
|
||||||
|
case r'exif': return SourceType.exif;
|
||||||
|
default:
|
||||||
|
if (!allowNull) {
|
||||||
|
throw ArgumentError('Unknown enum value to decode: $data');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Singleton [SourceTypeTypeTransformer] instance.
|
||||||
|
static SourceTypeTypeTransformer? _instance;
|
||||||
|
}
|
||||||
|
|
10
mobile/openapi/lib/model/system_config_dto.dart
generated
10
mobile/openapi/lib/model/system_config_dto.dart
generated
@ -20,6 +20,7 @@ class SystemConfigDto {
|
|||||||
required this.logging,
|
required this.logging,
|
||||||
required this.machineLearning,
|
required this.machineLearning,
|
||||||
required this.map,
|
required this.map,
|
||||||
|
required this.metadata,
|
||||||
required this.newVersionCheck,
|
required this.newVersionCheck,
|
||||||
required this.notifications,
|
required this.notifications,
|
||||||
required this.oauth,
|
required this.oauth,
|
||||||
@ -46,6 +47,8 @@ class SystemConfigDto {
|
|||||||
|
|
||||||
SystemConfigMapDto map;
|
SystemConfigMapDto map;
|
||||||
|
|
||||||
|
SystemConfigMetadataDto metadata;
|
||||||
|
|
||||||
SystemConfigNewVersionCheckDto newVersionCheck;
|
SystemConfigNewVersionCheckDto newVersionCheck;
|
||||||
|
|
||||||
SystemConfigNotificationsDto notifications;
|
SystemConfigNotificationsDto notifications;
|
||||||
@ -75,6 +78,7 @@ class SystemConfigDto {
|
|||||||
other.logging == logging &&
|
other.logging == logging &&
|
||||||
other.machineLearning == machineLearning &&
|
other.machineLearning == machineLearning &&
|
||||||
other.map == map &&
|
other.map == map &&
|
||||||
|
other.metadata == metadata &&
|
||||||
other.newVersionCheck == newVersionCheck &&
|
other.newVersionCheck == newVersionCheck &&
|
||||||
other.notifications == notifications &&
|
other.notifications == notifications &&
|
||||||
other.oauth == oauth &&
|
other.oauth == oauth &&
|
||||||
@ -96,6 +100,7 @@ class SystemConfigDto {
|
|||||||
(logging.hashCode) +
|
(logging.hashCode) +
|
||||||
(machineLearning.hashCode) +
|
(machineLearning.hashCode) +
|
||||||
(map.hashCode) +
|
(map.hashCode) +
|
||||||
|
(metadata.hashCode) +
|
||||||
(newVersionCheck.hashCode) +
|
(newVersionCheck.hashCode) +
|
||||||
(notifications.hashCode) +
|
(notifications.hashCode) +
|
||||||
(oauth.hashCode) +
|
(oauth.hashCode) +
|
||||||
@ -108,7 +113,7 @@ class SystemConfigDto {
|
|||||||
(user.hashCode);
|
(user.hashCode);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]';
|
String toString() => 'SystemConfigDto[ffmpeg=$ffmpeg, image=$image, job=$job, library_=$library_, logging=$logging, machineLearning=$machineLearning, map=$map, metadata=$metadata, newVersionCheck=$newVersionCheck, notifications=$notifications, oauth=$oauth, passwordLogin=$passwordLogin, reverseGeocoding=$reverseGeocoding, server=$server, storageTemplate=$storageTemplate, theme=$theme, trash=$trash, user=$user]';
|
||||||
|
|
||||||
Map<String, dynamic> toJson() {
|
Map<String, dynamic> toJson() {
|
||||||
final json = <String, dynamic>{};
|
final json = <String, dynamic>{};
|
||||||
@ -119,6 +124,7 @@ class SystemConfigDto {
|
|||||||
json[r'logging'] = this.logging;
|
json[r'logging'] = this.logging;
|
||||||
json[r'machineLearning'] = this.machineLearning;
|
json[r'machineLearning'] = this.machineLearning;
|
||||||
json[r'map'] = this.map;
|
json[r'map'] = this.map;
|
||||||
|
json[r'metadata'] = this.metadata;
|
||||||
json[r'newVersionCheck'] = this.newVersionCheck;
|
json[r'newVersionCheck'] = this.newVersionCheck;
|
||||||
json[r'notifications'] = this.notifications;
|
json[r'notifications'] = this.notifications;
|
||||||
json[r'oauth'] = this.oauth;
|
json[r'oauth'] = this.oauth;
|
||||||
@ -147,6 +153,7 @@ class SystemConfigDto {
|
|||||||
logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!,
|
logging: SystemConfigLoggingDto.fromJson(json[r'logging'])!,
|
||||||
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
|
machineLearning: SystemConfigMachineLearningDto.fromJson(json[r'machineLearning'])!,
|
||||||
map: SystemConfigMapDto.fromJson(json[r'map'])!,
|
map: SystemConfigMapDto.fromJson(json[r'map'])!,
|
||||||
|
metadata: SystemConfigMetadataDto.fromJson(json[r'metadata'])!,
|
||||||
newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!,
|
newVersionCheck: SystemConfigNewVersionCheckDto.fromJson(json[r'newVersionCheck'])!,
|
||||||
notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!,
|
notifications: SystemConfigNotificationsDto.fromJson(json[r'notifications'])!,
|
||||||
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
|
oauth: SystemConfigOAuthDto.fromJson(json[r'oauth'])!,
|
||||||
@ -211,6 +218,7 @@ class SystemConfigDto {
|
|||||||
'logging',
|
'logging',
|
||||||
'machineLearning',
|
'machineLearning',
|
||||||
'map',
|
'map',
|
||||||
|
'metadata',
|
||||||
'newVersionCheck',
|
'newVersionCheck',
|
||||||
'notifications',
|
'notifications',
|
||||||
'oauth',
|
'oauth',
|
||||||
|
98
mobile/openapi/lib/model/system_config_faces_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/system_config_faces_dto.dart
generated
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SystemConfigFacesDto {
|
||||||
|
/// Returns a new [SystemConfigFacesDto] instance.
|
||||||
|
SystemConfigFacesDto({
|
||||||
|
required this.import_,
|
||||||
|
});
|
||||||
|
|
||||||
|
bool import_;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigFacesDto &&
|
||||||
|
other.import_ == import_;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(import_.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SystemConfigFacesDto[import_=$import_]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'import'] = this.import_;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SystemConfigFacesDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SystemConfigFacesDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SystemConfigFacesDto(
|
||||||
|
import_: mapValueOfType<bool>(json, r'import')!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SystemConfigFacesDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SystemConfigFacesDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SystemConfigFacesDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SystemConfigFacesDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SystemConfigFacesDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SystemConfigFacesDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SystemConfigFacesDto-objects as value to a dart map
|
||||||
|
static Map<String, List<SystemConfigFacesDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SystemConfigFacesDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SystemConfigFacesDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'import',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
98
mobile/openapi/lib/model/system_config_metadata_dto.dart
generated
Normal file
98
mobile/openapi/lib/model/system_config_metadata_dto.dart
generated
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
//
|
||||||
|
// AUTO-GENERATED FILE, DO NOT MODIFY!
|
||||||
|
//
|
||||||
|
// @dart=2.18
|
||||||
|
|
||||||
|
// ignore_for_file: unused_element, unused_import
|
||||||
|
// ignore_for_file: always_put_required_named_parameters_first
|
||||||
|
// ignore_for_file: constant_identifier_names
|
||||||
|
// ignore_for_file: lines_longer_than_80_chars
|
||||||
|
|
||||||
|
part of openapi.api;
|
||||||
|
|
||||||
|
class SystemConfigMetadataDto {
|
||||||
|
/// Returns a new [SystemConfigMetadataDto] instance.
|
||||||
|
SystemConfigMetadataDto({
|
||||||
|
required this.faces,
|
||||||
|
});
|
||||||
|
|
||||||
|
SystemConfigFacesDto faces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) => identical(this, other) || other is SystemConfigMetadataDto &&
|
||||||
|
other.faces == faces;
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode =>
|
||||||
|
// ignore: unnecessary_parenthesis
|
||||||
|
(faces.hashCode);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() => 'SystemConfigMetadataDto[faces=$faces]';
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() {
|
||||||
|
final json = <String, dynamic>{};
|
||||||
|
json[r'faces'] = this.faces;
|
||||||
|
return json;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a new [SystemConfigMetadataDto] instance and imports its values from
|
||||||
|
/// [value] if it's a [Map], null otherwise.
|
||||||
|
// ignore: prefer_constructors_over_static_methods
|
||||||
|
static SystemConfigMetadataDto? fromJson(dynamic value) {
|
||||||
|
if (value is Map) {
|
||||||
|
final json = value.cast<String, dynamic>();
|
||||||
|
|
||||||
|
return SystemConfigMetadataDto(
|
||||||
|
faces: SystemConfigFacesDto.fromJson(json[r'faces'])!,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<SystemConfigMetadataDto> listFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final result = <SystemConfigMetadataDto>[];
|
||||||
|
if (json is List && json.isNotEmpty) {
|
||||||
|
for (final row in json) {
|
||||||
|
final value = SystemConfigMetadataDto.fromJson(row);
|
||||||
|
if (value != null) {
|
||||||
|
result.add(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.toList(growable: growable);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Map<String, SystemConfigMetadataDto> mapFromJson(dynamic json) {
|
||||||
|
final map = <String, SystemConfigMetadataDto>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
json = json.cast<String, dynamic>(); // ignore: parameter_assignments
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
final value = SystemConfigMetadataDto.fromJson(entry.value);
|
||||||
|
if (value != null) {
|
||||||
|
map[entry.key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
// maps a json object with a list of SystemConfigMetadataDto-objects as value to a dart map
|
||||||
|
static Map<String, List<SystemConfigMetadataDto>> mapListFromJson(dynamic json, {bool growable = false,}) {
|
||||||
|
final map = <String, List<SystemConfigMetadataDto>>{};
|
||||||
|
if (json is Map && json.isNotEmpty) {
|
||||||
|
// ignore: parameter_assignments
|
||||||
|
json = json.cast<String, dynamic>();
|
||||||
|
for (final entry in json.entries) {
|
||||||
|
map[entry.key] = SystemConfigMetadataDto.listFromJson(entry.value, growable: growable,);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The list of required keys that must be present in a JSON.
|
||||||
|
static const requiredKeys = <String>{
|
||||||
|
'faces',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -7,11 +7,11 @@ version: '1.0.0'
|
|||||||
description: 'OpenAPI API client'
|
description: 'OpenAPI API client'
|
||||||
homepage: 'homepage'
|
homepage: 'homepage'
|
||||||
environment:
|
environment:
|
||||||
sdk: '>=2.12.0 <3.0.0'
|
sdk: '>=2.12.0 <4.0.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
collection: '^1.17.0'
|
collection: '>=1.17.0 <2.0.0'
|
||||||
http: '>=0.13.0 <0.14.0'
|
http: '>=0.13.0 <2.0.0'
|
||||||
intl: any
|
intl: any
|
||||||
meta: '^1.1.8'
|
meta: '>=1.1.8 <2.0.0'
|
||||||
immich_mobile:
|
immich_mobile:
|
||||||
path: ../
|
path: ../
|
||||||
|
@ -1737,10 +1737,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc
|
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.2.4"
|
version: "14.2.5"
|
||||||
wakelock_plus:
|
wakelock_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
OPENAPI_GENERATOR_VERSION=v7.5.0
|
OPENAPI_GENERATOR_VERSION=v7.8.0
|
||||||
|
|
||||||
# usage: ./bin/generate-open-api.sh
|
# usage: ./bin/generate-open-api.sh
|
||||||
|
|
||||||
|
@ -8018,6 +8018,9 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"nullable": true
|
"nullable": true
|
||||||
|
},
|
||||||
|
"sourceType": {
|
||||||
|
"$ref": "#/components/schemas/SourceType"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -8086,6 +8089,9 @@
|
|||||||
},
|
},
|
||||||
"imageWidth": {
|
"imageWidth": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"sourceType": {
|
||||||
|
"$ref": "#/components/schemas/SourceType"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -10688,6 +10694,9 @@
|
|||||||
"facialRecognition": {
|
"facialRecognition": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
|
"importFaces": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
"map": {
|
"map": {
|
||||||
"type": "boolean"
|
"type": "boolean"
|
||||||
},
|
},
|
||||||
@ -10721,6 +10730,7 @@
|
|||||||
"duplicateDetection",
|
"duplicateDetection",
|
||||||
"email",
|
"email",
|
||||||
"facialRecognition",
|
"facialRecognition",
|
||||||
|
"importFaces",
|
||||||
"map",
|
"map",
|
||||||
"oauth",
|
"oauth",
|
||||||
"oauthAutoLaunch",
|
"oauthAutoLaunch",
|
||||||
@ -11229,6 +11239,13 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SourceType": {
|
||||||
|
"enum": [
|
||||||
|
"machine-learning",
|
||||||
|
"exif"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
"StackCreateDto": {
|
"StackCreateDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"assetIds": {
|
"assetIds": {
|
||||||
@ -11299,6 +11316,9 @@
|
|||||||
"map": {
|
"map": {
|
||||||
"$ref": "#/components/schemas/SystemConfigMapDto"
|
"$ref": "#/components/schemas/SystemConfigMapDto"
|
||||||
},
|
},
|
||||||
|
"metadata": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigMetadataDto"
|
||||||
|
},
|
||||||
"newVersionCheck": {
|
"newVersionCheck": {
|
||||||
"$ref": "#/components/schemas/SystemConfigNewVersionCheckDto"
|
"$ref": "#/components/schemas/SystemConfigNewVersionCheckDto"
|
||||||
},
|
},
|
||||||
@ -11338,6 +11358,7 @@
|
|||||||
"logging",
|
"logging",
|
||||||
"machineLearning",
|
"machineLearning",
|
||||||
"map",
|
"map",
|
||||||
|
"metadata",
|
||||||
"newVersionCheck",
|
"newVersionCheck",
|
||||||
"notifications",
|
"notifications",
|
||||||
"oauth",
|
"oauth",
|
||||||
@ -11464,6 +11485,17 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SystemConfigFacesDto": {
|
||||||
|
"properties": {
|
||||||
|
"import": {
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"import"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SystemConfigImageDto": {
|
"SystemConfigImageDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"colorspace": {
|
"colorspace": {
|
||||||
@ -11656,6 +11688,17 @@
|
|||||||
],
|
],
|
||||||
"type": "object"
|
"type": "object"
|
||||||
},
|
},
|
||||||
|
"SystemConfigMetadataDto": {
|
||||||
|
"properties": {
|
||||||
|
"faces": {
|
||||||
|
"$ref": "#/components/schemas/SystemConfigFacesDto"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"faces"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
"SystemConfigNewVersionCheckDto": {
|
"SystemConfigNewVersionCheckDto": {
|
||||||
"properties": {
|
"properties": {
|
||||||
"enabled": {
|
"enabled": {
|
||||||
|
@ -2,6 +2,6 @@
|
|||||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||||
"spaces": 2,
|
"spaces": 2,
|
||||||
"generator-cli": {
|
"generator-cli": {
|
||||||
"version": "7.5.0"
|
"version": "7.8.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Include code from immich_mobile
|
# Include code from immich_mobile
|
||||||
@@ -13,5 +13,5 @@
|
@@ -13,5 +13,5 @@
|
||||||
http: '>=0.13.0 <0.14.0'
|
http: '>=0.13.0 <2.0.0'
|
||||||
intl: any
|
intl: any
|
||||||
meta: '^1.1.8'
|
meta: '>=1.1.8 <2.0.0'
|
||||||
-dev_dependencies:
|
-dev_dependencies:
|
||||||
- test: '>=1.21.6 <1.22.0'
|
- test: '>=1.21.6 <1.22.0'
|
||||||
+ immich_mobile:
|
+ immich_mobile:
|
||||||
|
@ -207,6 +207,7 @@ export type AssetFaceWithoutPersonResponseDto = {
|
|||||||
id: string;
|
id: string;
|
||||||
imageHeight: number;
|
imageHeight: number;
|
||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
|
sourceType?: SourceType;
|
||||||
};
|
};
|
||||||
export type PersonWithFacesResponseDto = {
|
export type PersonWithFacesResponseDto = {
|
||||||
birthDate: string | null;
|
birthDate: string | null;
|
||||||
@ -508,6 +509,7 @@ export type AssetFaceResponseDto = {
|
|||||||
imageHeight: number;
|
imageHeight: number;
|
||||||
imageWidth: number;
|
imageWidth: number;
|
||||||
person: (PersonResponseDto) | null;
|
person: (PersonResponseDto) | null;
|
||||||
|
sourceType?: SourceType;
|
||||||
};
|
};
|
||||||
export type FaceDto = {
|
export type FaceDto = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -893,6 +895,7 @@ export type ServerFeaturesDto = {
|
|||||||
duplicateDetection: boolean;
|
duplicateDetection: boolean;
|
||||||
email: boolean;
|
email: boolean;
|
||||||
facialRecognition: boolean;
|
facialRecognition: boolean;
|
||||||
|
importFaces: boolean;
|
||||||
map: boolean;
|
map: boolean;
|
||||||
oauth: boolean;
|
oauth: boolean;
|
||||||
oauthAutoLaunch: boolean;
|
oauthAutoLaunch: boolean;
|
||||||
@ -1122,6 +1125,12 @@ export type SystemConfigMapDto = {
|
|||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
lightStyle: string;
|
lightStyle: string;
|
||||||
};
|
};
|
||||||
|
export type SystemConfigFacesDto = {
|
||||||
|
"import": boolean;
|
||||||
|
};
|
||||||
|
export type SystemConfigMetadataDto = {
|
||||||
|
faces: SystemConfigFacesDto;
|
||||||
|
};
|
||||||
export type SystemConfigNewVersionCheckDto = {
|
export type SystemConfigNewVersionCheckDto = {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
@ -1178,6 +1187,7 @@ export type SystemConfigDto = {
|
|||||||
logging: SystemConfigLoggingDto;
|
logging: SystemConfigLoggingDto;
|
||||||
machineLearning: SystemConfigMachineLearningDto;
|
machineLearning: SystemConfigMachineLearningDto;
|
||||||
map: SystemConfigMapDto;
|
map: SystemConfigMapDto;
|
||||||
|
metadata: SystemConfigMetadataDto;
|
||||||
newVersionCheck: SystemConfigNewVersionCheckDto;
|
newVersionCheck: SystemConfigNewVersionCheckDto;
|
||||||
notifications: SystemConfigNotificationsDto;
|
notifications: SystemConfigNotificationsDto;
|
||||||
oauth: SystemConfigOAuthDto;
|
oauth: SystemConfigOAuthDto;
|
||||||
@ -3226,6 +3236,10 @@ export enum AlbumUserRole {
|
|||||||
Editor = "editor",
|
Editor = "editor",
|
||||||
Viewer = "viewer"
|
Viewer = "viewer"
|
||||||
}
|
}
|
||||||
|
export enum SourceType {
|
||||||
|
MachineLearning = "machine-learning",
|
||||||
|
Exif = "exif"
|
||||||
|
}
|
||||||
export enum AssetTypeEnum {
|
export enum AssetTypeEnum {
|
||||||
Image = "IMAGE",
|
Image = "IMAGE",
|
||||||
Video = "VIDEO",
|
Video = "VIDEO",
|
||||||
|
46
server/package-lock.json
generated
46
server/package-lock.json
generated
@ -24,7 +24,7 @@
|
|||||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
||||||
"@opentelemetry/sdk-node": "^0.53.0",
|
"@opentelemetry/sdk-node": "^0.53.0",
|
||||||
"@react-email/components": "^0.0.23",
|
"@react-email/components": "^0.0.24",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.0",
|
"archiver": "^7.0.0",
|
||||||
"async-lock": "^1.4.0",
|
"async-lock": "^1.4.0",
|
||||||
@ -5070,9 +5070,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/code-block": {
|
"node_modules/@react-email/code-block": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz",
|
||||||
"integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==",
|
"integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prismjs": "1.29.0"
|
"prismjs": "1.29.0"
|
||||||
},
|
},
|
||||||
@ -5106,13 +5106,13 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/components": {
|
"node_modules/@react-email/components": {
|
||||||
"version": "0.0.23",
|
"version": "0.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz",
|
||||||
"integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==",
|
"integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@react-email/body": "0.0.10",
|
"@react-email/body": "0.0.10",
|
||||||
"@react-email/button": "0.0.17",
|
"@react-email/button": "0.0.17",
|
||||||
"@react-email/code-block": "0.0.7",
|
"@react-email/code-block": "0.0.8",
|
||||||
"@react-email/code-inline": "0.0.4",
|
"@react-email/code-inline": "0.0.4",
|
||||||
"@react-email/column": "0.0.12",
|
"@react-email/column": "0.0.12",
|
||||||
"@react-email/container": "0.0.14",
|
"@react-email/container": "0.0.14",
|
||||||
@ -5125,7 +5125,7 @@
|
|||||||
"@react-email/link": "0.0.10",
|
"@react-email/link": "0.0.10",
|
||||||
"@react-email/markdown": "0.0.12",
|
"@react-email/markdown": "0.0.12",
|
||||||
"@react-email/preview": "0.0.11",
|
"@react-email/preview": "0.0.11",
|
||||||
"@react-email/render": "1.0.0",
|
"@react-email/render": "1.0.1",
|
||||||
"@react-email/row": "0.0.10",
|
"@react-email/row": "0.0.10",
|
||||||
"@react-email/section": "0.0.14",
|
"@react-email/section": "0.0.14",
|
||||||
"@react-email/tailwind": "0.1.0",
|
"@react-email/tailwind": "0.1.0",
|
||||||
@ -5249,9 +5249,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@react-email/render": {
|
"node_modules/@react-email/render": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz",
|
||||||
"integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==",
|
"integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"js-beautify": "^1.14.11",
|
"js-beautify": "^1.14.11",
|
||||||
@ -19280,9 +19280,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@react-email/code-block": {
|
"@react-email/code-block": {
|
||||||
"version": "0.0.7",
|
"version": "0.0.8",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.8.tgz",
|
||||||
"integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==",
|
"integrity": "sha512-WbuAEpTnB262i9C3SGPmmErgZ4iU5KIpqLUjr7uBJijqldLqZc5x39e8wPWaRdF7NLcShmrc/+G7GJgI1bdC5w==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"prismjs": "1.29.0"
|
"prismjs": "1.29.0"
|
||||||
}
|
}
|
||||||
@ -19300,13 +19300,13 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@react-email/components": {
|
"@react-email/components": {
|
||||||
"version": "0.0.23",
|
"version": "0.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.24.tgz",
|
||||||
"integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==",
|
"integrity": "sha512-/DNmfTREaT59UFdkHoIK3BewJ214LfRxmduiil3m7POj+gougkItANu1+BMmgbUATxjf7jH1WoBxo9x/rhFEFw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@react-email/body": "0.0.10",
|
"@react-email/body": "0.0.10",
|
||||||
"@react-email/button": "0.0.17",
|
"@react-email/button": "0.0.17",
|
||||||
"@react-email/code-block": "0.0.7",
|
"@react-email/code-block": "0.0.8",
|
||||||
"@react-email/code-inline": "0.0.4",
|
"@react-email/code-inline": "0.0.4",
|
||||||
"@react-email/column": "0.0.12",
|
"@react-email/column": "0.0.12",
|
||||||
"@react-email/container": "0.0.14",
|
"@react-email/container": "0.0.14",
|
||||||
@ -19319,7 +19319,7 @@
|
|||||||
"@react-email/link": "0.0.10",
|
"@react-email/link": "0.0.10",
|
||||||
"@react-email/markdown": "0.0.12",
|
"@react-email/markdown": "0.0.12",
|
||||||
"@react-email/preview": "0.0.11",
|
"@react-email/preview": "0.0.11",
|
||||||
"@react-email/render": "1.0.0",
|
"@react-email/render": "1.0.1",
|
||||||
"@react-email/row": "0.0.10",
|
"@react-email/row": "0.0.10",
|
||||||
"@react-email/section": "0.0.14",
|
"@react-email/section": "0.0.14",
|
||||||
"@react-email/tailwind": "0.1.0",
|
"@react-email/tailwind": "0.1.0",
|
||||||
@ -19389,9 +19389,9 @@
|
|||||||
"requires": {}
|
"requires": {}
|
||||||
},
|
},
|
||||||
"@react-email/render": {
|
"@react-email/render": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.1.tgz",
|
||||||
"integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==",
|
"integrity": "sha512-W3gTrcmLOVYnG80QuUp22ReIT/xfLsVJ+n7ghSlG2BITB8evNABn1AO2rGQoXuK84zKtDAlxCdm3hRyIpZdGSA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"html-to-text": "9.0.5",
|
"html-to-text": "9.0.5",
|
||||||
"js-beautify": "^1.14.11",
|
"js-beautify": "^1.14.11",
|
||||||
|
@ -50,7 +50,7 @@
|
|||||||
"@opentelemetry/context-async-hooks": "^1.24.0",
|
"@opentelemetry/context-async-hooks": "^1.24.0",
|
||||||
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
"@opentelemetry/exporter-prometheus": "^0.53.0",
|
||||||
"@opentelemetry/sdk-node": "^0.53.0",
|
"@opentelemetry/sdk-node": "^0.53.0",
|
||||||
"@react-email/components": "^0.0.23",
|
"@react-email/components": "^0.0.24",
|
||||||
"@socket.io/redis-adapter": "^8.3.0",
|
"@socket.io/redis-adapter": "^8.3.0",
|
||||||
"archiver": "^7.0.0",
|
"archiver": "^7.0.0",
|
||||||
"async-lock": "^1.4.0",
|
"async-lock": "^1.4.0",
|
||||||
|
@ -18,10 +18,11 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|||||||
import { AuthGuard } from 'src/middleware/auth.guard';
|
import { AuthGuard } from 'src/middleware/auth.guard';
|
||||||
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
import { ErrorInterceptor } from 'src/middleware/error.interceptor';
|
||||||
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor';
|
||||||
import { HttpExceptionFilter } from 'src/middleware/http-exception.filter';
|
import { GlobalExceptionFilter } from 'src/middleware/global-exception.filter';
|
||||||
import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
import { LoggingInterceptor } from 'src/middleware/logging.interceptor';
|
||||||
import { repositories } from 'src/repositories';
|
import { repositories } from 'src/repositories';
|
||||||
import { services } from 'src/services';
|
import { services } from 'src/services';
|
||||||
|
import { DatabaseService } from 'src/services/database.service';
|
||||||
import { setupEventHandlers } from 'src/utils/events';
|
import { setupEventHandlers } from 'src/utils/events';
|
||||||
import { otelConfig } from 'src/utils/instrumentation';
|
import { otelConfig } from 'src/utils/instrumentation';
|
||||||
|
|
||||||
@ -29,7 +30,7 @@ const common = [...services, ...repositories];
|
|||||||
|
|
||||||
const middleware = [
|
const middleware = [
|
||||||
FileUploadInterceptor,
|
FileUploadInterceptor,
|
||||||
{ provide: APP_FILTER, useClass: HttpExceptionFilter },
|
{ provide: APP_FILTER, useClass: GlobalExceptionFilter },
|
||||||
{ provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
|
{ provide: APP_PIPE, useValue: new ValidationPipe({ transform: true, whitelist: true }) },
|
||||||
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
|
{ provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
|
||||||
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
{ provide: APP_INTERCEPTOR, useClass: ErrorInterceptor },
|
||||||
@ -43,7 +44,17 @@ const imports = [
|
|||||||
ConfigModule.forRoot(immichAppConfig),
|
ConfigModule.forRoot(immichAppConfig),
|
||||||
EventEmitterModule.forRoot(),
|
EventEmitterModule.forRoot(),
|
||||||
OpenTelemetryModule.forRoot(otelConfig),
|
OpenTelemetryModule.forRoot(otelConfig),
|
||||||
TypeOrmModule.forRoot(databaseConfig),
|
TypeOrmModule.forRootAsync({
|
||||||
|
inject: [ModuleRef],
|
||||||
|
useFactory: (moduleRef: ModuleRef) => {
|
||||||
|
return {
|
||||||
|
...databaseConfig,
|
||||||
|
poolErrorHandler: (error) => {
|
||||||
|
moduleRef.get(DatabaseService, { strict: false }).handleConnectionError(error);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
}),
|
||||||
TypeOrmModule.forFeature(entities),
|
TypeOrmModule.forFeature(entities),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -141,6 +141,11 @@ export interface SystemConfig {
|
|||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
};
|
};
|
||||||
|
metadata: {
|
||||||
|
faces: {
|
||||||
|
import: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
oauth: {
|
oauth: {
|
||||||
autoLaunch: boolean;
|
autoLaunch: boolean;
|
||||||
autoRegister: boolean;
|
autoRegister: boolean;
|
||||||
@ -286,6 +291,11 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||||||
reverseGeocoding: {
|
reverseGeocoding: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
faces: {
|
||||||
|
import: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
oauth: {
|
oauth: {
|
||||||
autoLaunch: false,
|
autoLaunch: false,
|
||||||
autoRegister: true,
|
autoRegister: true,
|
||||||
|
@ -301,7 +301,7 @@ export class StorageCore {
|
|||||||
return this.assetRepository.update({ id, sidecarPath: newPath });
|
return this.assetRepository.update({ id, sidecarPath: newPath });
|
||||||
}
|
}
|
||||||
case PersonPathType.FACE: {
|
case PersonPathType.FACE: {
|
||||||
return this.personRepository.update({ id, thumbnailPath: newPath });
|
return this.personRepository.update([{ id, thumbnailPath: newPath }]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,6 +6,7 @@ import { PropertyLifecycle } from 'src/decorators';
|
|||||||
import { AuthDto } from 'src/dtos/auth.dto';
|
import { AuthDto } from 'src/dtos/auth.dto';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
|
import { SourceType } from 'src/enum';
|
||||||
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
import { IsDateStringFormat, MaxDateString, Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||||
|
|
||||||
export class PersonCreateDto {
|
export class PersonCreateDto {
|
||||||
@ -113,6 +114,8 @@ export class AssetFaceWithoutPersonResponseDto {
|
|||||||
boundingBoxY1!: number;
|
boundingBoxY1!: number;
|
||||||
@ApiProperty({ type: 'integer' })
|
@ApiProperty({ type: 'integer' })
|
||||||
boundingBoxY2!: number;
|
boundingBoxY2!: number;
|
||||||
|
@ApiProperty({ enum: SourceType, enumName: 'SourceType' })
|
||||||
|
sourceType?: SourceType;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
|
export class AssetFaceResponseDto extends AssetFaceWithoutPersonResponseDto {
|
||||||
@ -176,6 +179,7 @@ export function mapFacesWithoutPerson(face: AssetFaceEntity): AssetFaceWithoutPe
|
|||||||
boundingBoxX2: face.boundingBoxX2,
|
boundingBoxX2: face.boundingBoxX2,
|
||||||
boundingBoxY1: face.boundingBoxY1,
|
boundingBoxY1: face.boundingBoxY1,
|
||||||
boundingBoxY2: face.boundingBoxY2,
|
boundingBoxY2: face.boundingBoxY2,
|
||||||
|
sourceType: face.sourceType,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,6 +131,7 @@ export class ServerFeaturesDto {
|
|||||||
map!: boolean;
|
map!: boolean;
|
||||||
trash!: boolean;
|
trash!: boolean;
|
||||||
reverseGeocoding!: boolean;
|
reverseGeocoding!: boolean;
|
||||||
|
importFaces!: boolean;
|
||||||
oauth!: boolean;
|
oauth!: boolean;
|
||||||
oauthAutoLaunch!: boolean;
|
oauthAutoLaunch!: boolean;
|
||||||
passwordLogin!: boolean;
|
passwordLogin!: boolean;
|
||||||
|
@ -375,6 +375,18 @@ class SystemConfigReverseGeocodingDto {
|
|||||||
enabled!: boolean;
|
enabled!: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class SystemConfigFacesDto {
|
||||||
|
@IsBoolean()
|
||||||
|
import!: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SystemConfigMetadataDto {
|
||||||
|
@Type(() => SystemConfigFacesDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
faces!: SystemConfigFacesDto;
|
||||||
|
}
|
||||||
|
|
||||||
class SystemConfigServerDto {
|
class SystemConfigServerDto {
|
||||||
@ValidateIf((_, value: string) => value !== '')
|
@ValidateIf((_, value: string) => value !== '')
|
||||||
@IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] })
|
@IsUrl({ require_tld: false, require_protocol: true, protocols: ['http', 'https'] })
|
||||||
@ -555,6 +567,11 @@ export class SystemConfigDto implements SystemConfig {
|
|||||||
@IsObject()
|
@IsObject()
|
||||||
reverseGeocoding!: SystemConfigReverseGeocodingDto;
|
reverseGeocoding!: SystemConfigReverseGeocodingDto;
|
||||||
|
|
||||||
|
@Type(() => SystemConfigMetadataDto)
|
||||||
|
@ValidateNested()
|
||||||
|
@IsObject()
|
||||||
|
metadata!: SystemConfigMetadataDto;
|
||||||
|
|
||||||
@Type(() => SystemConfigStorageTemplateDto)
|
@Type(() => SystemConfigStorageTemplateDto)
|
||||||
@ValidateNested()
|
@ValidateNested()
|
||||||
@IsObject()
|
@IsObject()
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
import { FaceSearchEntity } from 'src/entities/face-search.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
|
import { SourceType } from 'src/enum';
|
||||||
import { Column, Entity, Index, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
import { Column, Entity, Index, ManyToOne, OneToOne, PrimaryGeneratedColumn } from 'typeorm';
|
||||||
|
|
||||||
@Entity('asset_faces', { synchronize: false })
|
@Entity('asset_faces', { synchronize: false })
|
||||||
@ -37,6 +38,9 @@ export class AssetFaceEntity {
|
|||||||
@Column({ default: 0, type: 'int' })
|
@Column({ default: 0, type: 'int' })
|
||||||
boundingBoxY2!: number;
|
boundingBoxY2!: number;
|
||||||
|
|
||||||
|
@Column({ default: SourceType.MACHINE_LEARNING, type: 'enum', enum: SourceType })
|
||||||
|
sourceType!: SourceType;
|
||||||
|
|
||||||
@ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
@ManyToOne(() => AssetEntity, (asset) => asset.faces, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||||
asset!: AssetEntity;
|
asset!: AssetEntity;
|
||||||
|
|
||||||
|
@ -180,3 +180,8 @@ export enum UserStatus {
|
|||||||
REMOVING = 'removing',
|
REMOVING = 'removing',
|
||||||
DELETED = 'deleted',
|
DELETED = 'deleted',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum SourceType {
|
||||||
|
MACHINE_LEARNING = 'machine-learning',
|
||||||
|
EXIF = 'exif',
|
||||||
|
}
|
||||||
|
@ -40,6 +40,7 @@ export interface VectorUpdateResult {
|
|||||||
export const IDatabaseRepository = 'IDatabaseRepository';
|
export const IDatabaseRepository = 'IDatabaseRepository';
|
||||||
|
|
||||||
export interface IDatabaseRepository {
|
export interface IDatabaseRepository {
|
||||||
|
reconnect(): Promise<boolean>;
|
||||||
getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion>;
|
getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion>;
|
||||||
getExtensionVersionRange(extension: VectorExtension): string;
|
getExtensionVersionRange(extension: VectorExtension): string;
|
||||||
getPostgresVersion(): Promise<string>;
|
getPostgresVersion(): Promise<string>;
|
||||||
|
@ -7,7 +7,8 @@ export interface ExifDuration {
|
|||||||
Scale?: number;
|
Scale?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription'> {
|
type TagsWithWrongTypes = 'FocalLength' | 'Duration' | 'Description' | 'ImageDescription' | 'RegionInfo';
|
||||||
|
export interface ImmichTags extends Omit<Tags, TagsWithWrongTypes> {
|
||||||
ContentIdentifier?: string;
|
ContentIdentifier?: string;
|
||||||
MotionPhoto?: number;
|
MotionPhoto?: number;
|
||||||
MotionPhotoVersion?: number;
|
MotionPhotoVersion?: number;
|
||||||
@ -23,6 +24,28 @@ export interface ImmichTags extends Omit<Tags, 'FocalLength' | 'Duration' | 'Des
|
|||||||
// Type is wrong, can also be number.
|
// Type is wrong, can also be number.
|
||||||
Description?: string | number;
|
Description?: string | number;
|
||||||
ImageDescription?: string | number;
|
ImageDescription?: string | number;
|
||||||
|
|
||||||
|
// Extended properties for image regions, such as faces
|
||||||
|
RegionInfo?: {
|
||||||
|
AppliedToDimensions: {
|
||||||
|
W: number;
|
||||||
|
H: number;
|
||||||
|
Unit: string;
|
||||||
|
};
|
||||||
|
RegionList: {
|
||||||
|
Area: {
|
||||||
|
// (X,Y) // center of the rectangle
|
||||||
|
X: number;
|
||||||
|
Y: number;
|
||||||
|
W: number;
|
||||||
|
H: number;
|
||||||
|
Unit: string;
|
||||||
|
};
|
||||||
|
Rotation?: number;
|
||||||
|
Type?: string;
|
||||||
|
Name?: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IMetadataRepository {
|
export interface IMetadataRepository {
|
||||||
@ -30,9 +53,9 @@ export interface IMetadataRepository {
|
|||||||
readTags(path: string): Promise<ImmichTags | null>;
|
readTags(path: string): Promise<ImmichTags | null>;
|
||||||
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
writeTags(path: string, tags: Partial<Tags>): Promise<void>;
|
||||||
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
extractBinaryTag(tagName: string, path: string): Promise<Buffer>;
|
||||||
getCountries(userId: string): Promise<Array<string | null>>;
|
getCountries(userIds: string[]): Promise<Array<string | null>>;
|
||||||
getStates(userId: string, country?: string): Promise<Array<string | null>>;
|
getStates(userIds: string[], country?: string): Promise<Array<string | null>>;
|
||||||
getCities(userId: string, country?: string, state?: string): Promise<Array<string | null>>;
|
getCities(userIds: string[], country?: string, state?: string): Promise<Array<string | null>>;
|
||||||
getCameraMakes(userId: string, model?: string): Promise<Array<string | null>>;
|
getCameraMakes(userIds: string[], model?: string): Promise<Array<string | null>>;
|
||||||
getCameraModels(userId: string, make?: string): Promise<Array<string | null>>;
|
getCameraModels(userIds: string[], make?: string): Promise<Array<string | null>>;
|
||||||
}
|
}
|
||||||
|
@ -15,6 +15,11 @@ export interface PersonNameSearchOptions {
|
|||||||
withHidden?: boolean;
|
withHidden?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PersonNameResponse {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface AssetFaceId {
|
export interface AssetFaceId {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
personId: string;
|
personId: string;
|
||||||
@ -35,20 +40,26 @@ export interface PeopleStatistics {
|
|||||||
hidden: number;
|
hidden: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface DeleteAllFacesOptions {
|
||||||
|
sourceType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface IPersonRepository {
|
export interface IPersonRepository {
|
||||||
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
getAll(pagination: PaginationOptions, options?: FindManyOptions<PersonEntity>): Paginated<PersonEntity>;
|
||||||
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
getAllForUser(pagination: PaginationOptions, userId: string, options: PersonSearchOptions): Paginated<PersonEntity>;
|
||||||
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
getAllWithoutFaces(): Promise<PersonEntity[]>;
|
||||||
getById(personId: string): Promise<PersonEntity | null>;
|
getById(personId: string): Promise<PersonEntity | null>;
|
||||||
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
getByName(userId: string, personName: string, options: PersonNameSearchOptions): Promise<PersonEntity[]>;
|
||||||
|
getDistinctNames(userId: string, options: PersonNameSearchOptions): Promise<PersonNameResponse[]>;
|
||||||
|
|
||||||
getAssets(personId: string): Promise<AssetEntity[]>;
|
getAssets(personId: string): Promise<AssetEntity[]>;
|
||||||
|
|
||||||
create(entity: Partial<PersonEntity>): Promise<PersonEntity>;
|
create(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]>;
|
||||||
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
createFaces(entities: Partial<AssetFaceEntity>[]): Promise<string[]>;
|
||||||
delete(entities: PersonEntity[]): Promise<void>;
|
delete(entities: PersonEntity[]): Promise<void>;
|
||||||
deleteAll(): Promise<void>;
|
deleteAll(): Promise<void>;
|
||||||
deleteAllFaces(): Promise<void>;
|
deleteAllFaces(options: DeleteAllFacesOptions): Promise<void>;
|
||||||
|
replaceFaces(assetId: string, entities: Partial<AssetFaceEntity>[], sourceType?: string): Promise<string[]>;
|
||||||
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
getAllFaces(pagination: PaginationOptions, options?: FindManyOptions<AssetFaceEntity>): Paginated<AssetFaceEntity>;
|
||||||
getFaceById(id: string): Promise<AssetFaceEntity>;
|
getFaceById(id: string): Promise<AssetFaceEntity>;
|
||||||
getFaceByIdWithAssets(
|
getFaceByIdWithAssets(
|
||||||
@ -63,6 +74,6 @@ export interface IPersonRepository {
|
|||||||
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
reassignFace(assetFaceId: string, newPersonId: string): Promise<number>;
|
||||||
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
getNumberOfPeople(userId: string): Promise<PeopleStatistics>;
|
||||||
reassignFaces(data: UpdateFacesData): Promise<number>;
|
reassignFaces(data: UpdateFacesData): Promise<number>;
|
||||||
update(entity: Partial<PersonEntity>): Promise<PersonEntity>;
|
update(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]>;
|
||||||
getLatestFaceDate(): Promise<string | undefined>;
|
getLatestFaceDate(): Promise<string | undefined>;
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import {
|
|||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { Observable, catchError, throwError } from 'rxjs';
|
import { Observable, catchError, throwError } from 'rxjs';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { logGlobalError } from 'src/utils/logger';
|
||||||
import { routeToErrorMessage } from 'src/utils/misc';
|
import { routeToErrorMessage } from 'src/utils/misc';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@ -25,9 +26,10 @@ export class ErrorInterceptor implements NestInterceptor {
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const errorMessage = routeToErrorMessage(context.getHandler().name);
|
logGlobalError(this.logger, error);
|
||||||
this.logger.error(errorMessage, error, error?.errors, error?.stack);
|
|
||||||
return new InternalServerErrorException(errorMessage);
|
const message = routeToErrorMessage(context.getHandler().name);
|
||||||
|
return new InternalServerErrorException(message);
|
||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
47
server/src/middleware/global-exception.filter.ts
Normal file
47
server/src/middleware/global-exception.filter.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common';
|
||||||
|
import { Response } from 'express';
|
||||||
|
import { ClsService } from 'nestjs-cls';
|
||||||
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
|
import { logGlobalError } from 'src/utils/logger';
|
||||||
|
|
||||||
|
@Catch()
|
||||||
|
export class GlobalExceptionFilter implements ExceptionFilter<Error> {
|
||||||
|
constructor(
|
||||||
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
|
private cls: ClsService,
|
||||||
|
) {
|
||||||
|
this.logger.setContext(GlobalExceptionFilter.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
catch(error: Error, host: ArgumentsHost) {
|
||||||
|
const ctx = host.switchToHttp();
|
||||||
|
const response = ctx.getResponse<Response>();
|
||||||
|
const { status, body } = this.fromError(error);
|
||||||
|
if (!response.headersSent) {
|
||||||
|
response.status(status).json({ ...body, statusCode: status, correlationId: this.cls.getId() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fromError(error: Error) {
|
||||||
|
logGlobalError(this.logger, error);
|
||||||
|
|
||||||
|
if (error instanceof HttpException) {
|
||||||
|
const status = error.getStatus();
|
||||||
|
let body = error.getResponse();
|
||||||
|
|
||||||
|
// unclear what circumstances would return a string
|
||||||
|
if (typeof body === 'string') {
|
||||||
|
body = { message: body };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status, body };
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: 500,
|
||||||
|
body: {
|
||||||
|
message: 'Internal server error',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -1,39 +0,0 @@
|
|||||||
import { ArgumentsHost, Catch, ExceptionFilter, HttpException, Inject } from '@nestjs/common';
|
|
||||||
import { Response } from 'express';
|
|
||||||
import { ClsService } from 'nestjs-cls';
|
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
|
||||||
|
|
||||||
@Catch(HttpException)
|
|
||||||
export class HttpExceptionFilter implements ExceptionFilter {
|
|
||||||
constructor(
|
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
|
||||||
private cls: ClsService,
|
|
||||||
) {
|
|
||||||
this.logger.setContext(HttpExceptionFilter.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
catch(exception: HttpException, host: ArgumentsHost) {
|
|
||||||
const ctx = host.switchToHttp();
|
|
||||||
const response = ctx.getResponse<Response>();
|
|
||||||
const status = exception.getStatus();
|
|
||||||
|
|
||||||
this.logger.debug(`HttpException(${status}) ${JSON.stringify(exception.getResponse())}`);
|
|
||||||
|
|
||||||
let responseBody = exception.getResponse();
|
|
||||||
// unclear what circumstances would return a string
|
|
||||||
if (typeof responseBody === 'string') {
|
|
||||||
responseBody = {
|
|
||||||
error: 'Unknown',
|
|
||||||
message: responseBody,
|
|
||||||
statusCode: status,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.headersSent) {
|
|
||||||
response.status(status).json({
|
|
||||||
...responseBody,
|
|
||||||
correlationId: this.cls.getId(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -11,7 +11,7 @@ export class AddAssetChecksum1661881837496 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "checksum"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -17,8 +17,8 @@ export class CreateTagsTable1670257571385 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`);
|
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`);
|
||||||
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`);
|
await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`);
|
||||||
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
|
await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_e99f31ea4cdf3a2c35c7287eb4"`);
|
await queryRunner.query(`DROP INDEX "IDX_e99f31ea4cdf3a2c35c7287eb4"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_f8e8a9e893cb5c54907f1b798e"`);
|
await queryRunner.query(`DROP INDEX "IDX_f8e8a9e893cb5c54907f1b798e"`);
|
||||||
await queryRunner.query(`DROP TABLE "tag_asset"`);
|
await queryRunner.query(`DROP TABLE "tag_asset"`);
|
||||||
await queryRunner.query(`DROP TABLE "tags"`);
|
await queryRunner.query(`DROP TABLE "tags"`);
|
||||||
}
|
}
|
||||||
|
@ -18,10 +18,10 @@ export class AddSharedLinkTable1673150490490 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`);
|
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_c9fab4aa97ffd1b034f3d6581ab"`);
|
||||||
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`);
|
await queryRunner.query(`ALTER TABLE "shared_link__asset" DROP CONSTRAINT "FK_5b7decce6c8d3db9593d6111a66"`);
|
||||||
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`);
|
await queryRunner.query(`ALTER TABLE "shared_links" DROP CONSTRAINT "FK_0c6ce9058c29f07cdf7014eac66"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_c9fab4aa97ffd1b034f3d6581a"`);
|
await queryRunner.query(`DROP INDEX "IDX_c9fab4aa97ffd1b034f3d6581a"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_5b7decce6c8d3db9593d6111a6"`);
|
await queryRunner.query(`DROP INDEX "IDX_5b7decce6c8d3db9593d6111a6"`);
|
||||||
await queryRunner.query(`DROP TABLE "shared_link__asset"`);
|
await queryRunner.query(`DROP TABLE "shared_link__asset"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_key"`);
|
await queryRunner.query(`DROP INDEX "IDX_sharedlink_key"`);
|
||||||
await queryRunner.query(`DROP TABLE "shared_links"`);
|
await queryRunner.query(`DROP TABLE "shared_links"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,10 +44,10 @@ export class FixAlbumEntityTypeORM1675812532822 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621"`);
|
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_4bd1303d199f4e72ccdf998c621"`);
|
||||||
await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_427c350ad49bd3935a50baab737"`);
|
await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_427c350ad49bd3935a50baab737"`);
|
||||||
await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06"`);
|
await queryRunner.query(`ALTER TABLE "user_shared_album" DROP CONSTRAINT "FK_f48513bf9bccefd6ff3ad30bd06"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_427c350ad49bd3935a50baab73"`);
|
await queryRunner.query(`DROP INDEX "IDX_427c350ad49bd3935a50baab73"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_f48513bf9bccefd6ff3ad30bd0"`);
|
await queryRunner.query(`DROP INDEX "IDX_f48513bf9bccefd6ff3ad30bd0"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_e590fa396c6898fcd4a50e4092"`);
|
await queryRunner.query(`DROP INDEX "IDX_e590fa396c6898fcd4a50e4092"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_4bd1303d199f4e72ccdf998c62"`);
|
await queryRunner.query(`DROP INDEX "IDX_4bd1303d199f4e72ccdf998c62"`);
|
||||||
|
|
||||||
await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`);
|
await queryRunner.query(`ALTER TABLE "albums" DROP CONSTRAINT "FK_b22c53f35ef20c28c21637c85f4"`);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
|
@ -9,7 +9,7 @@ export class AppleContentIdentifier1676437878377 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_live_photo_cid"`);
|
await queryRunner.query(`DROP INDEX "IDX_live_photo_cid"`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "livePhotoCID"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "livePhotoCID"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export class ExifEntityDefinitionFixes1676848629119 implements MigrationInterfac
|
|||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "description" SET NOT NULL`);
|
||||||
|
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_c0117fdbc50b917ef9067740c4"`);
|
await queryRunner.query(`DROP INDEX "IDX_c0117fdbc50b917ef9067740c4"`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_28663352d85078ad0046dafafaa"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "PK_28663352d85078ad0046dafafaa"`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "id"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "id"`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP CONSTRAINT "FK_c0117fdbc50b917ef9067740c44"`);
|
||||||
|
@ -4,7 +4,7 @@ export class SmartInfoEntityDefinitionFixes1676852143506 implements MigrationInt
|
|||||||
name = 'SmartInfoEntityDefinitionFixes1676852143506'
|
name = 'SmartInfoEntityDefinitionFixes1676852143506'
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_5e3753aadd956110bf3ec0244a"`);
|
await queryRunner.query(`DROP INDEX "IDX_5e3753aadd956110bf3ec0244a"`);
|
||||||
await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_0beace66440e9713f5c40470e46"`);
|
await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "PK_0beace66440e9713f5c40470e46"`);
|
||||||
await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN "id"`);
|
await queryRunner.query(`ALTER TABLE "smart_info" DROP COLUMN "id"`);
|
||||||
await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`);
|
await queryRunner.query(`ALTER TABLE "smart_info" DROP CONSTRAINT "FK_5e3753aadd956110bf3ec0244ac"`);
|
||||||
|
@ -8,7 +8,7 @@ export class AddIndexForAlbumInSharedLinkTable1677535643119 implements Migration
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_sharedlink_albumId"`);
|
await queryRunner.query(`DROP INDEX "IDX_sharedlink_albumId"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,13 +4,13 @@ export class RequireChecksumNotNull1684328185099 implements MigrationInterface {
|
|||||||
name = 'removeNotNullFromChecksumIndex1684328185099';
|
name = 'removeNotNullFromChecksumIndex1684328185099';
|
||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_64c507300988dd1764f9a6530c"`);
|
await queryRunner.query(`DROP INDEX "IDX_64c507300988dd1764f9a6530c"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" SET NOT NULL`);
|
||||||
await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `);
|
await queryRunner.query(`CREATE INDEX "IDX_8d3efe36c0755849395e6ea866" ON "assets" ("checksum") `);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_8d3efe36c0755849395e6ea866"`);
|
await queryRunner.query(`DROP INDEX "IDX_8d3efe36c0755849395e6ea866"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "checksum" DROP NOT NULL`);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`,
|
`CREATE INDEX "IDX_64c507300988dd1764f9a6530c" ON "assets" ("checksum") WHERE ('checksum' IS NOT NULL)`,
|
||||||
|
@ -9,7 +9,7 @@ export class AddAuditTable1692804658140 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_ownerId_createdAt"`);
|
await queryRunner.query(`DROP INDEX "IDX_ownerId_createdAt"`);
|
||||||
await queryRunner.query(`DROP TABLE "audit"`);
|
await queryRunner.query(`DROP TABLE "audit"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,6 @@ export class AddOriginalPathIndex1696888644031 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`);
|
await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -15,7 +15,7 @@ export class AddActivity1698693294632 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_1af8519996fbfb3684b58df280b"`);
|
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_1af8519996fbfb3684b58df280b"`);
|
||||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`);
|
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_3571467bcbe021f66e2bdce96ea"`);
|
||||||
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_8091ea76b12338cb4428d33d782"`);
|
await queryRunner.query(`ALTER TABLE "activity" DROP CONSTRAINT "FK_8091ea76b12338cb4428d33d782"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_activity_like"`);
|
await queryRunner.query(`DROP INDEX "IDX_activity_like"`);
|
||||||
await queryRunner.query(`DROP TABLE "activity"`);
|
await queryRunner.query(`DROP TABLE "activity"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,8 +9,8 @@ export class AddAssetFaceIndicies1700752078178 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`);
|
await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_bf339a24070dac7e71304ec530"`);
|
await queryRunner.query(`DROP INDEX "IDX_bf339a24070dac7e71304ec530"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ export class AddExifCityIndex1701665867595 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."exif_city"`);
|
await queryRunner.query(`DROP INDEX "exif_city"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -9,7 +9,7 @@ export class AddAutoStackId1703035138085 implements MigrationInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_auto_stack_id"`);
|
await queryRunner.query(`DROP INDEX "IDX_auto_stack_id"`);
|
||||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "autoStackId"`);
|
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "autoStackId"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -8,6 +8,6 @@ export class AddOriginalFileNameIndex1705306747072 implements MigrationInterface
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_4d66e76dada1ca180f67a205dc"`);
|
await queryRunner.query(`DROP INDEX "IDX_4d66e76dada1ca180f67a205dc"`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,7 +41,7 @@ export class CreateAssetStackTable1705197515600 implements MigrationInterface {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// update constraints
|
// update constraints
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_b463c8edb01364bf2beba08ef1"`);
|
await queryRunner.query(`DROP INDEX "IDX_b463c8edb01364bf2beba08ef1"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_b463c8edb01364bf2beba08ef19"`);
|
||||||
await queryRunner.query(
|
await queryRunner.query(
|
||||||
`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack"("id") ON DELETE SET NULL ON UPDATE CASCADE`,
|
`ALTER TABLE "assets" ADD CONSTRAINT "FK_f15d48fa3ea5e4bda05ca8ab207" FOREIGN KEY ("stackId") REFERENCES "asset_stack"("id") ON DELETE SET NULL ON UPDATE CASCADE`,
|
||||||
|
@ -17,8 +17,8 @@ export class AddMemoryTable1711637874206 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`);
|
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_6942ecf52d75d4273de19d2c16f"`);
|
||||||
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`);
|
await queryRunner.query(`ALTER TABLE "memories_assets_assets" DROP CONSTRAINT "FK_984e5c9ab1f04d34538cd32334e"`);
|
||||||
await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`);
|
await queryRunner.query(`ALTER TABLE "memories" DROP CONSTRAINT "FK_575842846f0c28fa5da46c99b19"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_6942ecf52d75d4273de19d2c16"`);
|
await queryRunner.query(`DROP INDEX "IDX_6942ecf52d75d4273de19d2c16"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_984e5c9ab1f04d34538cd32334"`);
|
await queryRunner.query(`DROP INDEX "IDX_984e5c9ab1f04d34538cd32334"`);
|
||||||
await queryRunner.query(`DROP TABLE "memories_assets_assets"`);
|
await queryRunner.query(`DROP TABLE "memories_assets_assets"`);
|
||||||
await queryRunner.query(`DROP TABLE "memories"`);
|
await queryRunner.query(`DROP TABLE "memories"`);
|
||||||
}
|
}
|
||||||
|
@ -5,8 +5,8 @@ export class RemoveLibraryType1715804005643 implements MigrationInterface {
|
|||||||
|
|
||||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`);
|
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."UQ_assets_owner_library_checksum"`);
|
await queryRunner.query(`DROP INDEX "UQ_assets_owner_library_checksum"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`);
|
await queryRunner.query(`DROP INDEX "IDX_originalPath_libraryId"`);
|
||||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`);
|
||||||
await queryRunner.query(`
|
await queryRunner.query(`
|
||||||
UPDATE "assets"
|
UPDATE "assets"
|
||||||
|
@ -0,0 +1,16 @@
|
|||||||
|
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||||
|
|
||||||
|
export class AddSourceColumnToAssetFace1721249222549 implements MigrationInterface {
|
||||||
|
name = 'AddSourceColumnToAssetFace1721249222549'
|
||||||
|
|
||||||
|
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`CREATE TYPE sourceType AS ENUM ('machine-learning', 'exif');`);
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_faces" ADD "sourceType" sourceType NOT NULL DEFAULT 'machine-learning'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||||
|
await queryRunner.query(`ALTER TABLE "asset_faces" DROP COLUMN "sourceType"`);
|
||||||
|
await queryRunner.query(`DROP TYPE sourceType`);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -27,7 +27,7 @@ export class AddAssetFilesTable1724101822106 implements MigrationInterface {
|
|||||||
await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`);
|
await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`);
|
||||||
|
|
||||||
await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`);
|
await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`);
|
await queryRunner.query(`DROP INDEX "IDX_asset_files_assetId"`);
|
||||||
await queryRunner.query(`DROP TABLE "asset_files"`);
|
await queryRunner.query(`DROP TABLE "asset_files"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -47,8 +47,8 @@ export class NestedTagTable1724790460210 implements MigrationInterface {
|
|||||||
await queryRunner.query(`ALTER TABLE "tags" ADD "name" character varying NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "tags" ADD "name" character varying NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "tags" ADD "type" character varying NOT NULL`);
|
await queryRunner.query(`ALTER TABLE "tags" ADD "type" character varying NOT NULL`);
|
||||||
await queryRunner.query(`ALTER TABLE "tags" ADD "renameTagId" uuid`);
|
await queryRunner.query(`ALTER TABLE "tags" ADD "renameTagId" uuid`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_b1a2a7ed45c29179b5ad51548a"`);
|
await queryRunner.query(`DROP INDEX "IDX_b1a2a7ed45c29179b5ad51548a"`);
|
||||||
await queryRunner.query(`DROP INDEX "public"."IDX_15fbcbc67663c6bfc07b354c22"`);
|
await queryRunner.query(`DROP INDEX "IDX_15fbcbc67663c6bfc07b354c22"`);
|
||||||
await queryRunner.query(`DROP TABLE "tags_closure"`);
|
await queryRunner.query(`DROP TABLE "tags_closure"`);
|
||||||
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId")`);
|
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId")`);
|
||||||
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
|
||||||
|
@ -199,6 +199,7 @@ SELECT
|
|||||||
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
|
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
|
||||||
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
|
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
|
||||||
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
|
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
|
||||||
|
"AssetEntity__AssetEntity_faces"."sourceType" AS "AssetEntity__AssetEntity_faces_sourceType",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
|
||||||
|
@ -74,6 +74,7 @@ SELECT
|
|||||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||||
|
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||||
@ -106,6 +107,7 @@ FROM
|
|||||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||||
|
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||||
@ -141,6 +143,7 @@ FROM
|
|||||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||||
|
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
"AssetFaceEntity__AssetFaceEntity_person"."id" AS "AssetFaceEntity__AssetFaceEntity_person_id",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_person_createdAt",
|
||||||
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
"AssetFaceEntity__AssetFaceEntity_person"."updatedAt" AS "AssetFaceEntity__AssetFaceEntity_person_updatedAt",
|
||||||
@ -226,6 +229,16 @@ ORDER BY
|
|||||||
LIMIT
|
LIMIT
|
||||||
20
|
20
|
||||||
|
|
||||||
|
-- PersonRepository.getDistinctNames
|
||||||
|
SELECT DISTINCT
|
||||||
|
ON (lower("person"."name")) "person"."id" AS "person_id",
|
||||||
|
"person"."name" AS "person_name"
|
||||||
|
FROM
|
||||||
|
"person" "person"
|
||||||
|
WHERE
|
||||||
|
"person"."ownerId" = $1
|
||||||
|
AND "person"."name" != ''
|
||||||
|
|
||||||
-- PersonRepository.getStatistics
|
-- PersonRepository.getStatistics
|
||||||
SELECT
|
SELECT
|
||||||
COUNT(DISTINCT ("asset"."id")) AS "count"
|
COUNT(DISTINCT ("asset"."id")) AS "count"
|
||||||
@ -282,6 +295,7 @@ FROM
|
|||||||
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
|
"AssetEntity__AssetEntity_faces"."boundingBoxY1" AS "AssetEntity__AssetEntity_faces_boundingBoxY1",
|
||||||
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
|
"AssetEntity__AssetEntity_faces"."boundingBoxX2" AS "AssetEntity__AssetEntity_faces_boundingBoxX2",
|
||||||
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
|
"AssetEntity__AssetEntity_faces"."boundingBoxY2" AS "AssetEntity__AssetEntity_faces_boundingBoxY2",
|
||||||
|
"AssetEntity__AssetEntity_faces"."sourceType" AS "AssetEntity__AssetEntity_faces_sourceType",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."id" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_id",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."createdAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_createdAt",
|
||||||
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
|
"8258e303a73a72cf6abb13d73fb592dde0d68280"."updatedAt" AS "8258e303a73a72cf6abb13d73fb592dde0d68280_updatedAt",
|
||||||
@ -375,6 +389,7 @@ SELECT
|
|||||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||||
|
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType",
|
||||||
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
"AssetFaceEntity__AssetFaceEntity_asset"."id" AS "AssetFaceEntity__AssetFaceEntity_asset_id",
|
||||||
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
"AssetFaceEntity__AssetFaceEntity_asset"."deviceAssetId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceAssetId",
|
||||||
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
"AssetFaceEntity__AssetFaceEntity_asset"."ownerId" AS "AssetFaceEntity__AssetFaceEntity_asset_ownerId",
|
||||||
@ -425,7 +440,8 @@ SELECT
|
|||||||
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
"AssetFaceEntity"."boundingBoxX1" AS "AssetFaceEntity_boundingBoxX1",
|
||||||
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
"AssetFaceEntity"."boundingBoxY1" AS "AssetFaceEntity_boundingBoxY1",
|
||||||
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
"AssetFaceEntity"."boundingBoxX2" AS "AssetFaceEntity_boundingBoxX2",
|
||||||
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2"
|
"AssetFaceEntity"."boundingBoxY2" AS "AssetFaceEntity_boundingBoxY2",
|
||||||
|
"AssetFaceEntity"."sourceType" AS "AssetFaceEntity_sourceType"
|
||||||
FROM
|
FROM
|
||||||
"asset_faces" "AssetFaceEntity"
|
"asset_faces" "AssetFaceEntity"
|
||||||
WHERE
|
WHERE
|
||||||
|
@ -235,6 +235,7 @@ WITH
|
|||||||
"faces"."boundingBoxY1" AS "boundingBoxY1",
|
"faces"."boundingBoxY1" AS "boundingBoxY1",
|
||||||
"faces"."boundingBoxX2" AS "boundingBoxX2",
|
"faces"."boundingBoxX2" AS "boundingBoxX2",
|
||||||
"faces"."boundingBoxY2" AS "boundingBoxY2",
|
"faces"."boundingBoxY2" AS "boundingBoxY2",
|
||||||
|
"faces"."sourceType" AS "sourceType",
|
||||||
"search"."embedding" <= > $1 AS "distance"
|
"search"."embedding" <= > $1 AS "distance"
|
||||||
FROM
|
FROM
|
||||||
"asset_faces" "faces"
|
"asset_faces" "faces"
|
||||||
|
@ -31,6 +31,19 @@ export class DatabaseRepository implements IDatabaseRepository {
|
|||||||
this.logger.setContext(DatabaseRepository.name);
|
this.logger.setContext(DatabaseRepository.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async reconnect() {
|
||||||
|
try {
|
||||||
|
if (this.dataSource.isInitialized) {
|
||||||
|
await this.dataSource.destroy();
|
||||||
|
}
|
||||||
|
const { isInitialized } = await this.dataSource.initialize();
|
||||||
|
return isInitialized;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Database connection failed: ${error}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion> {
|
async getExtensionVersion(extension: DatabaseExtension): Promise<ExtensionVersion> {
|
||||||
const [res]: ExtensionVersion[] = await this.dataSource.query(
|
const [res]: ExtensionVersion[] = await this.dataSource.query(
|
||||||
`SELECT default_version as "availableVersion", installed_version as "installedVersion"
|
`SELECT default_version as "availableVersion", installed_version as "installedVersion"
|
||||||
|
@ -3,7 +3,7 @@ import { isLogLevelEnabled } from '@nestjs/common/services/utils/is-log-level-en
|
|||||||
import { ClsService } from 'nestjs-cls';
|
import { ClsService } from 'nestjs-cls';
|
||||||
import { LogLevel } from 'src/config';
|
import { LogLevel } from 'src/config';
|
||||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||||
import { LogColor } from 'src/utils/logger-colors';
|
import { LogColor } from 'src/utils/logger';
|
||||||
|
|
||||||
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
const LOG_LEVELS = [LogLevel.VERBOSE, LogLevel.DEBUG, LogLevel.LOG, LogLevel.WARN, LogLevel.ERROR, LogLevel.FATAL];
|
||||||
|
|
||||||
|
@ -56,11 +56,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getCountries(userId: string): Promise<string[]> {
|
async getCountries(userIds: string[]): Promise<string[]> {
|
||||||
const results = await this.exifRepository
|
const results = await this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
.leftJoin('exif.asset', 'asset')
|
.leftJoin('exif.asset', 'asset')
|
||||||
.where('asset.ownerId = :userId', { userId })
|
.where('asset.ownerId IN (:...userIds )', { userIds })
|
||||||
.select('exif.country', 'country')
|
.select('exif.country', 'country')
|
||||||
.distinctOn(['exif.country'])
|
.distinctOn(['exif.country'])
|
||||||
.getRawMany<{ country: string }>();
|
.getRawMany<{ country: string }>();
|
||||||
@ -69,11 +69,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||||
async getStates(userId: string, country: string | undefined): Promise<string[]> {
|
async getStates(userIds: string[], country: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
.leftJoin('exif.asset', 'asset')
|
.leftJoin('exif.asset', 'asset')
|
||||||
.where('asset.ownerId = :userId', { userId })
|
.where('asset.ownerId IN (:...userIds )', { userIds })
|
||||||
.select('exif.state', 'state')
|
.select('exif.state', 'state')
|
||||||
.distinctOn(['exif.state']);
|
.distinctOn(['exif.state']);
|
||||||
|
|
||||||
@ -87,11 +87,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING, DummyValue.STRING] })
|
||||||
async getCities(userId: string, country: string | undefined, state: string | undefined): Promise<string[]> {
|
async getCities(userIds: string[], country: string | undefined, state: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
.leftJoin('exif.asset', 'asset')
|
.leftJoin('exif.asset', 'asset')
|
||||||
.where('asset.ownerId = :userId', { userId })
|
.where('asset.ownerId IN (:...userIds )', { userIds })
|
||||||
.select('exif.city', 'city')
|
.select('exif.city', 'city')
|
||||||
.distinctOn(['exif.city']);
|
.distinctOn(['exif.city']);
|
||||||
|
|
||||||
@ -109,11 +109,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||||
async getCameraMakes(userId: string, model: string | undefined): Promise<string[]> {
|
async getCameraMakes(userIds: string[], model: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
.leftJoin('exif.asset', 'asset')
|
.leftJoin('exif.asset', 'asset')
|
||||||
.where('asset.ownerId = :userId', { userId })
|
.where('asset.ownerId IN (:...userIds )', { userIds })
|
||||||
.select('exif.make', 'make')
|
.select('exif.make', 'make')
|
||||||
.distinctOn(['exif.make']);
|
.distinctOn(['exif.make']);
|
||||||
|
|
||||||
@ -126,11 +126,11 @@ export class MetadataRepository implements IMetadataRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
@GenerateSql({ params: [DummyValue.UUID, DummyValue.STRING] })
|
||||||
async getCameraModels(userId: string, make: string | undefined): Promise<string[]> {
|
async getCameraModels(userIds: string[], make: string | undefined): Promise<string[]> {
|
||||||
const query = this.exifRepository
|
const query = this.exifRepository
|
||||||
.createQueryBuilder('exif')
|
.createQueryBuilder('exif')
|
||||||
.leftJoin('exif.asset', 'asset')
|
.leftJoin('exif.asset', 'asset')
|
||||||
.where('asset.ownerId = :userId', { userId })
|
.where('asset.ownerId IN (:...userIds )', { userIds })
|
||||||
.select('exif.model', 'model')
|
.select('exif.model', 'model')
|
||||||
.distinctOn(['exif.model']);
|
.distinctOn(['exif.model']);
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import { Injectable } from '@nestjs/common';
|
import { Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
import { ChunkedArray, DummyValue, GenerateSql } from 'src/decorators';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
@ -8,8 +8,10 @@ import { AssetEntity } from 'src/entities/asset.entity';
|
|||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import {
|
import {
|
||||||
AssetFaceId,
|
AssetFaceId,
|
||||||
|
DeleteAllFacesOptions,
|
||||||
IPersonRepository,
|
IPersonRepository,
|
||||||
PeopleStatistics,
|
PeopleStatistics,
|
||||||
|
PersonNameResponse,
|
||||||
PersonNameSearchOptions,
|
PersonNameSearchOptions,
|
||||||
PersonSearchOptions,
|
PersonSearchOptions,
|
||||||
PersonStatistics,
|
PersonStatistics,
|
||||||
@ -17,12 +19,13 @@ import {
|
|||||||
} from 'src/interfaces/person.interface';
|
} from 'src/interfaces/person.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
import { Paginated, PaginationMode, PaginationOptions, paginate, paginatedBuilder } from 'src/utils/pagination';
|
||||||
import { FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
import { DataSource, FindManyOptions, FindOptionsRelations, FindOptionsSelect, In, Repository } from 'typeorm';
|
||||||
|
|
||||||
@Instrumentation()
|
@Instrumentation()
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PersonRepository implements IPersonRepository {
|
export class PersonRepository implements IPersonRepository {
|
||||||
constructor(
|
constructor(
|
||||||
|
@InjectDataSource() private dataSource: DataSource,
|
||||||
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>,
|
||||||
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
@InjectRepository(PersonEntity) private personRepository: Repository<PersonEntity>,
|
||||||
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
@InjectRepository(AssetFaceEntity) private assetFaceRepository: Repository<AssetFaceEntity>,
|
||||||
@ -49,7 +52,16 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
await this.personRepository.clear();
|
await this.personRepository.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
async deleteAllFaces(): Promise<void> {
|
async deleteAllFaces({ sourceType }: DeleteAllFacesOptions): Promise<void> {
|
||||||
|
if (sourceType) {
|
||||||
|
await this.assetFaceRepository
|
||||||
|
.createQueryBuilder('asset_faces')
|
||||||
|
.delete()
|
||||||
|
.andWhere('sourceType = :sourceType', { sourceType })
|
||||||
|
.execute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
await this.assetFaceRepository.query('TRUNCATE TABLE asset_faces CASCADE');
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -182,6 +194,21 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
return queryBuilder.getMany();
|
return queryBuilder.getMany();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GenerateSql({ params: [DummyValue.UUID, { withHidden: true }] })
|
||||||
|
getDistinctNames(userId: string, { withHidden }: PersonNameSearchOptions): Promise<PersonNameResponse[]> {
|
||||||
|
const queryBuilder = this.personRepository
|
||||||
|
.createQueryBuilder('person')
|
||||||
|
.select(['person.id', 'person.name'])
|
||||||
|
.distinctOn(['lower(person.name)'])
|
||||||
|
.where(`person.ownerId = :userId AND person.name != ''`, { userId });
|
||||||
|
|
||||||
|
if (!withHidden) {
|
||||||
|
queryBuilder.andWhere('person.isHidden = false');
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryBuilder.getMany();
|
||||||
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [DummyValue.UUID] })
|
@GenerateSql({ params: [DummyValue.UUID] })
|
||||||
async getStatistics(personId: string): Promise<PersonStatistics> {
|
async getStatistics(personId: string): Promise<PersonStatistics> {
|
||||||
const items = await this.assetFaceRepository
|
const items = await this.assetFaceRepository
|
||||||
@ -248,8 +275,8 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
create(entity: Partial<PersonEntity>): Promise<PersonEntity> {
|
create(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]> {
|
||||||
return this.personRepository.save(entity);
|
return this.personRepository.save(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
async createFaces(entities: AssetFaceEntity[]): Promise<string[]> {
|
||||||
@ -257,9 +284,16 @@ export class PersonRepository implements IPersonRepository {
|
|||||||
return res.map((row) => row.id);
|
return res.map((row) => row.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(entity: Partial<PersonEntity>): Promise<PersonEntity> {
|
async replaceFaces(assetId: string, entities: AssetFaceEntity[], sourceType: string): Promise<string[]> {
|
||||||
const { id } = await this.personRepository.save(entity);
|
return this.dataSource.transaction(async (manager) => {
|
||||||
return this.personRepository.findOneByOrFail({ id });
|
await manager.delete(AssetFaceEntity, { assetId, sourceType });
|
||||||
|
const assetFaces = await manager.save(AssetFaceEntity, entities);
|
||||||
|
return assetFaces.map(({ id }) => id);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async update(entities: Partial<PersonEntity>[]): Promise<PersonEntity[]> {
|
||||||
|
return await this.personRepository.save(entities);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
@GenerateSql({ params: [[{ assetId: DummyValue.UUID, personId: DummyValue.UUID }]] })
|
||||||
|
@ -115,7 +115,7 @@ export class AuditService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case PersonPathType.FACE: {
|
case PersonPathType.FACE: {
|
||||||
await this.personRepository.update({ id, thumbnailPath: pathValue });
|
await this.personRepository.update([{ id, thumbnailPath: pathValue }]);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
import { Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import { Duration } from 'luxon';
|
||||||
import semver from 'semver';
|
import semver from 'semver';
|
||||||
import { getVectorExtension } from 'src/database.config';
|
import { getVectorExtension } from 'src/database.config';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
@ -59,8 +60,12 @@ const messages = {
|
|||||||
If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`,
|
If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const RETRY_DURATION = Duration.fromObject({ seconds: 5 });
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class DatabaseService {
|
export class DatabaseService {
|
||||||
|
private reconnection?: NodeJS.Timeout;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||||
@ -117,6 +122,26 @@ export class DatabaseService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleConnectionError(error: Error) {
|
||||||
|
if (this.reconnection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.logger.error(`Database disconnected: ${error}`);
|
||||||
|
this.reconnection = setInterval(() => void this.reconnect(), RETRY_DURATION.toMillis());
|
||||||
|
}
|
||||||
|
|
||||||
|
private async reconnect() {
|
||||||
|
const isConnected = await this.databaseRepository.reconnect();
|
||||||
|
if (isConnected) {
|
||||||
|
this.logger.log('Database reconnected');
|
||||||
|
clearInterval(this.reconnection);
|
||||||
|
delete this.reconnection;
|
||||||
|
} else {
|
||||||
|
this.logger.warn(`Database connection failed, retrying in ${RETRY_DURATION.toHuman()}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async createExtension(extension: DatabaseExtension) {
|
private async createExtension(extension: DatabaseExtension) {
|
||||||
try {
|
try {
|
||||||
await this.databaseRepository.createExtension(extension);
|
await this.databaseRepository.createExtension(extension);
|
||||||
|
@ -117,7 +117,7 @@ export class MediaService {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.personRepository.update({ id: person.id, faceAssetId: face.id });
|
await this.personRepository.update([{ id: person.id, faceAssetId: face.id }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: person.id } });
|
||||||
@ -176,7 +176,7 @@ export class MediaService {
|
|||||||
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePreview({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
const [{ image }, [asset]] = await Promise.all([
|
const [{ image }, [asset]] = await Promise.all([
|
||||||
this.configCore.getConfig({ withCache: true }),
|
this.configCore.getConfig({ withCache: true }),
|
||||||
this.assetRepository.getByIds([id], { exifInfo: true }),
|
this.assetRepository.getByIds([id], { exifInfo: true, files: true }),
|
||||||
]);
|
]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
import { BinaryField } from 'exiftool-vendored';
|
import { BinaryField, ExifDateTime } from 'exiftool-vendored';
|
||||||
import { randomBytes } from 'node:crypto';
|
import { randomBytes } from 'node:crypto';
|
||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import { constants } from 'node:fs/promises';
|
import { constants } from 'node:fs/promises';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
@ -24,6 +24,8 @@ import { MetadataService, Orientation } from 'src/services/metadata.service';
|
|||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
import { fileStub } from 'test/fixtures/file.stub';
|
import { fileStub } from 'test/fixtures/file.stub';
|
||||||
import { probeStub } from 'test/fixtures/media.stub';
|
import { probeStub } from 'test/fixtures/media.stub';
|
||||||
|
import { metadataStub } from 'test/fixtures/metadata.stub';
|
||||||
|
import { personStub } from 'test/fixtures/person.stub';
|
||||||
import { tagStub } from 'test/fixtures/tag.stub';
|
import { tagStub } from 'test/fixtures/tag.stub';
|
||||||
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock';
|
||||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||||
@ -449,6 +451,60 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should extract hierarchy from HierarchicalSubject', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent|Child'] });
|
||||||
|
tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||||
|
tagMock.upsertValue.mockResolvedValueOnce(tagStub.child);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined });
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
|
userId: 'user-id',
|
||||||
|
value: 'Parent/Child',
|
||||||
|
parent: tagStub.parent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should extract ignore / characters in a HierarchicalSubject tag', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Mom/Dad'] });
|
||||||
|
tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenCalledWith({
|
||||||
|
userId: 'user-id',
|
||||||
|
value: 'Mom|Dad',
|
||||||
|
parent: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore HierarchicalSubject when TagsList is present', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({ HierarchicalSubject: ['Parent2|Child2'], TagsList: ['Parent/Child'] });
|
||||||
|
tagMock.upsertValue.mockResolvedValue(tagStub.parent);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined });
|
||||||
|
expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, {
|
||||||
|
userId: 'user-id',
|
||||||
|
value: 'Parent/Child',
|
||||||
|
parent: tagStub.parent,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove existing tags', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue({});
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
|
||||||
|
expect(tagMock.upsertAssetTags).toHaveBeenCalledWith({ assetId: 'asset-id', tagIds: [] });
|
||||||
|
});
|
||||||
|
|
||||||
it('should not apply motion photos if asset is video', async () => {
|
it('should not apply motion photos if asset is video', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]);
|
||||||
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer);
|
||||||
@ -701,6 +757,8 @@ describe(MetadataService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should save all metadata', async () => {
|
it('should save all metadata', async () => {
|
||||||
|
const dateForTest = new Date('1970-01-01T00:00:00.000-11:30');
|
||||||
|
|
||||||
const tags: ImmichTags = {
|
const tags: ImmichTags = {
|
||||||
BitsPerSample: 1,
|
BitsPerSample: 1,
|
||||||
ComponentBitDepth: 1,
|
ComponentBitDepth: 1,
|
||||||
@ -708,7 +766,7 @@ describe(MetadataService.name, () => {
|
|||||||
BitDepth: 1,
|
BitDepth: 1,
|
||||||
ColorBitDepth: 1,
|
ColorBitDepth: 1,
|
||||||
ColorSpace: '1',
|
ColorSpace: '1',
|
||||||
DateTimeOriginal: new Date('1970-01-01').toISOString(),
|
DateTimeOriginal: ExifDateTime.fromISO(dateForTest.toISOString()),
|
||||||
ExposureTime: '100ms',
|
ExposureTime: '100ms',
|
||||||
FocalLength: 20,
|
FocalLength: 20,
|
||||||
ImageDescription: 'test description',
|
ImageDescription: 'test description',
|
||||||
@ -717,11 +775,11 @@ describe(MetadataService.name, () => {
|
|||||||
MediaGroupUUID: 'livePhoto',
|
MediaGroupUUID: 'livePhoto',
|
||||||
Make: 'test-factory',
|
Make: 'test-factory',
|
||||||
Model: "'mockel'",
|
Model: "'mockel'",
|
||||||
ModifyDate: new Date('1970-01-01').toISOString(),
|
ModifyDate: ExifDateTime.fromISO(dateForTest.toISOString()),
|
||||||
Orientation: 0,
|
Orientation: 0,
|
||||||
ProfileDescription: 'extensive description',
|
ProfileDescription: 'extensive description',
|
||||||
ProjectionType: 'equirectangular',
|
ProjectionType: 'equirectangular',
|
||||||
tz: '+02:00',
|
tz: 'UTC-11:30',
|
||||||
Rating: 3,
|
Rating: 3,
|
||||||
};
|
};
|
||||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
@ -734,7 +792,7 @@ describe(MetadataService.name, () => {
|
|||||||
bitsPerSample: expect.any(Number),
|
bitsPerSample: expect.any(Number),
|
||||||
autoStackId: null,
|
autoStackId: null,
|
||||||
colorspace: tags.ColorSpace,
|
colorspace: tags.ColorSpace,
|
||||||
dateTimeOriginal: new Date('1970-01-01'),
|
dateTimeOriginal: dateForTest,
|
||||||
description: tags.ImageDescription,
|
description: tags.ImageDescription,
|
||||||
exifImageHeight: null,
|
exifImageHeight: null,
|
||||||
exifImageWidth: null,
|
exifImageWidth: null,
|
||||||
@ -760,11 +818,37 @@ describe(MetadataService.name, () => {
|
|||||||
expect(assetMock.update).toHaveBeenCalledWith({
|
expect(assetMock.update).toHaveBeenCalledWith({
|
||||||
id: assetStub.image.id,
|
id: assetStub.image.id,
|
||||||
duration: null,
|
duration: null,
|
||||||
fileCreatedAt: new Date('1970-01-01'),
|
fileCreatedAt: dateForTest,
|
||||||
localDateTime: new Date('1970-01-01'),
|
localDateTime: dateForTest,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should extract +00:00 timezone from raw value', async () => {
|
||||||
|
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
|
||||||
|
// https://github.com/photostructure/exiftool-vendored.js/issues/203
|
||||||
|
|
||||||
|
// this only tests our assumptions of exiftool-vendored, demonstrating the issue
|
||||||
|
const someDate = '2024-09-01T00:00:00.000';
|
||||||
|
expect(ExifDateTime.fromISO(someDate + 'Z')?.zone).toBe('UTC');
|
||||||
|
expect(ExifDateTime.fromISO(someDate + '+00:00')?.zone).toBe('UTC'); // this is the issue, should be UTC+0
|
||||||
|
expect(ExifDateTime.fromISO(someDate + '+04:00')?.zone).toBe('UTC+4');
|
||||||
|
|
||||||
|
const tags: ImmichTags = {
|
||||||
|
DateTimeOriginal: ExifDateTime.fromISO(someDate + '+00:00'),
|
||||||
|
tz: undefined,
|
||||||
|
};
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||||
|
metadataMock.readTags.mockResolvedValue(tags);
|
||||||
|
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]);
|
||||||
|
expect(assetMock.upsertExif).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
timeZone: 'UTC+0',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('should extract duration', async () => {
|
it('should extract duration', async () => {
|
||||||
assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]);
|
||||||
mediaMock.probe.mockResolvedValue({
|
mediaMock.probe.mockResolvedValue({
|
||||||
@ -883,6 +967,123 @@ describe(MetadataService.name, () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should skip importing metadata when the feature is disabled', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: false } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(personMock.getDistinctNames).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip importing metadata face for assets without tags.RegionInfo', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.empty);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(personMock.getDistinctNames).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip importing faces without name', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceNoName);
|
||||||
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
|
personMock.create.mockResolvedValue([]);
|
||||||
|
personMock.replaceFaces.mockResolvedValue([]);
|
||||||
|
personMock.update.mockResolvedValue([]);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(personMock.create).toHaveBeenCalledWith([]);
|
||||||
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
||||||
|
expect(personMock.update).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip importing faces with empty name', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFaceEmptyName);
|
||||||
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
|
personMock.create.mockResolvedValue([]);
|
||||||
|
personMock.replaceFaces.mockResolvedValue([]);
|
||||||
|
personMock.update.mockResolvedValue([]);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||||
|
expect(personMock.create).toHaveBeenCalledWith([]);
|
||||||
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(assetStub.primaryImage.id, [], SourceType.EXIF);
|
||||||
|
expect(personMock.update).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should apply metadata face tags creating new persons', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
||||||
|
personMock.getDistinctNames.mockResolvedValue([]);
|
||||||
|
personMock.create.mockResolvedValue([personStub.withName]);
|
||||||
|
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
||||||
|
personMock.update.mockResolvedValue([personStub.withName]);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
||||||
|
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
|
expect(personMock.create).toHaveBeenCalledWith([expect.objectContaining({ name: personStub.withName.name })]);
|
||||||
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
||||||
|
assetStub.primaryImage.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'random-uuid',
|
||||||
|
assetId: assetStub.primaryImage.id,
|
||||||
|
personId: 'random-uuid',
|
||||||
|
imageHeight: 100,
|
||||||
|
imageWidth: 100,
|
||||||
|
boundingBoxX1: 0,
|
||||||
|
boundingBoxX2: 10,
|
||||||
|
boundingBoxY1: 0,
|
||||||
|
boundingBoxY2: 10,
|
||||||
|
sourceType: SourceType.EXIF,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SourceType.EXIF,
|
||||||
|
);
|
||||||
|
expect(personMock.update).toHaveBeenCalledWith([{ id: 'random-uuid', faceAssetId: 'random-uuid' }]);
|
||||||
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||||
|
data: { id: personStub.withName.id },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should assign metadata face tags to existing persons', async () => {
|
||||||
|
assetMock.getByIds.mockResolvedValue([assetStub.primaryImage]);
|
||||||
|
systemMock.get.mockResolvedValue({ metadata: { faces: { import: true } } });
|
||||||
|
metadataMock.readTags.mockResolvedValue(metadataStub.withFace);
|
||||||
|
personMock.getDistinctNames.mockResolvedValue([{ id: personStub.withName.id, name: personStub.withName.name }]);
|
||||||
|
personMock.create.mockResolvedValue([]);
|
||||||
|
personMock.replaceFaces.mockResolvedValue(['face-asset-uuid']);
|
||||||
|
personMock.update.mockResolvedValue([personStub.withName]);
|
||||||
|
await sut.handleMetadataExtraction({ id: assetStub.primaryImage.id });
|
||||||
|
expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.primaryImage.id]);
|
||||||
|
expect(personMock.getDistinctNames).toHaveBeenCalledWith(assetStub.primaryImage.ownerId, { withHidden: true });
|
||||||
|
expect(personMock.create).toHaveBeenCalledWith([]);
|
||||||
|
expect(personMock.replaceFaces).toHaveBeenCalledWith(
|
||||||
|
assetStub.primaryImage.id,
|
||||||
|
[
|
||||||
|
{
|
||||||
|
id: 'random-uuid',
|
||||||
|
assetId: assetStub.primaryImage.id,
|
||||||
|
personId: personStub.withName.id,
|
||||||
|
imageHeight: 100,
|
||||||
|
imageWidth: 100,
|
||||||
|
boundingBoxX1: 0,
|
||||||
|
boundingBoxX2: 10,
|
||||||
|
boundingBoxY1: 0,
|
||||||
|
boundingBoxY2: 10,
|
||||||
|
sourceType: SourceType.EXIF,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
SourceType.EXIF,
|
||||||
|
);
|
||||||
|
expect(personMock.update).toHaveBeenCalledWith([]);
|
||||||
|
expect(jobMock.queueAll).toHaveBeenCalledWith([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleQueueSidecar', () => {
|
describe('handleQueueSidecar', () => {
|
||||||
|
@ -9,9 +9,11 @@ import { SystemConfig } from 'src/config';
|
|||||||
import { StorageCore } from 'src/cores/storage.core';
|
import { StorageCore } from 'src/cores/storage.core';
|
||||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||||
import { OnEmit } from 'src/decorators';
|
import { OnEmit } from 'src/decorators';
|
||||||
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { ExifEntity } from 'src/entities/exif.entity';
|
import { ExifEntity } from 'src/entities/exif.entity';
|
||||||
import { AssetType } from 'src/enum';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
|
import { AssetType, SourceType } from 'src/enum';
|
||||||
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
import { IAlbumRepository } from 'src/interfaces/album.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
@ -37,6 +39,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|||||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||||
import { ITagRepository } from 'src/interfaces/tag.interface';
|
import { ITagRepository } from 'src/interfaces/tag.interface';
|
||||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||||
|
import { isFaceImportEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { upsertTags } from 'src/utils/tag';
|
import { upsertTags } from 'src/utils/tag';
|
||||||
|
|
||||||
@ -104,7 +107,7 @@ export class MetadataService {
|
|||||||
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
@Inject(IMediaRepository) private mediaRepository: IMediaRepository,
|
||||||
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
@Inject(IMetadataRepository) private repository: IMetadataRepository,
|
||||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
@Inject(IPersonRepository) private personRepository: IPersonRepository,
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
@Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository,
|
||||||
@Inject(ITagRepository) private tagRepository: ITagRepository,
|
@Inject(ITagRepository) private tagRepository: ITagRepository,
|
||||||
@ -215,6 +218,7 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
async handleMetadataExtraction({ id }: IEntityJob): Promise<JobStatus> {
|
||||||
|
const { metadata } = await this.configCore.getConfig({ withCache: true });
|
||||||
const [asset] = await this.assetRepository.getByIds([id]);
|
const [asset] = await this.assetRepository.getByIds([id]);
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@ -253,6 +257,10 @@ export class MetadataService {
|
|||||||
metadataExtractedAt: new Date(),
|
metadataExtractedAt: new Date(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isFaceImportEnabled(metadata)) {
|
||||||
|
await this.applyTaggedFaces(asset, exifTags);
|
||||||
|
}
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -355,6 +363,16 @@ export class MetadataService {
|
|||||||
const tags: unknown[] = [];
|
const tags: unknown[] = [];
|
||||||
if (exifTags.TagsList) {
|
if (exifTags.TagsList) {
|
||||||
tags.push(...exifTags.TagsList);
|
tags.push(...exifTags.TagsList);
|
||||||
|
} else if (exifTags.HierarchicalSubject) {
|
||||||
|
tags.push(
|
||||||
|
exifTags.HierarchicalSubject.map((tag) =>
|
||||||
|
tag
|
||||||
|
// convert | to /
|
||||||
|
.replaceAll('/', '<PLACEHOLDER>')
|
||||||
|
.replaceAll('|', '/')
|
||||||
|
.replaceAll('<PLACEHOLDER>', '|'),
|
||||||
|
),
|
||||||
|
);
|
||||||
} else if (exifTags.Keywords) {
|
} else if (exifTags.Keywords) {
|
||||||
let keywords = exifTags.Keywords;
|
let keywords = exifTags.Keywords;
|
||||||
if (!Array.isArray(keywords)) {
|
if (!Array.isArray(keywords)) {
|
||||||
@ -363,11 +381,8 @@ export class MetadataService {
|
|||||||
tags.push(...keywords);
|
tags.push(...keywords);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tags.length > 0) {
|
|
||||||
const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) });
|
const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) });
|
||||||
const tagIds = results.map((tag) => tag.id);
|
await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds: results.map((tag) => tag.id) });
|
||||||
await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) {
|
private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) {
|
||||||
@ -502,6 +517,65 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async applyTaggedFaces(asset: AssetEntity, tags: ImmichTags) {
|
||||||
|
if (!tags.RegionInfo?.AppliedToDimensions || tags.RegionInfo.RegionList.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const discoveredFaces: Partial<AssetFaceEntity>[] = [];
|
||||||
|
const existingNames = await this.personRepository.getDistinctNames(asset.ownerId, { withHidden: true });
|
||||||
|
const existingNameMap = new Map(existingNames.map(({ id, name }) => [name.toLowerCase(), id]));
|
||||||
|
const missing: Partial<PersonEntity>[] = [];
|
||||||
|
const missingWithFaceAsset: Partial<PersonEntity>[] = [];
|
||||||
|
for (const region of tags.RegionInfo.RegionList) {
|
||||||
|
if (!region.Name) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageWidth = tags.RegionInfo.AppliedToDimensions.W;
|
||||||
|
const imageHeight = tags.RegionInfo.AppliedToDimensions.H;
|
||||||
|
const loweredName = region.Name.toLowerCase();
|
||||||
|
const personId = existingNameMap.get(loweredName) || this.cryptoRepository.randomUUID();
|
||||||
|
|
||||||
|
const face = {
|
||||||
|
id: this.cryptoRepository.randomUUID(),
|
||||||
|
personId,
|
||||||
|
assetId: asset.id,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
boundingBoxX1: Math.floor((region.Area.X - region.Area.W / 2) * imageWidth),
|
||||||
|
boundingBoxY1: Math.floor((region.Area.Y - region.Area.H / 2) * imageHeight),
|
||||||
|
boundingBoxX2: Math.floor((region.Area.X + region.Area.W / 2) * imageWidth),
|
||||||
|
boundingBoxY2: Math.floor((region.Area.Y + region.Area.H / 2) * imageHeight),
|
||||||
|
sourceType: SourceType.EXIF,
|
||||||
|
};
|
||||||
|
|
||||||
|
discoveredFaces.push(face);
|
||||||
|
if (!existingNameMap.has(loweredName)) {
|
||||||
|
missing.push({ id: personId, ownerId: asset.ownerId, name: region.Name });
|
||||||
|
missingWithFaceAsset.push({ id: personId, faceAssetId: face.id });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
this.logger.debug(`Creating missing persons: ${missing.map((p) => `${p.name}/${p.id}`)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPersons = await this.personRepository.create(missing);
|
||||||
|
|
||||||
|
const faceIds = await this.personRepository.replaceFaces(asset.id, discoveredFaces, SourceType.EXIF);
|
||||||
|
this.logger.debug(`Created ${faceIds.length} faces for asset ${asset.id}`);
|
||||||
|
|
||||||
|
await this.personRepository.update(missingWithFaceAsset);
|
||||||
|
|
||||||
|
await this.jobRepository.queueAll(
|
||||||
|
newPersons.map((person) => ({
|
||||||
|
name: JobName.GENERATE_PERSON_THUMBNAIL,
|
||||||
|
data: { id: person.id },
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private async exifData(
|
private async exifData(
|
||||||
asset: AssetEntity,
|
asset: AssetEntity,
|
||||||
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> {
|
): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> {
|
||||||
@ -521,12 +595,16 @@ export class MetadataService {
|
|||||||
|
|
||||||
this.logger.verbose('Exif Tags', exifTags);
|
this.logger.verbose('Exif Tags', exifTags);
|
||||||
|
|
||||||
|
const dateTimeOriginalWithRawValue = this.getDateTimeOriginalWithRawValue(exifTags);
|
||||||
|
const dateTimeOriginal = dateTimeOriginalWithRawValue.exifDate ?? asset.fileCreatedAt;
|
||||||
|
const timeZone = this.getTimeZone(exifTags, dateTimeOriginalWithRawValue.rawValue);
|
||||||
|
|
||||||
const exifData = {
|
const exifData = {
|
||||||
// altitude: tags.GPSAltitude ?? null,
|
// altitude: tags.GPSAltitude ?? null,
|
||||||
assetId: asset.id,
|
assetId: asset.id,
|
||||||
bitsPerSample: this.getBitsPerSample(exifTags),
|
bitsPerSample: this.getBitsPerSample(exifTags),
|
||||||
colorspace: exifTags.ColorSpace ?? null,
|
colorspace: exifTags.ColorSpace ?? null,
|
||||||
dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt,
|
dateTimeOriginal,
|
||||||
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
description: String(exifTags.ImageDescription || exifTags.Description || '').trim(),
|
||||||
exifImageHeight: validate(exifTags.ImageHeight),
|
exifImageHeight: validate(exifTags.ImageHeight),
|
||||||
exifImageWidth: validate(exifTags.ImageWidth),
|
exifImageWidth: validate(exifTags.ImageWidth),
|
||||||
@ -547,7 +625,7 @@ export class MetadataService {
|
|||||||
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
||||||
profileDescription: exifTags.ProfileDescription || null,
|
profileDescription: exifTags.ProfileDescription || null,
|
||||||
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
||||||
timeZone: exifTags.tz ?? null,
|
timeZone,
|
||||||
rating: exifTags.Rating ?? null,
|
rating: exifTags.Rating ?? null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -568,10 +646,25 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
|
private getDateTimeOriginal(tags: ImmichTags | Tags | null) {
|
||||||
if (!tags) {
|
return this.getDateTimeOriginalWithRawValue(tags).exifDate;
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
return exifDate(firstDateTime(tags as Tags, EXIF_DATE_TAGS));
|
|
||||||
|
private getDateTimeOriginalWithRawValue(tags: ImmichTags | Tags | null): { exifDate: Date | null; rawValue: string } {
|
||||||
|
if (!tags) {
|
||||||
|
return { exifDate: null, rawValue: '' };
|
||||||
|
}
|
||||||
|
const first = firstDateTime(tags as Tags, EXIF_DATE_TAGS);
|
||||||
|
return { exifDate: exifDate(first), rawValue: first?.rawValue ?? '' };
|
||||||
|
}
|
||||||
|
|
||||||
|
private getTimeZone(exifTags: ImmichTags, rawValue: string) {
|
||||||
|
const timeZone = exifTags.tz ?? null;
|
||||||
|
if (timeZone == null && rawValue.endsWith('+00:00')) {
|
||||||
|
// exiftool-vendored returns "no timezone" information even though "+00:00" might be set explicitly
|
||||||
|
// https://github.com/photostructure/exiftool-vendored.js/issues/203
|
||||||
|
return 'UTC+0';
|
||||||
|
}
|
||||||
|
return timeZone;
|
||||||
}
|
}
|
||||||
|
|
||||||
private getBitsPerSample(tags: ImmichTags): number | null {
|
private getBitsPerSample(tags: ImmichTags): number | null {
|
||||||
|
@ -3,7 +3,7 @@ import { Colorspace } from 'src/config';
|
|||||||
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto';
|
||||||
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto';
|
||||||
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
||||||
import { SystemMetadataKey } from 'src/enum';
|
import { SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
@ -241,18 +241,18 @@ describe(PersonService.name, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's name", async () => {
|
it("should update a person's name", async () => {
|
||||||
personMock.update.mockResolvedValue(personStub.withName);
|
personMock.update.mockResolvedValue([personStub.withName]);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
|
await expect(sut.update(authStub.admin, 'person-1', { name: 'Person 1' })).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', name: 'Person 1' });
|
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', name: 'Person 1' }]);
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's date of birth", async () => {
|
it("should update a person's date of birth", async () => {
|
||||||
personMock.update.mockResolvedValue(personStub.withBirthDate);
|
personMock.update.mockResolvedValue([personStub.withBirthDate]);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
@ -264,25 +264,25 @@ describe(PersonService.name, () => {
|
|||||||
isHidden: false,
|
isHidden: false,
|
||||||
updatedAt: expect.any(Date),
|
updatedAt: expect.any(Date),
|
||||||
});
|
});
|
||||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', birthDate: '1976-06-30' });
|
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', birthDate: '1976-06-30' }]);
|
||||||
expect(jobMock.queue).not.toHaveBeenCalled();
|
expect(jobMock.queue).not.toHaveBeenCalled();
|
||||||
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
expect(jobMock.queueAll).not.toHaveBeenCalled();
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update a person visibility', async () => {
|
it('should update a person visibility', async () => {
|
||||||
personMock.update.mockResolvedValue(personStub.withName);
|
personMock.update.mockResolvedValue([personStub.withName]);
|
||||||
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
personMock.getAssets.mockResolvedValue([assetStub.image]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
|
|
||||||
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
|
await expect(sut.update(authStub.admin, 'person-1', { isHidden: false })).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', isHidden: false });
|
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', isHidden: false }]);
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should update a person's thumbnailPath", async () => {
|
it("should update a person's thumbnailPath", async () => {
|
||||||
personMock.update.mockResolvedValue(personStub.withName);
|
personMock.update.mockResolvedValue([personStub.withName]);
|
||||||
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
personMock.getFacesByIds.mockResolvedValue([faceStub.face1]);
|
||||||
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set([assetStub.image.id]));
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValue(new Set(['person-1']));
|
||||||
@ -291,7 +291,7 @@ describe(PersonService.name, () => {
|
|||||||
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
|
sut.update(authStub.admin, 'person-1', { featureFaceAssetId: faceStub.face1.assetId }),
|
||||||
).resolves.toEqual(responseDto);
|
).resolves.toEqual(responseDto);
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith({ id: 'person-1', faceAssetId: faceStub.face1.id });
|
expect(personMock.update).toHaveBeenCalledWith([{ id: 'person-1', faceAssetId: faceStub.face1.id }]);
|
||||||
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
expect(personMock.getFacesByIds).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
assetId: faceStub.face1.assetId,
|
assetId: faceStub.face1.assetId,
|
||||||
@ -441,11 +441,11 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
describe('createPerson', () => {
|
describe('createPerson', () => {
|
||||||
it('should create a new person', async () => {
|
it('should create a new person', async () => {
|
||||||
personMock.create.mockResolvedValue(personStub.primaryPerson);
|
personMock.create.mockResolvedValue([personStub.primaryPerson]);
|
||||||
|
|
||||||
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
|
await expect(sut.create(authStub.admin, {})).resolves.toBe(personStub.primaryPerson);
|
||||||
|
|
||||||
expect(personMock.create).toHaveBeenCalledWith({ ownerId: authStub.admin.user.id });
|
expect(personMock.create).toHaveBeenCalledWith([{ ownerId: authStub.admin.user.id }]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -496,6 +496,7 @@ describe(PersonService.name, () => {
|
|||||||
items: [personStub.withName],
|
items: [personStub.withName],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleQueueDetectFaces({ force: true });
|
await sut.handleQueueDetectFaces({ force: true });
|
||||||
|
|
||||||
@ -510,7 +511,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
it('should delete existing people and faces if forced', async () => {
|
it('should delete existing people and faces if forced', async () => {
|
||||||
personMock.getAll.mockResolvedValue({
|
personMock.getAll.mockResolvedValue({
|
||||||
items: [faceStub.face1.person],
|
items: [faceStub.face1.person, personStub.randomPerson],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
personMock.getAllFaces.mockResolvedValue({
|
personMock.getAllFaces.mockResolvedValue({
|
||||||
@ -521,6 +522,7 @@ describe(PersonService.name, () => {
|
|||||||
items: [assetStub.image],
|
items: [assetStub.image],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||||
|
|
||||||
await sut.handleQueueDetectFaces({ force: true });
|
await sut.handleQueueDetectFaces({ force: true });
|
||||||
|
|
||||||
@ -531,8 +533,8 @@ describe(PersonService.name, () => {
|
|||||||
data: { id: assetStub.image.id },
|
data: { id: assetStub.image.id },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(personMock.delete).toHaveBeenCalledWith([faceStub.face1.person]);
|
expect(personMock.delete).toHaveBeenCalledWith([personStub.randomPerson]);
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(faceStub.face1.person.thumbnailPath);
|
expect(storageMock.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -561,10 +563,14 @@ describe(PersonService.name, () => {
|
|||||||
items: [faceStub.face1],
|
items: [faceStub.face1],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({});
|
await sut.handleQueueRecognizeFaces({});
|
||||||
|
|
||||||
expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, { where: { personId: IsNull() } });
|
expect(personMock.getAllFaces).toHaveBeenCalledWith(
|
||||||
|
{ skip: 0, take: 1000 },
|
||||||
|
{ where: { personId: IsNull(), sourceType: IsNull() } },
|
||||||
|
);
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.FACIAL_RECOGNITION,
|
name: JobName.FACIAL_RECOGNITION,
|
||||||
@ -586,6 +592,7 @@ describe(PersonService.name, () => {
|
|||||||
items: [faceStub.face1],
|
items: [faceStub.face1],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({ force: true });
|
await sut.handleQueueRecognizeFaces({ force: true });
|
||||||
|
|
||||||
@ -616,6 +623,8 @@ describe(PersonService.name, () => {
|
|||||||
items: [faceStub.face1],
|
items: [faceStub.face1],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
||||||
|
|
||||||
expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
|
expect(systemMock.get).toHaveBeenCalledWith(SystemMetadataKey.FACIAL_RECOGNITION_STATE);
|
||||||
@ -641,6 +650,7 @@ describe(PersonService.name, () => {
|
|||||||
items: [faceStub.face1],
|
items: [faceStub.face1],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([]);
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
await sut.handleQueueRecognizeFaces({ force: true, nightly: true });
|
||||||
|
|
||||||
@ -654,7 +664,7 @@ describe(PersonService.name, () => {
|
|||||||
it('should delete existing people and faces if forced', async () => {
|
it('should delete existing people and faces if forced', async () => {
|
||||||
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
jobMock.getJobCounts.mockResolvedValue({ active: 1, waiting: 0, paused: 0, completed: 0, failed: 0, delayed: 0 });
|
||||||
personMock.getAll.mockResolvedValue({
|
personMock.getAll.mockResolvedValue({
|
||||||
items: [faceStub.face1.person],
|
items: [faceStub.face1.person, personStub.randomPerson],
|
||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
personMock.getAllFaces.mockResolvedValue({
|
personMock.getAllFaces.mockResolvedValue({
|
||||||
@ -662,17 +672,19 @@ describe(PersonService.name, () => {
|
|||||||
hasNextPage: false,
|
hasNextPage: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
personMock.getAllWithoutFaces.mockResolvedValue([personStub.randomPerson]);
|
||||||
|
|
||||||
await sut.handleQueueRecognizeFaces({ force: true });
|
await sut.handleQueueRecognizeFaces({ force: true });
|
||||||
|
|
||||||
expect(personMock.getAllFaces).toHaveBeenCalledWith({ skip: 0, take: 1000 }, {});
|
expect(personMock.deleteAllFaces).toHaveBeenCalledWith({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
expect(jobMock.queueAll).toHaveBeenCalledWith([
|
||||||
{
|
{
|
||||||
name: JobName.FACIAL_RECOGNITION,
|
name: JobName.FACIAL_RECOGNITION,
|
||||||
data: { id: faceStub.face1.id, deferred: false },
|
data: { id: faceStub.face1.id, deferred: false },
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
expect(personMock.delete).toHaveBeenCalledWith([faceStub.face1.person]);
|
expect(personMock.delete).toHaveBeenCalledWith([personStub.randomPerson]);
|
||||||
expect(storageMock.unlink).toHaveBeenCalledWith(faceStub.face1.person.thumbnailPath);
|
expect(storageMock.unlink).toHaveBeenCalledWith(personStub.randomPerson.thumbnailPath);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -807,7 +819,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(faceStub.primaryFace1.person);
|
personMock.create.mockResolvedValue([faceStub.primaryFace1.person]);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -832,14 +844,16 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 1 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue([personStub.withName]);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
expect(personMock.create).toHaveBeenCalledWith({
|
expect(personMock.create).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
ownerId: faceStub.noPerson1.asset.ownerId,
|
ownerId: faceStub.noPerson1.asset.ownerId,
|
||||||
faceAssetId: faceStub.noPerson1.id,
|
faceAssetId: faceStub.noPerson1.id,
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
expect(personMock.reassignFaces).toHaveBeenCalledWith({
|
||||||
faceIds: [faceStub.noPerson1.id],
|
faceIds: [faceStub.noPerson1.id],
|
||||||
newPersonId: personStub.withName.id,
|
newPersonId: personStub.withName.id,
|
||||||
@ -851,7 +865,7 @@ describe(PersonService.name, () => {
|
|||||||
|
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue([personStub.withName]);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -870,7 +884,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||||
searchMock.searchFaces.mockResolvedValue(faces);
|
searchMock.searchFaces.mockResolvedValue(faces);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue([personStub.withName]);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id });
|
||||||
|
|
||||||
@ -892,7 +906,7 @@ describe(PersonService.name, () => {
|
|||||||
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
systemMock.get.mockResolvedValue({ machineLearning: { facialRecognition: { minFaces: 3 } } });
|
||||||
searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
searchMock.searchFaces.mockResolvedValueOnce(faces).mockResolvedValueOnce([]);
|
||||||
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
personMock.getFaceByIdWithAssets.mockResolvedValue(faceStub.noPerson1);
|
||||||
personMock.create.mockResolvedValue(personStub.withName);
|
personMock.create.mockResolvedValue([personStub.withName]);
|
||||||
|
|
||||||
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
await sut.handleRecognizeFaces({ id: faceStub.noPerson1.id, deferred: true });
|
||||||
|
|
||||||
@ -965,10 +979,12 @@ describe(PersonService.name, () => {
|
|||||||
processInvalidImages: false,
|
processInvalidImages: false,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(personMock.update).toHaveBeenCalledWith({
|
expect(personMock.update).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
id: 'person-1',
|
id: 'person-1',
|
||||||
thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
|
thumbnailPath: 'upload/thumbs/admin_id/pe/rs/person-1.jpeg',
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate a thumbnail without going negative', async () => {
|
it('should generate a thumbnail without going negative', async () => {
|
||||||
@ -1087,7 +1103,7 @@ describe(PersonService.name, () => {
|
|||||||
it('should merge two people with smart merge', async () => {
|
it('should merge two people with smart merge', async () => {
|
||||||
personMock.getById.mockResolvedValueOnce(personStub.randomPerson);
|
personMock.getById.mockResolvedValueOnce(personStub.randomPerson);
|
||||||
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
personMock.getById.mockResolvedValueOnce(personStub.primaryPerson);
|
||||||
personMock.update.mockResolvedValue({ ...personStub.randomPerson, name: personStub.primaryPerson.name });
|
personMock.update.mockResolvedValue([{ ...personStub.randomPerson, name: personStub.primaryPerson.name }]);
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-3']));
|
||||||
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
accessMock.person.checkOwnerAccess.mockResolvedValueOnce(new Set(['person-1']));
|
||||||
|
|
||||||
@ -1100,10 +1116,12 @@ describe(PersonService.name, () => {
|
|||||||
oldPersonId: personStub.primaryPerson.id,
|
oldPersonId: personStub.primaryPerson.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(personMock.update).toHaveBeenCalledWith({
|
expect(personMock.update).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
id: personStub.randomPerson.id,
|
id: personStub.randomPerson.id,
|
||||||
name: personStub.primaryPerson.name,
|
name: personStub.primaryPerson.name,
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
expect(accessMock.person.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['person-1']));
|
||||||
});
|
});
|
||||||
@ -1177,6 +1195,7 @@ describe(PersonService.name, () => {
|
|||||||
id: faceStub.face1.id,
|
id: faceStub.face1.id,
|
||||||
imageHeight: 1024,
|
imageHeight: 1024,
|
||||||
imageWidth: 1024,
|
imageWidth: 1024,
|
||||||
|
sourceType: SourceType.MACHINE_LEARNING,
|
||||||
person: mapPerson(personStub.withName),
|
person: mapPerson(personStub.withName),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -25,7 +25,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity';
|
|||||||
import { AssetEntity } from 'src/entities/asset.entity';
|
import { AssetEntity } from 'src/entities/asset.entity';
|
||||||
import { PersonPathType } from 'src/entities/move.entity';
|
import { PersonPathType } from 'src/entities/move.entity';
|
||||||
import { PersonEntity } from 'src/entities/person.entity';
|
import { PersonEntity } from 'src/entities/person.entity';
|
||||||
import { AssetType, Permission, SystemMetadataKey } from 'src/enum';
|
import { AssetType, Permission, SourceType, SystemMetadataKey } from 'src/enum';
|
||||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||||
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface';
|
||||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
@ -53,7 +53,7 @@ import { checkAccess, requireAccess } from 'src/utils/access';
|
|||||||
import { getAssetFiles } from 'src/utils/asset.util';
|
import { getAssetFiles } from 'src/utils/asset.util';
|
||||||
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
import { CacheControl, ImmichFileResponse } from 'src/utils/file';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
import { isFacialRecognitionEnabled } from 'src/utils/misc';
|
import { isFaceImportEnabled, isFacialRecognitionEnabled } from 'src/utils/misc';
|
||||||
import { usePagination } from 'src/utils/pagination';
|
import { usePagination } from 'src/utils/pagination';
|
||||||
import { IsNull } from 'typeorm';
|
import { IsNull } from 'typeorm';
|
||||||
|
|
||||||
@ -173,10 +173,7 @@ export class PersonService {
|
|||||||
const assetFace = await this.repository.getRandomFace(personId);
|
const assetFace = await this.repository.getRandomFace(personId);
|
||||||
|
|
||||||
if (assetFace !== null) {
|
if (assetFace !== null) {
|
||||||
await this.repository.update({
|
await this.repository.update([{ id: personId, faceAssetId: assetFace.id }]);
|
||||||
id: personId,
|
|
||||||
faceAssetId: assetFace.id,
|
|
||||||
});
|
|
||||||
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
|
jobs.push({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: personId } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -214,13 +211,16 @@ export class PersonService {
|
|||||||
return assets.map((asset) => mapAsset(asset));
|
return assets.map((asset) => mapAsset(asset));
|
||||||
}
|
}
|
||||||
|
|
||||||
create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
async create(auth: AuthDto, dto: PersonCreateDto): Promise<PersonResponseDto> {
|
||||||
return this.repository.create({
|
const [created] = await this.repository.create([
|
||||||
|
{
|
||||||
ownerId: auth.user.id,
|
ownerId: auth.user.id,
|
||||||
name: dto.name,
|
name: dto.name,
|
||||||
birthDate: dto.birthDate,
|
birthDate: dto.birthDate,
|
||||||
isHidden: dto.isHidden,
|
isHidden: dto.isHidden,
|
||||||
});
|
},
|
||||||
|
]);
|
||||||
|
return created;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise<PersonResponseDto> {
|
||||||
@ -239,7 +239,7 @@ export class PersonService {
|
|||||||
faceId = face.id;
|
faceId = face.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
const person = await this.repository.update({ id, faceAssetId: faceId, name, birthDate, isHidden });
|
const [person] = await this.repository.update([{ id, faceAssetId: faceId, name, birthDate, isHidden }]);
|
||||||
|
|
||||||
if (assetId) {
|
if (assetId) {
|
||||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id } });
|
||||||
@ -296,8 +296,8 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.deleteAllPeople();
|
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
await this.repository.deleteAllFaces();
|
await this.handlePersonCleanup();
|
||||||
}
|
}
|
||||||
|
|
||||||
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => {
|
||||||
@ -339,11 +339,7 @@ export class PersonService {
|
|||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
if (!asset.isVisible || asset.faces.length > 0) {
|
||||||
return JobStatus.SKIPPED;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset.isVisible) {
|
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -408,7 +404,8 @@ export class PersonService {
|
|||||||
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
|
const { waiting } = await this.jobRepository.getJobCounts(QueueName.FACIAL_RECOGNITION);
|
||||||
|
|
||||||
if (force) {
|
if (force) {
|
||||||
await this.deleteAllPeople();
|
await this.repository.deleteAllFaces({ sourceType: SourceType.MACHINE_LEARNING });
|
||||||
|
await this.handlePersonCleanup();
|
||||||
} else if (waiting) {
|
} else if (waiting) {
|
||||||
this.logger.debug(
|
this.logger.debug(
|
||||||
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
`Skipping facial recognition queueing because ${waiting} job${waiting > 1 ? 's are' : ' is'} already queued`,
|
||||||
@ -418,7 +415,9 @@ export class PersonService {
|
|||||||
|
|
||||||
const lastRun = new Date().toISOString();
|
const lastRun = new Date().toISOString();
|
||||||
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
const facePagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
||||||
this.repository.getAllFaces(pagination, { where: force ? undefined : { personId: IsNull() } }),
|
this.repository.getAllFaces(pagination, {
|
||||||
|
where: force ? undefined : { personId: IsNull(), sourceType: IsNull() },
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const page of facePagination) {
|
for await (const page of facePagination) {
|
||||||
@ -441,13 +440,18 @@ export class PersonService {
|
|||||||
const face = await this.repository.getFaceByIdWithAssets(
|
const face = await this.repository.getFaceByIdWithAssets(
|
||||||
id,
|
id,
|
||||||
{ person: true, asset: true, faceSearch: true },
|
{ person: true, asset: true, faceSearch: true },
|
||||||
{ id: true, personId: true, faceSearch: { embedding: true } },
|
{ id: true, personId: true, sourceType: true, faceSearch: { embedding: true } },
|
||||||
);
|
);
|
||||||
if (!face || !face.asset) {
|
if (!face || !face.asset) {
|
||||||
this.logger.warn(`Face ${id} not found`);
|
this.logger.warn(`Face ${id} not found`);
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (face.sourceType !== SourceType.MACHINE_LEARNING) {
|
||||||
|
this.logger.warn(`Skipping face ${id} due to source ${face.sourceType}`);
|
||||||
|
return JobStatus.SKIPPED;
|
||||||
|
}
|
||||||
|
|
||||||
if (!face.faceSearch?.embedding) {
|
if (!face.faceSearch?.embedding) {
|
||||||
this.logger.warn(`Face ${id} does not have an embedding`);
|
this.logger.warn(`Face ${id} does not have an embedding`);
|
||||||
return JobStatus.FAILED;
|
return JobStatus.FAILED;
|
||||||
@ -497,7 +501,7 @@ export class PersonService {
|
|||||||
|
|
||||||
if (isCore && !personId) {
|
if (isCore && !personId) {
|
||||||
this.logger.log(`Creating new person for face ${id}`);
|
this.logger.log(`Creating new person for face ${id}`);
|
||||||
const newPerson = await this.repository.create({ ownerId: face.asset.ownerId, faceAssetId: face.id });
|
const [newPerson] = await this.repository.create([{ ownerId: face.asset.ownerId, faceAssetId: face.id }]);
|
||||||
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
await this.jobRepository.queue({ name: JobName.GENERATE_PERSON_THUMBNAIL, data: { id: newPerson.id } });
|
||||||
personId = newPerson.id;
|
personId = newPerson.id;
|
||||||
}
|
}
|
||||||
@ -522,8 +526,8 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
async handleGeneratePersonThumbnail(data: IEntityJob): Promise<JobStatus> {
|
||||||
const { machineLearning, image } = await this.configCore.getConfig({ withCache: true });
|
const { machineLearning, metadata, image } = await this.configCore.getConfig({ withCache: true });
|
||||||
if (!isFacialRecognitionEnabled(machineLearning)) {
|
if (!isFacialRecognitionEnabled(machineLearning) && !isFaceImportEnabled(metadata)) {
|
||||||
return JobStatus.SKIPPED;
|
return JobStatus.SKIPPED;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -573,7 +577,7 @@ export class PersonService {
|
|||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
await this.mediaRepository.generateThumbnail(inputPath, thumbnailPath, thumbnailOptions);
|
||||||
await this.repository.update({ id: person.id, thumbnailPath });
|
await this.repository.update([{ id: person.id, thumbnailPath }]);
|
||||||
|
|
||||||
return JobStatus.SUCCESS;
|
return JobStatus.SUCCESS;
|
||||||
}
|
}
|
||||||
@ -620,7 +624,7 @@ export class PersonService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Object.keys(update).length > 0) {
|
if (Object.keys(update).length > 0) {
|
||||||
primaryPerson = await this.repository.update({ id: primaryPerson.id, ...update });
|
[primaryPerson] = await this.repository.update([{ id: primaryPerson.id, ...update }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const mergeName = mergePerson.name || mergePerson.id;
|
const mergeName = mergePerson.name || mergePerson.id;
|
||||||
|
@ -103,7 +103,7 @@ describe(SearchService.name, () => {
|
|||||||
await expect(
|
await expect(
|
||||||
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
|
sut.getSearchSuggestions(authStub.user1, { includeNull: true, type: SearchSuggestionType.COUNTRY }),
|
||||||
).resolves.toEqual(['USA', null]);
|
).resolves.toEqual(['USA', null]);
|
||||||
expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id);
|
expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return search suggestions (without null)', async () => {
|
it('should return search suggestions (without null)', async () => {
|
||||||
@ -111,7 +111,7 @@ describe(SearchService.name, () => {
|
|||||||
await expect(
|
await expect(
|
||||||
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
|
sut.getSearchSuggestions(authStub.user1, { includeNull: false, type: SearchSuggestionType.COUNTRY }),
|
||||||
).resolves.toEqual(['USA']);
|
).resolves.toEqual(['USA']);
|
||||||
expect(metadataMock.getCountries).toHaveBeenCalledWith(authStub.user1.user.id);
|
expect(metadataMock.getCountries).toHaveBeenCalledWith([authStub.user1.user.id]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -121,26 +121,27 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) {
|
async getSearchSuggestions(auth: AuthDto, dto: SearchSuggestionRequestDto) {
|
||||||
const results = await this.getSuggestions(auth.user.id, dto);
|
const userIds = await this.getUserIdsToSearch(auth);
|
||||||
|
const results = await this.getSuggestions(userIds, dto);
|
||||||
return results.filter((result) => (dto.includeNull ? true : result !== null));
|
return results.filter((result) => (dto.includeNull ? true : result !== null));
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSuggestions(userId: string, dto: SearchSuggestionRequestDto) {
|
private getSuggestions(userIds: string[], dto: SearchSuggestionRequestDto) {
|
||||||
switch (dto.type) {
|
switch (dto.type) {
|
||||||
case SearchSuggestionType.COUNTRY: {
|
case SearchSuggestionType.COUNTRY: {
|
||||||
return this.metadataRepository.getCountries(userId);
|
return this.metadataRepository.getCountries(userIds);
|
||||||
}
|
}
|
||||||
case SearchSuggestionType.STATE: {
|
case SearchSuggestionType.STATE: {
|
||||||
return this.metadataRepository.getStates(userId, dto.country);
|
return this.metadataRepository.getStates(userIds, dto.country);
|
||||||
}
|
}
|
||||||
case SearchSuggestionType.CITY: {
|
case SearchSuggestionType.CITY: {
|
||||||
return this.metadataRepository.getCities(userId, dto.country, dto.state);
|
return this.metadataRepository.getCities(userIds, dto.country, dto.state);
|
||||||
}
|
}
|
||||||
case SearchSuggestionType.CAMERA_MAKE: {
|
case SearchSuggestionType.CAMERA_MAKE: {
|
||||||
return this.metadataRepository.getCameraMakes(userId, dto.model);
|
return this.metadataRepository.getCameraMakes(userIds, dto.model);
|
||||||
}
|
}
|
||||||
case SearchSuggestionType.CAMERA_MODEL: {
|
case SearchSuggestionType.CAMERA_MODEL: {
|
||||||
return this.metadataRepository.getCameraModels(userId, dto.make);
|
return this.metadataRepository.getCameraModels(userIds, dto.make);
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
return [];
|
return [];
|
||||||
|
@ -160,6 +160,7 @@ describe(ServerService.name, () => {
|
|||||||
smartSearch: true,
|
smartSearch: true,
|
||||||
duplicateDetection: true,
|
duplicateDetection: true,
|
||||||
facialRecognition: true,
|
facialRecognition: true,
|
||||||
|
importFaces: false,
|
||||||
map: true,
|
map: true,
|
||||||
reverseGeocoding: true,
|
reverseGeocoding: true,
|
||||||
oauth: false,
|
oauth: false,
|
||||||
|
@ -90,7 +90,7 @@ export class ServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getFeatures(): Promise<ServerFeaturesDto> {
|
async getFeatures(): Promise<ServerFeaturesDto> {
|
||||||
const { reverseGeocoding, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
const { reverseGeocoding, metadata, map, machineLearning, trash, oauth, passwordLogin, notifications } =
|
||||||
await this.configCore.getConfig({ withCache: false });
|
await this.configCore.getConfig({ withCache: false });
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -99,6 +99,7 @@ export class ServerService {
|
|||||||
duplicateDetection: isDuplicateDetectionEnabled(machineLearning),
|
duplicateDetection: isDuplicateDetectionEnabled(machineLearning),
|
||||||
map: map.enabled,
|
map: map.enabled,
|
||||||
reverseGeocoding: reverseGeocoding.enabled,
|
reverseGeocoding: reverseGeocoding.enabled,
|
||||||
|
importFaces: metadata.faces.import,
|
||||||
sidecar: true,
|
sidecar: true,
|
||||||
search: true,
|
search: true,
|
||||||
trash: trash.enabled,
|
trash: trash.enabled,
|
||||||
|
@ -74,6 +74,11 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
level: LogLevel.LOG,
|
level: LogLevel.LOG,
|
||||||
},
|
},
|
||||||
|
metadata: {
|
||||||
|
faces: {
|
||||||
|
import: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
machineLearning: {
|
machineLearning: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
url: 'http://immich-machine-learning:3003',
|
url: 'http://immich-machine-learning:3003',
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user